Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 137 additions & 12 deletions ee/apps/den-api/src/routes/auth/scim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { resolver } from "hono-openapi"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import { z } from "zod"
import { auth } from "../../auth.js"
import { deleteScimProvisionedAccessForProvider, recordScimSyncFailure, recordScimSyncFailureFromBearerToken, resolveScimProviderFromBearerToken, syncExternalIdentityFromScimResource, syncExternalIdentityFromScimUserId } from "../../scim.js"
import { authenticatedRoute, publicRoute, tokenRoute } from "../../middleware/index.js"
import { createScimGroupForToken, deleteScimGroupForToken, deleteScimProvisionedAccessForProvider, getScimGroupForToken, listScimGroupsForToken, patchScimGroupForToken, recordScimSyncFailure, recordScimSyncFailureFromBearerToken, replaceScimGroupForToken, resolveScimProviderFromBearerToken, syncExternalIdentityFromScimResource, syncExternalIdentityFromScimUserId } from "../../scim.js"
import { authenticatedRoute, tokenRoute } from "../../middleware/index.js"
import type { AuthContextVariables } from "../../session.js"

const scimErrorSchema = z.object({
Expand Down Expand Up @@ -134,14 +134,6 @@ export async function syncScimMutationFromResponse(input: {
}

export function registerScimAuthRoutes<T extends { Variables: AuthContextVariables }>(app: Hono<T>) {
const scimGroupsNotSupported = (c: { json: (object: unknown, status?: number | { status: number }) => Response }) => {
return c.json({
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM Groups are not supported yet.",
status: "501",
}, 501)
}

const rejectManagementRoute = (c: {
get: (key: "user") => AuthContextVariables["user"]
json: (object: unknown, status?: number | { status: number }) => Response
Expand Down Expand Up @@ -249,8 +241,141 @@ export function registerScimAuthRoutes<T extends { Variables: AuthContextVariabl
(c) => rejectManagementRoute(c),
)

app.all("/api/auth/scim/v2/Groups", publicRoute, (c) => scimGroupsNotSupported(c))
app.all("/api/auth/scim/v2/Groups/:groupId", publicRoute, (c) => scimGroupsNotSupported(c))
const scimJsonResponse = (result: Awaited<ReturnType<typeof listScimGroupsForToken>>) => {
const headers = new Headers(result.headers)
headers.set("content-type", "application/scim+json")
if (result.status === 204) {
return new Response(null, { status: result.status, headers })
}
return new Response(JSON.stringify(result.body), { status: result.status, headers })
}

const readScimJsonBody = async (request: Request) => {
const payload: unknown = await request.json().catch(() => null)
return payload
}

app.get("/api/auth/scim/v2/Groups", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await listScimGroupsForToken({
bearerToken,
filter: c.req.query("filter") ?? null,
})
return scimJsonResponse(result)
})

app.post("/api/auth/scim/v2/Groups", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await createScimGroupForToken({
bearerToken,
resource: await readScimJsonBody(c.req.raw),
})
return scimJsonResponse(result)
})

app.get("/api/auth/scim/v2/Groups/:groupId", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await getScimGroupForToken({
bearerToken,
groupId: c.req.param("groupId"),
})
return scimJsonResponse(result)
})

app.put("/api/auth/scim/v2/Groups/:groupId", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await replaceScimGroupForToken({
bearerToken,
groupId: c.req.param("groupId"),
resource: await readScimJsonBody(c.req.raw),
})
return scimJsonResponse(result)
})

app.patch("/api/auth/scim/v2/Groups/:groupId", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await patchScimGroupForToken({
bearerToken,
groupId: c.req.param("groupId"),
patch: await readScimJsonBody(c.req.raw),
})
return scimJsonResponse(result)
})

app.delete("/api/auth/scim/v2/Groups/:groupId", tokenRoute, async (c) => {
const bearerToken = readBearerToken(c.req.raw.headers)
if (!bearerToken) {
return scimJsonResponse({
status: 401,
body: {
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "SCIM token is required",
status: "401",
},
})
}

const result = await deleteScimGroupForToken({
bearerToken,
groupId: c.req.param("groupId"),
})
return scimJsonResponse(result)
})

app.delete(
"/api/auth/scim/v2/Users/:userId",
Expand Down
4 changes: 2 additions & 2 deletions ee/apps/den-api/src/routes/org/scim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function registerOrgScimRoutes<T extends { Variables: OrgRouteVariables }
describeRoute({
tags: ["SCIM"],
summary: "Get organization SCIM connection",
description: "Returns the SCIM User provisioning base URL and current connector metadata for the selected organization. SCIM Groups are not enabled yet.",
description: "Returns the SCIM provisioning base URL and current connector metadata for the selected organization. SCIM Groups are synced to organization teams.",
security: [{ bearerAuth: [] }],
responses: {
200: {
Expand Down Expand Up @@ -163,7 +163,7 @@ export function registerOrgScimRoutes<T extends { Variables: OrgRouteVariables }
describeRoute({
tags: ["SCIM"],
summary: "Create or rotate an organization SCIM token",
description: "Creates the organization SCIM User provisioning connector if needed and returns a freshly rotated bearer token. SCIM Groups are not enabled yet.",
description: "Creates the organization SCIM provisioning connector if needed and returns a freshly rotated bearer token. SCIM Groups are synced to organization teams.",
hide: process.env.NODE_ENV === "production",
security: [{ bearerAuth: [] }],
responses: {
Expand Down
Loading
Loading