Add typed account and password settings

- Add admin account list, create, role, delete, and password workflows
- Execute identity-checked mutations through the existing dispatcher
- Bound queued credential lifetime and wipe transient secrets
- Add explicit password generation with saved-value acknowledgement
- Handle self-revocation and uncertain outcomes without automatic
  retries
- Register optional account routes without disrupting terminal
  transports
- Expand host regressions and document contracts and pending target
  checks

Validated host suites and pio run; hardware validation remains pending.
This commit is contained in:
2026-09-08 09:27:02 +02:00
parent 42548f6334
commit 94433ef975
30 changed files with 1864 additions and 48 deletions
+101
View File
@@ -39,8 +39,109 @@ static void unchanged(const stored_database_t *before)
assert(all_zero(s_candidate,sizeof(*s_candidate)));
assert(!locks);
}
static void typed_account_tests(void)
{
reset(); user_database_accounts_t list;
assert(user_database_get_accounts(&list)==ESP_OK && last_wait==0 && list.count==3);
assert(!strcmp(list.users[1].username,"other"));
user_database_account_t other=list.users[1], admin=list.users[0];
snapshot_busy=true; memset(&list,0xff,sizeof(list));
assert(user_database_get_accounts(&list)==ESP_ERR_TIMEOUT && all_zero(&list,sizeof(list)));
snapshot_busy=false;
assert(user_database_delete_current(&admin)==ESP_ERR_INVALID_STATE);
assert(user_database_set_role_current(&admin,USER_ROLE_USER)==ESP_ERR_INVALID_STATE);
assert(!writes && !commits);
assert(user_database_set_role_current(&other,USER_ROLE_ADMIN)==ESP_OK);
unsigned saved=commits;
assert(user_database_delete_current(&other)==ESP_ERR_NOT_FOUND && commits==saved);
assert(user_database_set_role_current(&other,USER_ROLE_USER)==ESP_ERR_NOT_FOUND);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
for (fail_stage=1;fail_stage<=3;++fail_stage) {
stored_database_t before=s_database;
assert(user_database_delete_current(&other)==ESP_FAIL); unchanged(&before);
assert(user_database_set_role_current(&other,USER_ROLE_USER)==ESP_FAIL); unchanged(&before);
}
fail_stage=0; assert(user_database_delete_current(&other)==ESP_OK);
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_USER,(const uint8_t *)"test-password",13)==ESP_OK);
assert(user_database_delete_current(&other)==ESP_ERR_NOT_FOUND);
assert(user_database_set_role_current(&other,USER_ROLE_ADMIN)==ESP_ERR_NOT_FOUND);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
assert(user_database_delete_current(&other)==ESP_OK);
assert(all_zero(s_candidate,sizeof(*s_candidate)) && !locks);
s_initialized=false; memset(&list,0xff,sizeof(list));
assert(user_database_get_accounts(&list)==ESP_ERR_INVALID_STATE && all_zero(&list,sizeof(list)));
assert(user_database_delete_current(NULL)==ESP_ERR_INVALID_ARG);
}
static void typed_password_tests(void)
{
reset(); user_database_accounts_t list;
assert(user_database_get_accounts(&list)==ESP_OK);
user_database_account_t other=list.users[1], admin=list.users[0];
const uint8_t password[]="quote\"slash\\ space";
for (unsigned stage=1;stage<=5;++stage) {
fail_stage=stage; stored_database_t before=s_database;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_FAIL);
unchanged(&before);
}
fail_stage=0; writes=commits=0;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_OK);
assert(writes==1 && commits==1 && s_database.users[1].auth_generation==other.auth_generation+1);
assert(all_zero(s_candidate,sizeof(*s_candidate)));
stored_database_t before=s_database; unsigned rng=random_calls;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
unchanged(&before); assert(writes==1 && commits==1 && random_calls==rng);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
assert(user_database_delete_current(&other)==ESP_OK);
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_USER,password,sizeof(password)-1)==ESP_OK);
before=s_database; rng=random_calls;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
unchanged(&before); assert(random_calls==rng);
assert(user_database_set_password_current(NULL,password,sizeof(password)-1)==ESP_ERR_INVALID_ARG);
other.user_id=0;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
memset(other.username,'x',sizeof(other.username));
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_INVALID_ARG);
assert(user_database_set_password_current(&admin,(const uint8_t *)"short",5)==ESP_ERR_INVALID_ARG);
/* Own password is allowed even for the last administrator; old principal is stale. */
assert(user_database_set_password_current(&admin,password,sizeof(password)-1)==ESP_OK);
bool current=true; assert(user_database_principal_is_current(&actor,&current)==ESP_OK && !current);
assert(s_database.admin_count==1);
/* With a second admin, canonical self role/delete invariants allow both. */
assert(user_database_set_role((const uint8_t *)"other",5,USER_ROLE_ADMIN)==ESP_OK);
assert(user_database_get_accounts(&list)==ESP_OK); admin=list.users[0];
assert(user_database_set_role_current(&admin,USER_ROLE_USER)==ESP_OK);
assert(user_database_get_accounts(&list)==ESP_OK); admin=list.users[0];
assert(user_database_delete_current(&admin)==ESP_OK);
reset(); before=s_database; rng=random_calls;
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_ADMIN,password,sizeof(password)-1)==ESP_ERR_INVALID_STATE);
unchanged(&before); assert(!writes && !commits && rng==random_calls);
for (unsigned i=3;i<USER_DATABASE_MAX_USERS;++i) {
char name[17]; snprintf(name,sizeof(name),"account%u",i);
assert(user_database_create((const uint8_t *)name,strlen(name),USER_ROLE_USER,password,sizeof(password)-1)==ESP_OK);
}
before=s_database; rng=random_calls; unsigned saved=commits;
assert(user_database_create((const uint8_t *)"extra",5,USER_ROLE_USER,password,sizeof(password)-1)==ESP_ERR_NO_MEM);
unchanged(&before); assert(commits==saved && rng==random_calls);
/* RNG-only helper is independent of initialized storage and leaves all DB state alone. */
s_initialized=false; s_mutex=NULL;
for (unsigned mode=0;mode<2;++mode) {
user_database_generated_password_t generated; memset(&generated,0xa5,sizeof(generated));
fail_stage=mode ? 4 : 0;
assert(user_database_generate_password_value(&generated)==(mode ? ESP_FAIL : ESP_OK));
if (mode) assert(all_zero(&generated,sizeof(generated)));
else {
assert(generated.password_length==24 && strlen((const char *)generated.password)==24);
for (size_t i=0;i<24;++i) assert(strchr((const char *)s_generated_alphabet,generated.password[i]));
}
assert(!memcmp(&before,&s_database,sizeof(before)) && commits==saved && !locks);
secure_wipe(&generated,sizeof(generated));
}
assert(user_database_generate_password_value(NULL)==ESP_ERR_INVALID_ARG);
}
int main(void)
{
typed_account_tests();
typed_password_tests();
const char *supported[]={
"user add fresh user", "user add fresh admin", "user password other",
"user delete other --force", "user role other admin --force",