feat(manager-core): repos + admin patch for Phase 3.5 schema
* admin_user_repo: surface instance_role + email on AdminUserRow / Credentials; create() now takes instance_role; add update_instance_role, list_active_owners, count_other_active_owners. * admin_users_api: DTO + create/patch accept instance_role (defaults to Admin on create — only env-var bootstrap defaults to Owner). PATCH and DELETE enforce the last-owner guard alongside the existing last-active-admin guard. * app_members_repo: new — implements AuthzRepo::membership via the app_members table plus upsert/remove/list_for_user/list_for_app. * api_key_repo: new — create / find_active_by_prefix / touch_last_used / list_for_user / get / delete_by_id_and_user / expire_all_for_user. Separates ApiKeyRow (no hash) from ApiKeyVerification (hash, for the middleware verifier) so handlers can't leak the hash. * auth_bootstrap + picloud tests: pass Owner on the bootstrap seed and on the test admin seed respectively; in-memory test repo implements the new trait methods. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,15 @@ pub async fn bootstrap_first_admin_with<R: AdminUserRepository + ?Sized>(
|
||||
(None, None) => return Err(BootstrapError::MissingPassword),
|
||||
};
|
||||
|
||||
repo.create(&username, &password_hash).await?;
|
||||
// Bootstrap admin is always seeded as Owner — Phase 3.5 keys the
|
||||
// first row to full instance control. Subsequent admins minted via
|
||||
// the API default to Admin and can be promoted explicitly.
|
||||
repo.create(
|
||||
&username,
|
||||
&password_hash,
|
||||
picloud_shared::InstanceRole::Owner,
|
||||
)
|
||||
.await?;
|
||||
info!(username = %username, "bootstrapped initial admin user");
|
||||
Ok(())
|
||||
}
|
||||
@@ -130,7 +138,7 @@ mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use picloud_shared::AdminUserId;
|
||||
use picloud_shared::{AdminUserId, InstanceRole};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::admin_user_repo::{AdminUserCredentials, AdminUserRepositoryError, AdminUserRow};
|
||||
@@ -167,11 +175,14 @@ mod tests {
|
||||
&self,
|
||||
username: &str,
|
||||
_password_hash: &str,
|
||||
instance_role: InstanceRole,
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
let row = AdminUserRow {
|
||||
id: AdminUserId::new(),
|
||||
username: username.to_string(),
|
||||
is_active: true,
|
||||
instance_role,
|
||||
email: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
last_login_at: None,
|
||||
@@ -193,6 +204,13 @@ mod tests {
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn update_instance_role(
|
||||
&self,
|
||||
_i: AdminUserId,
|
||||
_r: InstanceRole,
|
||||
) -> Result<AdminUserRow, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn set_active(
|
||||
&self,
|
||||
_i: AdminUserId,
|
||||
@@ -215,6 +233,15 @@ mod tests {
|
||||
) -> Result<i64, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn list_active_owners(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn count_other_active_owners(
|
||||
&self,
|
||||
_i: AdminUserId,
|
||||
) -> Result<i64, AdminUserRepositoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -245,7 +272,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn populated_db_is_noop() {
|
||||
let repo = InMemoryRepo::default();
|
||||
repo.create("seeded", "x").await.unwrap();
|
||||
repo.create("seeded", "x", InstanceRole::Owner).await.unwrap();
|
||||
let env = BootstrapEnv {
|
||||
username: Some("alice".into()),
|
||||
password: Some("supersecret".into()),
|
||||
|
||||
Reference in New Issue
Block a user