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:
@@ -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,¤t)==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",
|
||||
|
||||
@@ -24,6 +24,7 @@ db = (ROOT / "src/user_database.c").read_text()
|
||||
console = (ROOT / "src/user_console.c").read_text()
|
||||
admin = (ROOT / "src/admin_ssh_console.c").read_text()
|
||||
prelude = r'''
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
@@ -33,7 +34,10 @@ prelude = r'''
|
||||
typedef int esp_err_t;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE,
|
||||
ESP_ERR_NO_MEM, ESP_ERR_NOT_FOUND, ESP_ERR_NOT_ALLOWED,
|
||||
ESP_ERR_INVALID_RESPONSE, ESP_ERR_INVALID_VERSION };
|
||||
ESP_ERR_INVALID_RESPONSE, ESP_ERR_INVALID_VERSION, ESP_ERR_TIMEOUT };
|
||||
#define pdTRUE 1
|
||||
static bool snapshot_busy;
|
||||
static int last_wait;
|
||||
typedef void *SemaphoreHandle_t;
|
||||
#define portMAX_DELAY 0
|
||||
#define NVS_READWRITE 1
|
||||
@@ -45,7 +49,7 @@ static bool owner_current = true, remote = true, web = true, mismatch, cancel_pr
|
||||
static int notify_error = ESP_OK;
|
||||
static char revoked_name[17];
|
||||
static void secure_wipe(void *p, size_t n) { memset(p, 0, n); }
|
||||
static void xSemaphoreTake(void *m, int t) { (void)m; (void)t; assert(!locks++); }
|
||||
static int xSemaphoreTake(void *m, int t) { (void)m; last_wait=t; if (snapshot_busy) return 0; assert(!locks++); return pdTRUE; }
|
||||
static void xSemaphoreGive(void *m) { (void)m; assert(locks-- == 1); }
|
||||
static const char *esp_err_to_name(int e) { (void)e; return "injected error"; }
|
||||
static int nvs_open(const char *ns, int mode, int *h) {
|
||||
@@ -143,8 +147,11 @@ db_names = ["constant_time_equal", "all_zero", "user_database_username_valid",
|
||||
"find_free_user", "stored_keys_equal", "validate_database", "recount",
|
||||
"next_generation", "discard_candidate", "commit_candidate_locked", "initialize_user",
|
||||
"user_database_principal_is_current", "create_locked", "user_database_create",
|
||||
"mutate_user_begin", "user_database_delete", "user_database_set_role",
|
||||
"user_database_set_password"]
|
||||
"mutate_user_begin", "target_matches_locked", "delete_user", "set_role",
|
||||
"user_database_delete", "user_database_set_role", "user_database_get_accounts",
|
||||
"user_database_delete_current", "user_database_set_role_current",
|
||||
"set_password", "user_database_set_password", "user_database_set_password_current",
|
||||
"user_database_generate_password_value"]
|
||||
console_names = ["print_usage", "revoke_user_network_sessions", "read_password",
|
||||
"show_generated_password", "mutation_currentness", "add_user", "change_password",
|
||||
"parse_key_index", "command_user_inner", "command_user"]
|
||||
|
||||
@@ -31,7 +31,8 @@ typedef int *SemaphoreHandle_t;
|
||||
#define pdMS_TO_TICKS(x) (x)
|
||||
#define CONSOLE_COMPLETION_OUTPUT_CAPACITY 1024U
|
||||
static unsigned lock_depth, ticks, runs, actions;
|
||||
static uint32_t serial_settings_executed;
|
||||
static uint32_t serial_settings_executed, account_settings_executed;
|
||||
static void web_account_settings_execute(uint32_t id) { assert(!lock_depth); account_settings_executed = id; }
|
||||
static unsigned serial_settings_preceding_runs, queue_send_wait;
|
||||
static void web_serial_settings_execute(uint32_t id) {
|
||||
assert(!lock_depth);
|
||||
|
||||
@@ -340,5 +340,16 @@ int main(void)
|
||||
pump(worker_task);
|
||||
assert(runs == before_serial + 5 && serial_settings_executed == 17 && !s_request_queue->count);
|
||||
puts("PASS: typed Serial admission uses zero wait on success/full queue, preserves all four queued UART requests and FIFO execution, no command-string dispatch");
|
||||
assert(admin_ssh_console_submit_account_settings(0) == ESP_ERR_INVALID_STATE);
|
||||
s_dispatch_ready = false;
|
||||
assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_INVALID_STATE);
|
||||
s_dispatch_ready = true; queue_full = true;
|
||||
assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0);
|
||||
queue_full = false;
|
||||
assert(admin_ssh_console_submit_serial_settings(21) == ESP_OK);
|
||||
assert(admin_ssh_console_submit_account_settings(22) == ESP_OK && queue_send_wait == 0);
|
||||
pump(worker_task);
|
||||
assert(serial_settings_executed == 21 && account_settings_executed == 22 && runs == before_serial + 5);
|
||||
puts("PASS: typed Accounts uses same bounded queue with nonblocking admission and isolated dispatcher routing");
|
||||
puts("PASS: admission/identity, two owners, completion contention/reopen, history, queued stale/revoked work, UART dispatch, hidden/disconnected prompts, exit-to-SELF_CLOSE, deferred rejection/drain/close, 5s output backpressure");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user