feat(user): add given_name/family_name auth.users
reflect OIDC schema migration, User entity additions, and three defense-in-depth gaps closed, with all 280 unit tests and 13 Hurl files green infrastructure (lettre + EmailSender port). - Migration migrations/20260612000003_users_username_email_login.sql — adds nullable given_name/family_name columns to auth.users. - User entity (src/domain/entities/user.rs) — has_login_credential() placeholder-check encapsulation, set_username revalidating setter, given/family-name fields + getters/setters, validate_username widened 32→254 and now accepts email shape. from_data_full extended with two new params; all 7 callsites in user_pg_repository.rs updated. - Schema-side legacy guards (src/application/services/auth_application_service.rs) — bumped the duplicated 32-char check in setup_create_admin and admin_create_user to 254 to match. - Gap #1 (subject_group_service.rs) — add_member now rejects external candidates with an audit-logged AccessDenied. Service gained an Arc<UserPgRepository> field, wired through DI. New integration test test_external_user_cannot_be_added_as_member. - Gap #2 (user_repository.rs + auth_ports.rs + user_pg_repository.rs) — list_users/search_users gained an include_external: bool param defaulting effectively to false everywhere internal-user-facing. auth_application_service exposes a new list_users_including_external for the admin surface. - Gap #3 (pg_acl_engine.rs) — expand_user now SELECTs is_external and skips INTERNAL_GROUP_ID for externals; defaults to is_external=true on missing user to fail closed.
This commit is contained in:
@@ -127,7 +127,16 @@ impl PgAclEngine {
|
||||
|
||||
/// Expand a user subject into the set of subject UUIDs that should match
|
||||
/// in `access_grants`: the user's own UUID, every group the user is
|
||||
/// transitively a member of, and the implicit `INTERNAL_GROUP_ID`.
|
||||
/// transitively a member of, and (for internal users only) the implicit
|
||||
/// `INTERNAL_GROUP_ID`.
|
||||
///
|
||||
/// External users (`auth.users.is_external = TRUE`) do NOT belong to
|
||||
/// the Internal virtual group — they are grant-only recipients whose
|
||||
/// access is determined exclusively by explicit grants on their
|
||||
/// `user_id` or on subject groups they were explicitly added to.
|
||||
/// `SubjectGroupService::add_member` rejects externals, so the only
|
||||
/// path by which an external user reaches a resource is via a
|
||||
/// `subject_type='user'` grant.
|
||||
///
|
||||
/// This is the **only** place transitive membership is walked. A future
|
||||
/// closure-table swap-in (Option 3 in the design doc) replaces just the
|
||||
@@ -147,10 +156,25 @@ impl PgAclEngine {
|
||||
|
||||
let mut set: HashSet<Uuid> = HashSet::new();
|
||||
set.insert(user_id);
|
||||
// The Internal virtual group: implicit membership for every
|
||||
// authenticated user. Once the external-users work lands this will
|
||||
// narrow to `if !user.is_external { ... }`.
|
||||
set.insert(INTERNAL_GROUP_ID);
|
||||
|
||||
// Look up `is_external` for the caller — external users do not
|
||||
// belong to the Internal virtual group. Unknown user (no row) is
|
||||
// treated as external to fail closed: a deleted or bogus user_id
|
||||
// must not gain implicit Internal membership.
|
||||
counters.sql_queries.fetch_add(1, Ordering::Relaxed);
|
||||
let is_external: bool =
|
||||
sqlx::query_scalar("SELECT is_external FROM auth.users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("PgAcl", format!("lookup is_external: {e}"))
|
||||
})?
|
||||
.unwrap_or(true);
|
||||
|
||||
if !is_external {
|
||||
set.insert(INTERNAL_GROUP_ID);
|
||||
}
|
||||
|
||||
if let Some(repo) = &self.group_repo {
|
||||
counters.sql_queries.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
Reference in New Issue
Block a user