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:
Edouard Vanbelle
2026-06-01 20:37:36 +02:00
parent ce25bfa209
commit 5fab0532dc
10 changed files with 399 additions and 45 deletions
@@ -0,0 +1,25 @@
-- ════════════════════════════════════════════════════════════════════════════
-- Prelude for magic-link external authentication
-- ════════════════════════════════════════════════════════════════════════════
-- This migration is purely additive — it lands the schema bits needed by the
-- subsequent magic-link work without altering existing rows or behaviour:
--
-- * `given_name` / `family_name` — optional human-readable identity fields.
-- Populated from OIDC standard claims (given_name, family_name) at JIT
-- provisioning. External users start with both NULL; either side can be
-- filled in later via a profile-edit endpoint.
--
-- Note on username length: `auth.users.username` is already `TEXT` with no
-- DB-level length constraint, so it can already hold the 254-char RFC 5321
-- maximum required for email-as-username. The widening happens at the
-- entity-level validator (`User::validate_username`), not the schema.
ALTER TABLE auth.users
ADD COLUMN IF NOT EXISTS given_name TEXT NULL,
ADD COLUMN IF NOT EXISTS family_name TEXT NULL;
COMMENT ON COLUMN auth.users.given_name IS
'Optional first/given name. Populated from OIDC standard claim `given_name` at JIT provisioning; settable via profile-edit endpoint. NULL until explicitly set.';
COMMENT ON COLUMN auth.users.family_name IS
'Optional last/family name. Populated from OIDC standard claim `family_name` at JIT provisioning; settable via profile-edit endpoint. NULL until explicitly set.';