Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dd9fa95
feat: add ScimGroup model for SCIM protocol compliance
TerrifiedBug Mar 7, 2026
1965391
feat: add shared group mapping helpers, remove resolveScimRole
TerrifiedBug Mar 7, 2026
f295b09
feat: rewrite SCIM Groups GET/POST to use ScimGroup model instead of …
TerrifiedBug Mar 7, 2026
b5a1c9c
feat: rewrite SCIM Groups PATCH/PUT/DELETE to use mapping table
TerrifiedBug Mar 7, 2026
7f2ae94
feat: rename OIDC-specific UI labels to generic IdP Group Mappings
TerrifiedBug Mar 7, 2026
8300d40
docs: update authentication docs for unified group mapping
TerrifiedBug Mar 7, 2026
442754d
fix: address Greptile findings on SCIM group mutation handlers
TerrifiedBug Mar 7, 2026
eda1032
fix: PATCH remove no-op and stale mapping context on rename
TerrifiedBug Mar 7, 2026
5f6d8ef
fix: PUT stale mapping context and role downgrade in applyMappedMembe…
TerrifiedBug Mar 7, 2026
5f31824
feat: add ScimGroupMember model and TeamMember.source field
TerrifiedBug Mar 7, 2026
f4daf80
feat: rewrite group-mappings.ts as reconciliation module
TerrifiedBug Mar 7, 2026
fe31cbc
feat: rewrite SCIM Groups POST/GET to use ScimGroupMember + reconcili…
TerrifiedBug Mar 7, 2026
0eed7b3
feat: rewrite SCIM Groups [id] endpoints with ScimGroupMember + recon…
TerrifiedBug Mar 7, 2026
0dc7972
feat: trigger bulk reconciliation when group mappings are saved
TerrifiedBug Mar 7, 2026
2093448
feat: update OIDC login to use reconciliation for group sync
TerrifiedBug Mar 7, 2026
529669d
docs: update auth and SCIM docs for reconciliation model
TerrifiedBug Mar 7, 2026
79ee6c6
fix: wrap SCIM Groups POST in single transaction
TerrifiedBug Mar 7, 2026
1e58382
fix: handle SCIM filter-notation removes and fix default team fallback
TerrifiedBug Mar 8, 2026
f866045
Merge remote-tracking branch 'origin/main' into feat/unified-group-ma…
TerrifiedBug Mar 8, 2026
541a486
fix: write audit log before user deletion to avoid FK constraint viol…
TerrifiedBug Mar 8, 2026
54d454d
fix: make SCIM group DELETE atomic with reconciliation
TerrifiedBug Mar 8, 2026
2f1df4b
fix: case-insensitive SCIM filter matching and upsert default team fa…
TerrifiedBug Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions docs/public/operations/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,26 @@ OIDC settings are stored encrypted in the database. The client secret is encrypt

### Group mapping

VectorFlow can automatically assign users to teams based on their identity provider group memberships. Group mappings are configured from **Settings > Team & Role Mapping** and are **shared between OIDC and SCIM** — the same mapping table drives both protocols:
VectorFlow can automatically assign users to teams based on their identity provider group memberships. Group mappings are configured from **Settings > Team & Role Mapping** and are **shared between OIDC and SCIM** — the same mapping table drives both protocols.

- **OIDC-only deployments:** Team access is assigned on each login based on the groups claim in the OIDC token.
- **SCIM + OIDC deployments:** SCIM pre-provisions team access when your IdP pushes group membership changes. OIDC then refreshes team access on each sign-in, keeping mappings current.
Group sync is **off by default** and must be explicitly enabled. When enabled, VectorFlow operates in one of two modes depending on whether SCIM is active:

Group sync is **off by default** and must be explicitly enabled.
{% tabs %}
{% tab title="OIDC-only mode (SCIM disabled)" %}
Groups are read from the OIDC token on each login. Team memberships are **reconciled** — users are added to mapped teams **and removed** from teams they no longer belong to (based on the groups present in the token). Changes to group mappings in **Settings > Team & Role Mapping** take effect on the user's next login.
{% endtab %}
{% tab title="SCIM + OIDC mode (SCIM enabled)" %}
SCIM is the **primary lifecycle manager** for group memberships. Your IdP pushes group membership changes (create, update, remove) via SCIM, and VectorFlow tracks them internally. OIDC login acts as a **real-time refresh**, using the union of SCIM group data and token groups to reconcile team memberships. Changes to group mappings in **Settings > Team & Role Mapping** take effect immediately for all SCIM-managed users.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Manual assignments are preserved.** Team memberships assigned manually in the VectorFlow UI are never modified by automated group sync. If you want group sync to fully manage a user's membership on a team, remove the manual assignment first.
{% endhint %}

{% hint style="info" %}
**Highest role wins.** When a user belongs to multiple IdP groups that map to the same VectorFlow team, the highest role is used (Admin > Editor > Viewer).
{% endhint %}

{% stepper %}
{% step %}
Expand Down Expand Up @@ -110,17 +124,15 @@ Map identity provider groups to VectorFlow teams with specific roles. These mapp
| Group Name | The group name as it appears in the OIDC token or SCIM Group displayName |
| Team | The VectorFlow team to assign the user to |
| Role | The role to assign: Viewer, Editor, or Admin |

If a user matches multiple mappings for the same team, the highest role wins.
{% endstep %}
{% step %}
### Set defaults
Configure a **Default Team** and **Default Role** as a fallback for users who do not match any group mapping. Users with no group matches are assigned to the default team with the default role.
Configure a **Default Team** and **Default Role** as a fallback. If a user logs in and has no group matches (no IdP groups map to any VectorFlow team), they are assigned to the default team with the default role.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
Changing group sync settings takes effect immediately — the OIDC provider configuration is rebuilt without requiring a server restart.
Changing group sync settings takes effect immediately — the OIDC provider configuration is rebuilt without requiring a server restart. In SCIM+OIDC mode, mapping changes are applied to all SCIM-managed users at save time. In OIDC-only mode, changes take effect on each user's next login.
{% endhint %}

## SCIM provisioning
Expand Down Expand Up @@ -240,5 +252,9 @@ When users authenticate via OIDC or are provisioned via SCIM, their team roles a

Role updates happen:

- **On login** -- OIDC group claims are mapped to team roles via the configured team mappings
- **Via SCIM** -- When SCIM group membership changes are pushed, roles are assigned based on team mappings
- **On login** -- OIDC group claims are reconciled against team mappings. The user is added to newly matched teams, removed from teams they no longer match, and roles are updated to reflect the highest mapped role.
- **Via SCIM** -- When SCIM group membership changes are pushed, team memberships are reconciled immediately based on team mappings. Removals cascade correctly — if a user is removed from their only group mapping for a team, they are removed from that team.

{% hint style="info" %}
Manual team assignments (made in the UI) are not affected by SSO-managed role sync. Only memberships created by group sync are subject to reconciliation.
{% endhint %}
42 changes: 32 additions & 10 deletions docs/public/operations/scim.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ SCIM provisioning automates the user lifecycle:
| **Deactivate user** | The user account is locked, preventing login |
| **Delete user** | The user account is locked (not deleted, to preserve audit history) |

SCIM Groups are mapped to VectorFlow Teams. When your IdP pushes group membership changes, users are added to or removed from teams.
SCIM group membership is tracked internally — VectorFlow knows exactly which IdP groups each user belongs to. When your IdP pushes group membership changes, VectorFlow reconciles team memberships based on the configured [group mapping table](authentication.md#group-mapping), adding users to mapped teams and removing them when they no longer qualify.

## Setup

Expand Down Expand Up @@ -53,16 +53,36 @@ Test the SCIM connection from your IdP, then assign users and groups to the Vect
{% endstep %}
{% endstepper %}

## Group role mapping
## Group lifecycle and reconciliation

SCIM group membership is tracked internally — VectorFlow maintains a record of exactly which IdP groups each user belongs to via SCIM. When group membership changes, VectorFlow reconciles team memberships using the shared [group mapping table](authentication.md#group-mapping).

### How SCIM Group operations work

| Operation | What happens |
|-----------|-------------|
| **POST /Groups** | Creates the group and processes initial members. Each member's team memberships are reconciled against the mapping table. |
| **PATCH add members** | Adds users to the group and reconciles their team memberships — users gain access to mapped teams with the configured role. |
| **PATCH remove members** | Removes users from the group and reconciles — if a user no longer belongs to any group that maps to a given team, their membership on that team is removed. |
| **PUT /Groups** | Full member sync. VectorFlow compares the provided member list against the current membership, adds missing members, removes absent members, and reconciles all affected users. |
| **DELETE /Groups** | Deletes the group, cascading to all group membership records. All affected users' team memberships are reconciled (memberships that were only justified by the deleted group are removed). |
| **PATCH displayName** | Updates the group name. If the new name matches a different mapping, team memberships are reconciled accordingly for all group members. |

### Role assignment

When SCIM pushes group membership changes, VectorFlow assigns roles using the same team mappings configured for OIDC:

1. If **OIDC Team Mappings** are configured in **Settings > Auth**, the mapping's role is used
2. If no mapping matches, the **Default Role** is used
3. If no default role is set, `VIEWER` is assigned
1. If **Team Mappings** are configured in **Settings > Team & Role Mapping**, the mapping's role is used
2. If a user is in multiple groups that map to the same team, the **highest role wins** (Admin > Editor > Viewer)
3. If no mapping matches, the **Default Role** is used
4. If no default role is set, `VIEWER` is assigned

This ensures consistent role assignment regardless of whether sync happens via SCIM push or OIDC login.

{% hint style="info" %}
**Manual assignments are preserved.** Team memberships assigned manually in the VectorFlow UI are never modified by SCIM group sync. Only memberships created by group sync are subject to reconciliation.
{% endhint %}

## IdP-specific instructions

{% tabs %}
Expand Down Expand Up @@ -118,10 +138,12 @@ Any SCIM 2.0 compatible identity provider can integrate with VectorFlow. Configu
| `PUT` | `/api/scim/v2/Users/:id` | Replace a user |
| `PATCH` | `/api/scim/v2/Users/:id` | Partial update (commonly used for deactivation) |
| `DELETE` | `/api/scim/v2/Users/:id` | Deactivate a user (locks the account) |
| `GET` | `/api/scim/v2/Groups` | List groups (maps to VectorFlow teams) |
| `GET` | `/api/scim/v2/Groups` | List groups |
| `POST` | `/api/scim/v2/Groups` | Create a group and process initial members |
| `GET` | `/api/scim/v2/Groups/:id` | Get a group |
| `PATCH` | `/api/scim/v2/Groups/:id` | Update group membership |
| `PUT` | `/api/scim/v2/Groups/:id` | Replace group |
| `PATCH` | `/api/scim/v2/Groups/:id` | Update group membership (add/remove members, rename) |
| `PUT` | `/api/scim/v2/Groups/:id` | Replace group (full member sync) |
| `DELETE` | `/api/scim/v2/Groups/:id` | Delete group and cascade membership removal |

### Filtering

Expand Down Expand Up @@ -157,7 +179,7 @@ SCIM provisioning works best alongside OIDC/SSO. Users created via SCIM receive
| IdP test connection fails | Verify the SCIM base URL is reachable from your IdP. Check that the bearer token is correct and SCIM is enabled in VectorFlow settings. |
| Users not being created | Check that "Create Users" is enabled in your IdP's provisioning settings. Review the IdP provisioning logs for error details. |
| Users not being deactivated | Check that "Deactivate Users" is enabled in your IdP. VectorFlow locks the account (sets `lockedAt`) rather than deleting it. |
| Group membership not syncing | SCIM Groups map to VectorFlow Teams. Ensure the groups are assigned to the VectorFlow application in your IdP. New members are added with the Viewer role by default. |
| Group membership not syncing | SCIM Groups are mapped to VectorFlow Teams via the shared group mapping table in **Settings > Team & Role Mapping**. Ensure groups are assigned to the VectorFlow application in your IdP and that corresponding mappings exist. Without a matching mapping, group membership is tracked but no team assignment is created. |
| Token expired/invalid | Generate a new token from **Settings > Auth** and update it in your IdP. The previous token is invalidated immediately. |

### SCIM sync returns HTML error
Expand All @@ -172,4 +194,4 @@ VectorFlow exposes a `ServiceProviderConfig` endpoint at `/api/scim/v2/ServicePr

### Roles not updating via SCIM

Ensure that **OIDC Team Mappings** are configured in **Settings > Auth**. Without team mappings, all SCIM-provisioned members default to the **VIEWER** role.
Ensure that **Team Mappings** are configured in **Settings > Team & Role Mapping**. Without team mappings, all SCIM-provisioned members default to the **VIEWER** role. If a user belongs to multiple groups that map to the same team, the highest role wins (Admin > Editor > Viewer).
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "ScimGroupMember" (
"id" TEXT NOT NULL,
"scimGroupId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ScimGroupMember_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ScimGroupMember_scimGroupId_userId_key" ON "ScimGroupMember"("scimGroupId", "userId");

-- AddForeignKey
ALTER TABLE "ScimGroupMember" ADD CONSTRAINT "ScimGroupMember_scimGroupId_fkey" FOREIGN KEY ("scimGroupId") REFERENCES "ScimGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ScimGroupMember" ADD CONSTRAINT "ScimGroupMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AlterTable: add source column to TeamMember
ALTER TABLE "TeamMember" ADD COLUMN "source" TEXT NOT NULL DEFAULT 'manual';
26 changes: 20 additions & 6 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ model User {
image String?
passwordHash String?
authMethod AuthMethod @default(LOCAL)
memberships TeamMember[]
accounts Account[]
memberships TeamMember[]
scimGroupMemberships ScimGroupMember[]
accounts Account[]
lockedAt DateTime?
lockedBy String?
isSuperAdmin Boolean @default(false)
Expand Down Expand Up @@ -54,17 +55,30 @@ model Team {
}

model ScimGroup {
id String @id @default(cuid())
displayName String @unique
externalId String? @unique
createdAt DateTime @default(now())
id String @id @default(cuid())
displayName String @unique
externalId String? @unique
members ScimGroupMember[]
createdAt DateTime @default(now())
}

model ScimGroupMember {
id String @id @default(cuid())
scimGroupId String
userId String
scimGroup ScimGroup @relation(fields: [scimGroupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())

@@unique([scimGroupId, userId])
}

model TeamMember {
id String @id @default(cuid())
userId String
teamId String
role Role
source String @default("manual")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id])

Expand Down
Loading
Loading