Skip to content

Identity & Role Projection — omadia as a team-player in the existing IT landscape (Principal = user|role everywhere) #333

Description

@iret77

Summary & vision

Make omadia an aggregator/projector of identity and roles, not an owner of them. omadia
is not an ERP and must not become one — but ERPs, HR systems, and IdPs (which already are
the org's systems of record for who exists and who is responsible for what) dock onto
omadia. omadia should project a live user/role view from those sources, joined on a
primary key, rather than maintain a second/third copy that has to be kept in sync. This is
the concrete expression of omadia's differentiator: it is itself a team-player in the
company's existing IT landscape
, not another island.

This is already true for authentication (see "What already exists"). This issue extends
the same "project, don't own" pattern from access identity to org roles + employee
attributes
, and establishes a platform-wide paradigm for how any module or agent
addresses a person.

Foundation for: omadia Conductor (#321, role-addressed steps & reporting) and the
omadia Facilitator (#330, report/escalation to a user or role). Both should consume
this layer instead of owning role tables.

The paradigm (headline)

Principal = user:<id> | role:<key> is the DEFAULT addressee type everywhere in omadia.

Any omadia module or agent that targets a person as the destination of something — an
escalation target, a report/notification recipient, an approval/decision step, an assignment,
a hand-off — MUST by default accept either a specific user or a role.

  • Restricting a target to user-only (no role) is the exception, permitted ONLY when
    it is technically or legally necessary to deal with one identified natural person
    (e.g. a legally binding personal sign-off, a per-person secret, a regulatory 4-eyes record
    tied to named individuals).
  • Such a user-only restriction MUST be explicitly justified and documented at the point of
    definition
    — mirroring the existing multi_instance_justification precedent (a manifest
    that sets multi_instance: false must say why; see specs/001-multi-orchestrator-runtime).
    The default is role-capable; opting out costs a justification.
  • Rationale: people change roles, go on leave, leave the company. Hard-wiring a workflow to a
    named person is brittle and a maintenance burden; addressing a role (late-bound to the
    current holder) is the resilient default. The user-only case should be rare and deliberate.

Concretely, this paradigm governs (non-exhaustive): Conductor human-step addressees,
Conductor escalation/fallback targets, report and interim-status recipients, notifications,
assignments, and any future "send to / ask / require sign-off from " surface.

What already exists (grounded)

Capability Status Evidence
Auth is provider-based, pluggable, hot-swappable; allowed set via AUTH_PROVIDERS (default local,entra); kernel knows no concrete provider impl EXISTS middleware/src/auth/providerRegistry.ts (comment: "Entra moves to a plugin V1.x"), config.ts (AUTH_PROVIDERS)
Entra / OIDC provider EXISTS middleware/src/auth/providers/EntraProvider.ts (kind: 'oidc', "Microsoft / Entra ID")
Entra is already the access source-of-truth: users are IdP-managed; omadia keeps only a thin projection row + an email whitelist EXISTS middleware/src/auth/requireAuth.ts ("IdP-managed users don't self-provision"), auth/whitelist.ts
users stores provider + provider_user_id (= Entra oid), insulating from email churn EXISTS middleware/src/auth/migrations/0001_users.sql
Identity correlation channel ↔ auth ↔ KG in seed form: for Entra provider_user_id is the AAD object id, merged as aadObjectId; resolveIdentity(ChannelUserRef) → PlatformIdentity seam EXISTS (seed) middleware/src/index.ts (~2126–2151), middleware/src/channels/coreApi.ts (resolveIdentity)
Org roles: still only users.role IN ('admin'); Entra groups / app-roles are NOT pulled; no HR roster projection ABSENT 0001_users.sql (users_role_chk); no groups/ms-graph integration found
Odoo HR master data (hr.employee, hr.department) reachable via the knowledge graph (candidate role/attribute source) EXISTS (as data) middleware/packages/harness-verifier/src/graphLookupTool.ts
TTL / cache / cleanup patterns for a projection cache EXISTS (reuse) platform/flowState.ts, plugins/jobScheduler.ts

Conclusion: "project, don't own" is already real for access. The gap is extending it to
roles + attributes, and formalizing the Principal-everywhere paradigm.

Architecture — two pluggable source classes + a join/projection layer

1. Identity / Access providers (EXISTS — generalize)

The authentication source of truth: who can log in, stable identity. Entra/OIDC today;
local password as the standalone fallback. Already a registry (ProviderRegistry /
ProviderCatalog).

2. Attribute / Role sources (NEW)

The source of truth for org roles + employee attributes (department, title, manager,
availability, …)
. Pluggable, independently configurable from the IdP:

  • The IdP itself — Entra groups / app-roles via token claims or MS Graph (when the org
    expresses roles in Entra). Not pulled today; a clean extension at the provider seam.
  • A connector — Odoo HR / ERP as the roster + role SoT (hr.employee + position/department),
    matched on work_email.
  • Both, joined — the common real case: access via Entra, business roles via HR.

3. Identity resolution / join layer (NEW)

Builds a live, derived Principal by correlating channel user ↔ IdP identity ↔ role/attribute
source(s) on a configurable primary key (email default; provider oid as the churn-proof
anchor). Extends the existing resolveIdentity seam and the aadObjectId merge. Output: a
Principal carrying current roles, attributes, and per-channel addresses (e.g. the Teams
conversation reference / AAD oid needed to actually deliver to a holder). Nobody maintains
user/role data in omadia.

4. Caching & freshness (NEW)

Resolution is conceptually live, but cached with a short TTL + on-demand refresh + change
webhooks where available (Entra change notifications / Odoo polling), so "always up to date"
does not mean hammering the source every turn. Reuse flowState/jobScheduler patterns.

5. Standalone fallback (NEW, narrow)

When omadia runs with no external identity/role source configured (fully stand-alone, like
OpenClaw / Hermes), and ONLY then, omadia maintains a minimal local user + role/membership
store. This is the single mode in which omadia owns this data. Conductor's previously-proposed
local conductor_role_assignments table (#321) is demoted to this fallback, not the
primary model.

Recommended modeling: generalize the provider registry

Model the role/attribute sources as a generalization of the existing auth-provider registry:
one registry pattern, two facets — AuthnProvider (who can log in) and Attribute/RoleSource
(who is who / who holds what). A source may implement both facets (Entra: authn + groups) or
just one (Odoo HR: attributes/roles only). This keeps one consistent, kernel-enforced,
manifest-declared seam — the same shape the codebase already uses for auth and for plugin
capabilities (permissionSurface). (Alternative considered: two separate registries — rejected
for adding a parallel structure with no clear benefit.)

Practical concerns (must be designed in)

Concern Direction
Correlation key Email as pragmatic default (Entra UPN/mail ↔ Odoo work_email ↔ Teams mail); provider oid as churn-proof anchor. Configurable.
Unmatched users Explicit "unresolved principal" diagnostic + an override mapping table for edge cases (guests, shared mailboxes, in-HR-not-in-Entra, name/email churn). Never a silent mis-match — especially for report/escalation targets.
Role source choice Entra groups (easy, often IT-centric) vs HR (real business roles). Source configurable independently of the IdP; joining both supported.
Freshness vs cost TTL cache + on-demand refresh + change webhooks where available.
Channel reachability The layer must yield each holder's channel address so a role-addressed delivery (e.g. Facilitator Teams DM, #330 B3) can actually reach them.
Privacy / data minimization Project only the attributes a feature needs; respect existing privacy guards; cache with bounded TTL; honor GDPR posture (the projection is derived, not a new master copy).

How Conductor (#321) and Facilitator (#330) consume this

Acceptance criteria (V1)

  • An operator can configure (a) an IdP (Entra) and (b) a role/attribute source (Entra
    groups/app-roles and/or an Odoo-HR roster), independently.
  • A logged-in user (via Entra) is resolved to a derived Principal carrying current roles and
    attributes pulled from the configured source(s), correlated on the configured key — with no
    per-user data maintained in omadia
    .
  • A role:<key> resolves to the current holder(s) live; changing the role in the source
    (e.g. HR reassignment) changes who omadia targets, without any omadia-side edit.
  • An unmatched/unresolved user surfaces a clear diagnostic and can be reconciled via an override
    mapping — never silently mis-addressed.
  • With no external source configured, omadia falls back to a minimal local user/role store and
    still functions stand-alone.
  • The Principal paradigm is enforced: a target surface that is user-only must carry a documented
    justification; otherwise it accepts user or role by default.

Scope / out of scope

  • In scope: the source-class abstraction + registry generalization, the join/projection
    layer + correlation, caching, the standalone fallback, and the Principal-everywhere paradigm
    (incl. the user-only justification rule).
  • Out of scope: building the specific connectors as products (the Odoo HR reader, MS Graph
    group reader) beyond a reference implementation; the HR-side role-movement policy; any ERP
    functionality in omadia Core.

Sequencing & dependencies

  1. This layer (source abstraction + resolution + paradigm) — foundational.
  2. A reference Attribute/Role source (Entra groups and/or Odoo HR) on top.
  3. Conductor (feat(conductor): Spec 005 — Omadia Conductor (deterministic engine, designer, human-in-the-loop) — US1–US8 implemented + live-tested #321) wires its RoleResolver to this layer; demotes the local table to fallback.
  4. Facilitator (omadia Facilitator — agent that moderates group chats to a defined outcome (+ Conductor JIT/ephemeral workflows & Core group primitives) #330) consumes role-addressed reporting/escalation through it.

Drafted with Claude Code as the conception artifact for this feature; refine inline as needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions