feat: store app-only Graph credentials in the staff-api tenant (PPT-2000) - #442
feat: store app-only Graph credentials in the staff-api tenant (PPT-2000)#442camreeves wants to merge 3 commits into
Conversation
The admin-consent flow registered the Bookings Visualiser application
with app-only Graph permissions (Calendars.ReadWrite, Group.Read.All,
User.Read.All) but never minted a secret for it and discarded its
client id - the consented access was unusable, and calendar credentials
still had to be entered by hand in Backoffice.
The visualiser registration now mints a client secret and the flow
writes {tenant, client_id, client_secret} into the staff-api tenant for
the domain (created when missing, updated otherwise - the flow owns the
domain's Microsoft configuration, as it already does for login_url and
outlook_config). Existing delegated tenants are switched to app-only;
delegated mode can be re-enabled in Backoffice. Creating the tenant
before the outlook step also fixes the silent no-op where outlook_config
had nothing to attach to on a fresh domain.
Credentials are encrypted at rest by the model (Level::NeverDisplay)
and surface on the progress page as a new 'Connect room calendar
access' step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flow looked up the staff-api tenant and built the Outlook add-in URLs from the host in the request. Backoffice can drive the integration for any domain, so integrating one authority while browsing another wrote the new Microsoft configuration onto the wrong tenant - observed on dev, where integrating the demo authority (azure-demo.placeos-dev.aca.im) overwrote the dev domain's own tenant (placeos-dev.aca.im), switching it off delegated access and replacing its calendar credentials. The authority record already supplied the domain used for the auth app's redirect URI; use it for the tenant lookup, the Outlook identifier URIs and the add-in URLs too. Only the consent callback URL still derives from the request host, where it has to - it must match the redirect URI registered on the management application. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug found by the first dev e2e run — fixed in
|
Re-verified end to end on
|
The visualiser application was granted Calendars.ReadWrite, Group.Read.All and User.Read.All. Room mailboxes were therefore readable, but the Graph places API - which room discovery uses to enumerate rooms and room lists - returned 403, verified against the sandbox tenant with a provisioned app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Stacked on #441 (which stacks on #440). Closes the gap where the 1-click flow registered the app-only "PlaceOS Bookings Visualiser" application — with admin-consented
Calendars.ReadWrite/Group.Read.All/User.Read.Allapplication permissions — but never minted a secret for it and discarded its client id, so the consented app-only Graph access was unusable and calendar credentials still had to be typed into Backoffice → Admin → Staff API by hand.What changes
create_appnow mints a client secret for the visualiser app (sameaddPasswordcall the auth app already used, wrapped in the replication retry).{tenant, client_id, client_secret}into the staff-apiTenantfor the domain — created if the domain has none, updated otherwise. The model encrypts credentials at rest (Level::NeverDisplay) and validates them asOffice365Config.delegated: false). Rationale: the flow already owns the domain's Microsoft configuration (login_url,outlook_config) and the visualiser app exists precisely to provide app-only access. Delegated mode remains one toggle away in Backoffice.create_outlook_config, which previously silently no-oped on a fresh domain (Tenant.find_by?→ log + return).rescue flow_errornaming, verbose block).Verification
crystal build --no-codegenclean on 1.21; ameba clean on all touched files.spec/tenant_consent_spec.cr): create-when-missing and switch-existing-delegated — both assert the stored ciphertext decrypts to exactly the written credential and thatplace_calendar_clientbuilds from the row.The triggers service (
GraphSecretExpiryFinder) will start probing the stored credential for secret-expiry metadata — that's existing behaviour for any non-delegated office365 tenant.🤖 Generated with Claude Code