Skip to main content

Authentication & Role Management Updates

This document summarizes the recent authentication and role-management changes across backend, CLI, and Desk/custom frontend UX.

What changed

1) Persistent roles on local users

LocalUser now stores explicit roles in the user record.

  • Field: roles: list[str]
  • Default: ["User"]
  • Location: libs/framework-m-core/src/framework_m_core/doctypes/user.py

This enables role checks to use persisted role values instead of empty/default runtime placeholders.

2) Role propagation through identity and tokens

LocalIdentityAdapter now propagates roles end-to-end:

  • UserContext.roles is populated from LocalUser.roles
  • get_attributes(...)["roles"] returns persisted roles
  • JWT payload includes roles
  • Location: libs/framework-m-standard/src/framework_m_standard/adapters/auth/local_identity.py

This makes RBAC decisions, attribute-based checks, and downstream consumers consistent with stored user roles.

3) OAuth/social provisioning persists roles

Local user creation in web adapter now persists incoming roles and defaults to User when not provided.

  • OAuth first-login creation now writes roles
  • Default for social-created users remains User
  • Location: libs/framework-m-standard/src/framework_m_standard/adapters/web/app.py

4) New CLI flow: create-user

Added a first-class user creation command with role input.

  • New command: m create-user
  • Existing command retained: m create-admin (backward-compatible alias)
  • Role normalization:
    • admin, administrator -> Admin
    • user -> User
    • other inputs title-cased
  • Locations:
    • Command implementation: libs/framework-m-standard/src/framework_m_standard/cli/admin.py
    • Entry points: libs/framework-m-standard/pyproject.toml

CLI usage

# explicit
m create-user --email user@example.com --password "strong-pass" --role User --name "App User"

# interactive role prompt
m create-user --email admin@example.com --password "strong-pass"

# backward-compatible alias (forces Admin role)
m create-admin --email admin@example.com --password "strong-pass"

5) Desk and template auth/error UX improvements

Authentication and permission failures are now surfaced as user-visible, contextual messages in both the main frontend and scaffolded custom UI/template paths.

Key improvements:

  • Login page displays backend-provided error messages (message/detail) instead of generic failures
  • Permission/auth failures (401/403) get clearer action-aware text in data providers
  • List/form/meta load failures show inline error banners instead of only console/alert behavior

Primary locations:

  • Main frontend:
    • frontend/src/providers/frameworkMDataProvider.ts
    • frontend/src/providers/authProvider.ts
    • frontend/src/pages/LoginPage.tsx
    • frontend/src/pages/ListView.tsx
    • frontend/src/pages/FormView.tsx
    • frontend/src/hooks/useDocTypeMeta.ts
  • Shared desk package + template:
    • libs/framework-m-desk/src/data.ts
    • libs/framework-m-desk/src/auth.ts
    • libs/framework-m/src/framework_m/templates/frontend/src/App.tsx
    • libs/framework-m/src/framework_m/templates/frontend/src/pages/LoginPage.tsx

LocalUser Management List Managing users and roles directly from the Desk.

Activity Log Audit Trail The system-wide activity log capturing CRUD events.

6) DocType Meta.permissions and role matching behavior

Role checks for DocType actions are resolved from Meta.permissions and matched against persisted user roles.

Example:

class Meta:
permissions: ClassVar[dict[str, list[str]]] = {
"create": ["Admin"],
"read": ["All"],
"write": ["Manager"],
"delete": ["Admin", "Manager"],
}

Important behavior:

  • Matching for role names is exact string match (set(user_roles) & set(allowed_roles)).
  • Use canonical role names consistently (recommended: Admin, Manager, User).
  • All is treated as wildcard for that action (case-insensitive).
  • Admin bypass is handled by RBAC adapter defaults (Admin, System).

Practical implication:

  • If you configure "write": ["manager"] but user role is "Manager", write will be denied.
  • Since CLI role creation normalizes to canonical casing, docs/examples should also use canonical role names.

Behavior notes

  • RBAC role checks operate on role names, not email addresses.
  • Matching semantics remain role-based and policy-driven; persisted roles now supply reliable input.
  • New users created via local CLI default to supplied role; social-created users default to User unless another role list is passed at creation path.
  • For DocType permissions, role names are case-sensitive matches; keep role casing consistent in both user records and Meta.permissions.

7) Security Model (RLS, Read All, Anonymous Access)

Framework M now enforces these RLS and object-level access rules consistently:

  • requires_auth = False allows anonymous access for that DocType.
  • apply_rls = True owner-scopes non-admin users by default.
  • read: ["All"] in Meta.permissions grants global read and bypasses owner-scoping for both single-document reads and list filters.
  • If no read-all grant is present, non-admin logged-in users remain owner-scoped.
  • Admin/System users always bypass RLS owner filters.
  • Todo is configured with apply_rls = False for global testing and demo flows.

Practical examples:

  • A non-owner user can read a specific record when the DocType has read: ["All"].
  • The same user receives {"owner": user_id} list filters when the DocType has role-based read permissions without read-all grants.
  • Anonymous reads are allowed only when requires_auth = False.

Migration / compatibility

  • m create-admin still works and now delegates to the new flow.
  • Existing users without explicit role data may still resolve to default behavior where model defaults apply.
  • New records created after this change consistently include role data.

Validation completed for these changes

Targeted auth tests were updated and run for role propagation:

python -m pytest \
libs/framework-m-standard/tests/adapters/auth/test_local_identity.py \
libs/framework-m-standard/tests/adapters/auth/test_local_identity_extended.py

Result: passing in local validation during implementation.