fix: SCIM compatibility, user deletion FK constraints, and agent update UX#57
fix: SCIM compatibility, user deletion FK constraints, and agent update UX#57TerrifiedBug merged 6 commits intomainfrom
Conversation
Rolling dev releases replace the binary at the download URL on every push to main. When the UI caches version/checksum data and a new build lands before the user clicks Update, the agent downloads the new binary but the checksum from the stale cache doesn't match — causing a silent update failure loop. Force-refresh dev release info server-side in triggerAgentUpdate so the checksum always matches the binary currently at the download URL.
…LETE, fix user FK constraints - SCIM user creation now adopts existing users by linking scimExternalId instead of returning 409 (fixes pocket-id sync for pre-existing OIDC users) - Added POST /api/scim/v2/Groups to create teams via SCIM - Added DELETE /api/scim/v2/Groups/[id] to remove team memberships via SCIM - Fixed foreign key constraints that blocked user deletion: TeamMember.userId → onDelete: Cascade VrlSnippet.createdBy → onDelete: SetNull (nullable) DeployRequest.requestedById → onDelete: SetNull (nullable) ServiceAccount.createdById → onDelete: SetNull (nullable)
downloadUrl is never reassigned — only parsed for the binary name. Split from let destructuring to satisfy prefer-const lint rule.
|
@greptile review |
Greptile SummaryThis PR delivers follow-up fixes for SCIM compatibility (pocket-id sync), user deletion FK constraints, and agent update UX. The SCIM changes (group POST/DELETE, user adoption) are well-structured, the schema/migration changes are consistent and correct, and the fleet force-refresh correctly throws on failure rather than silently proceeding with stale data. Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant IdP as pocket-id (SCIM)
participant API as /api/scim/v2
participant SCIM as scim.ts
participant DB as PostgreSQL
Note over IdP,DB: User Provisioning
IdP->>API: POST /Users {email, externalId}
API->>SCIM: scimCreateUser(body)
SCIM->>DB: findUnique({email})
alt User exists (OIDC/SCIM-linked)
DB-->>SCIM: existing user
SCIM->>DB: update scimExternalId
SCIM-->>API: {user, adopted: true}
API-->>IdP: 200 OK
else User exists (local credential)
DB-->>SCIM: existing user (passwordHash set)
SCIM-->>API: throw {scimConflict: true}
API-->>IdP: 409 Conflict
else User not found
SCIM->>DB: create user (random password)
SCIM-->>API: {user, adopted: false}
API-->>IdP: 201 Created
end
Note over IdP,DB: Group Sync
IdP->>API: POST /Groups {displayName}
API->>DB: findFirst({name})
alt Team exists
DB-->>API: existing team
API-->>IdP: 200 OK (adopted)
else Team not found
API->>DB: create team
API-->>IdP: 201 Created
end
IdP->>API: DELETE /Groups/{id}
API->>DB: deleteMany TeamMember (teamId)
Note right of DB: Team record preserved
API-->>IdP: 204 No Content
Last reviewed commit: 9da431d |
- Gate SCIM user adoption to SSO accounts only (authMethod OIDC or existing scimExternalId). Local-credential accounts now reject with a clear error instead of silently linking to IdP identity. - Add missing audit log for SCIM group adoption (POST /Groups returning 200 for existing team). - Always apply fresh checksum for dev releases regardless of version string change — the binary can be rebuilt at the same commit, so the version comparison guard was insufficient.
|
@greptile review Fixed. All three findings. |
- Dev release updates now throw when checkDevAgentVersion returns no version (GitHub unreachable), instead of silently falling through with the stale UI-cached checksum. - SCIM local-account conflicts return 409 per RFC 7644 §3.3 instead of 400, signaling to the SCIM client that the conflict is permanent.
|
@greptile review. fixed. GitHub unavailability silent │ Valid │ Throw INTERNAL_SERVER_ERROR when latestVersion │ |
| const result = await deployAgent( | ||
| request.pipelineId, | ||
| request.requestedById, | ||
| request.requestedById ?? ctx.session.user.id, |
There was a problem hiding this comment.
Admin misattributed as requester in PipelineVersion
When requestedById is null (the original requester was deleted), the fallback ctx.session.user.id is passed as the userId to deployAgent. Inside deployAgent, this value is used in two places:
createVersion(pipelineId, ..., userId, ...)— the immutablePipelineVersionsnapshot is permanently stamped with the approving admin's ID as the version creator.prisma.user.findUnique({ where: { id: userId } })— the git sync commit is attributed to the admin.
So the admin who clicks "Approve" now permanently appears as the person who both requested and deployed the pipeline version — not just as the reviewer. The DeployRequest row correctly records reviewedById = ctx.session.user.id, but the PipelineVersion is a separate record that future audits and git history will show as authored by the admin.
Consider passing null (or a sentinel like "deleted-user") and updating createVersion/deployAgent to accept string | null for the userId, then treating null as "original requester deleted":
// Option: pass null and let deployAgent handle it
const result = await deployAgent(
request.pipelineId,
request.requestedById, // may be null — deployAgent should handle
request.changelog,
request.configYaml,
);Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/routers/deploy.ts
Line: 370
Comment:
**Admin misattributed as requester in PipelineVersion**
When `requestedById` is `null` (the original requester was deleted), the fallback `ctx.session.user.id` is passed as the `userId` to `deployAgent`. Inside `deployAgent`, this value is used in two places:
1. `createVersion(pipelineId, ..., userId, ...)` — the immutable `PipelineVersion` snapshot is permanently stamped with the approving admin's ID as the version creator.
2. `prisma.user.findUnique({ where: { id: userId } })` — the git sync commit is attributed to the admin.
So the admin who clicks "Approve" now permanently appears as the person who both requested and deployed the pipeline version — not just as the reviewer. The `DeployRequest` row correctly records `reviewedById = ctx.session.user.id`, but the `PipelineVersion` is a separate record that future audits and git history will show as authored by the admin.
Consider passing `null` (or a sentinel like `"deleted-user"`) and updating `createVersion`/`deployAgent` to accept `string | null` for the userId, then treating null as "original requester deleted":
```typescript
// Option: pass null and let deployAgent handle it
const result = await deployAgent(
request.pipelineId,
request.requestedById, // may be null — deployAgent should handle
request.changelog,
request.configYaml,
);
```
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| await writeAuditLog({ | ||
| userId: null, | ||
| action: "scim.group_deleted", | ||
| entityType: "Team", | ||
| entityId: id, | ||
| metadata: { displayName: team.name }, |
There was a problem hiding this comment.
Audit action scim.group_deleted is misleading
The handler intentionally preserves the team (to avoid cascading deletes of environments, pipelines, etc.) — it only removes memberships. But the audit entry fires "scim.group_deleted", which would lead an admin reviewing the audit trail to believe the team was removed when it wasn't.
Consider using an action name that accurately describes what occurred:
| await writeAuditLog({ | |
| userId: null, | |
| action: "scim.group_deleted", | |
| entityType: "Team", | |
| entityId: id, | |
| metadata: { displayName: team.name }, | |
| await writeAuditLog({ | |
| userId: null, | |
| action: "scim.group_memberships_cleared", | |
| entityType: "Team", | |
| entityId: id, | |
| metadata: { displayName: team.name }, | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/scim/v2/Groups/[id]/route.ts
Line: 285-291
Comment:
**Audit action `scim.group_deleted` is misleading**
The handler intentionally preserves the team (to avoid cascading deletes of environments, pipelines, etc.) — it only removes memberships. But the audit entry fires `"scim.group_deleted"`, which would lead an admin reviewing the audit trail to believe the team was removed when it wasn't.
Consider using an action name that accurately describes what occurred:
```suggestion
await writeAuditLog({
userId: null,
action: "scim.group_memberships_cleared",
entityType: "Team",
entityId: id,
metadata: { displayName: team.name },
});
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
Follow-up fixes from testing the operations UX improvements (#51) against pocket-id SCIM sync.
POST /Usersnow adopts existing users (e.g. created via OIDC login) by linking theirscimExternalIdinstead of returning 409. Returns 200 for adopted users, 201 for new.POST /api/scim/v2/Groupsto create teams via SCIM (adopts existing by name). AddedDELETE /api/scim/v2/Groups/[id]to remove team memberships. Fixes 405 errors from pocket-id.TeamMember.userId→onDelete: CascadeVrlSnippet.createdBy→onDelete: SetNull(nullable)DeployRequest.requestedById→onDelete: SetNull(nullable)ServiceAccount.createdById→onDelete: SetNull(nullable)Test plan