|
| 1 | +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import { ObjectSchema, Field } from '@objectstack/spec/data'; |
| 4 | + |
| 5 | +/** |
| 6 | + * sys_scim_provider — Registered SCIM 2.0 connection (@better-auth/scim) |
| 7 | + * |
| 8 | + * Backed by `@better-auth/scim`'s `scimProvider` model. Each row is a SCIM |
| 9 | + * connection: a bearer token an external IdP (Okta / Entra / OneLogin) uses to |
| 10 | + * **auto-provision / deprovision** THIS environment's users. The environment is |
| 11 | + * the SCIM **Service Provider** (the receiver); the IdP is the SCIM **client** |
| 12 | + * (the sender). This is the paid Identity lifecycle (ADR-0071) — the mechanism |
| 13 | + * is OPEN (here, framework `plugin-auth`); enablement is entitlement-gated by |
| 14 | + * the cloud / EE license. |
| 15 | + * |
| 16 | + * `scim_token` holds the connection's bearer credential. With the plugin's |
| 17 | + * `storeSCIMToken: 'hashed'` (the default this env wires) it stores only a |
| 18 | + * HASH — the plaintext is returned exactly once at `/scim/generate-token`. Even |
| 19 | + * so, treat this object as sensitive: it is read-only over the generic data API |
| 20 | + * and the token is excluded from list views. |
| 21 | + * |
| 22 | + * All mutations route through @better-auth/scim's endpoints under |
| 23 | + * `/api/v1/auth/scim/*` (generate-token / delete-provider-connection) and the |
| 24 | + * SCIM 2.0 protocol under `/api/v1/auth/scim/v2/*`; the generic data layer is |
| 25 | + * read-only (see `enable.apiMethods`). |
| 26 | + * |
| 27 | + * @namespace sys |
| 28 | + */ |
| 29 | +export const SysScimProvider = ObjectSchema.create({ |
| 30 | + name: 'sys_scim_provider', |
| 31 | + label: 'SCIM Provider', |
| 32 | + pluralLabel: 'SCIM Providers', |
| 33 | + icon: 'users', |
| 34 | + isSystem: true, |
| 35 | + managedBy: 'better-auth', |
| 36 | + // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema. |
| 37 | + protection: { |
| 38 | + lock: 'full', |
| 39 | + reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.', |
| 40 | + docsUrl: 'https://docs.objectstack.ai/adr/0010-metadata-protection', |
| 41 | + }, |
| 42 | + description: 'SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment\'s users', |
| 43 | + displayNameField: 'provider_id', |
| 44 | + titleFormat: '{provider_id}', |
| 45 | + compactLayout: ['provider_id', 'organization_id'], |
| 46 | + |
| 47 | + listViews: { |
| 48 | + all: { |
| 49 | + type: 'grid', |
| 50 | + name: 'all', |
| 51 | + label: 'All', |
| 52 | + data: { provider: 'object', object: 'sys_scim_provider' }, |
| 53 | + // scim_token is intentionally excluded — never surface the credential. |
| 54 | + columns: ['provider_id', 'organization_id', 'created_at'], |
| 55 | + sort: [{ field: 'provider_id', order: 'asc' }], |
| 56 | + pagination: { pageSize: 50 }, |
| 57 | + }, |
| 58 | + }, |
| 59 | + |
| 60 | + fields: { |
| 61 | + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), |
| 62 | + |
| 63 | + provider_id: Field.text({ |
| 64 | + label: 'Provider ID', |
| 65 | + required: true, |
| 66 | + searchable: true, |
| 67 | + maxLength: 255, |
| 68 | + description: 'Stable SCIM provider identifier (e.g. "okta-scim")', |
| 69 | + group: 'Identity', |
| 70 | + }), |
| 71 | + |
| 72 | + scim_token: Field.text({ |
| 73 | + label: 'SCIM Token (hash)', |
| 74 | + required: false, |
| 75 | + readonly: true, |
| 76 | + maxLength: 1024, |
| 77 | + description: 'Hashed bearer credential for this SCIM connection — the plaintext is shown once at generate-token. Sensitive; do not expose.', |
| 78 | + group: 'Secret', |
| 79 | + }), |
| 80 | + |
| 81 | + organization_id: Field.text({ |
| 82 | + label: 'Organization', |
| 83 | + required: false, |
| 84 | + maxLength: 255, |
| 85 | + description: 'Organization scope of this token (org-scoped tokens restrict provisioning to that org)', |
| 86 | + group: 'System', |
| 87 | + }), |
| 88 | + |
| 89 | + user_id: Field.lookup('sys_user', { |
| 90 | + label: 'Owned By', |
| 91 | + required: false, |
| 92 | + description: 'User who generated this token (when provider-ownership is enabled)', |
| 93 | + group: 'System', |
| 94 | + }), |
| 95 | + |
| 96 | + created_at: Field.datetime({ label: 'Created At', defaultValue: 'NOW()', readonly: true, group: 'System' }), |
| 97 | + updated_at: Field.datetime({ label: 'Updated At', defaultValue: 'NOW()', readonly: true, group: 'System' }), |
| 98 | + }, |
| 99 | + |
| 100 | + indexes: [ |
| 101 | + { fields: ['provider_id'], unique: true }, |
| 102 | + { fields: ['organization_id'] }, |
| 103 | + { fields: ['user_id'] }, |
| 104 | + ], |
| 105 | + |
| 106 | + enable: { |
| 107 | + trackHistory: true, |
| 108 | + searchable: false, |
| 109 | + apiEnabled: true, |
| 110 | + // Mutations + token issuance go through @better-auth/scim's endpoints |
| 111 | + // under /api/v1/auth/scim/*; the generic data layer is read-only so the |
| 112 | + // credential cannot be written/bypassed through it. |
| 113 | + apiMethods: ['list'], |
| 114 | + trash: false, |
| 115 | + mru: false, |
| 116 | + }, |
| 117 | +}); |
0 commit comments