Skip to content

Commit

Permalink
Pass Okta SCIM 2.0 SPEC Test
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Feb 14, 2024
1 parent 3a7b697 commit c73ee49
Show file tree
Hide file tree
Showing 29 changed files with 1,097 additions and 135 deletions.
2 changes: 2 additions & 0 deletions backend/src/@types/fastify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
Expand Down Expand Up @@ -105,6 +106,7 @@ declare module "fastify" {
secretRotation: TSecretRotationServiceFactory;
snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory;
scim: TScimServiceFactory;
auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
Expand Down
8 changes: 8 additions & 0 deletions backend/src/@types/knex.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ import {
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
TScimTokens,
TScimTokensInsert,
TScimTokensUpdate,
TSecretApprovalPolicies,
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
Expand Down Expand Up @@ -262,6 +265,11 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<
TScimTokens,
TScimTokensInsert,
TScimTokensUpdate
>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,
Expand Down
6 changes: 3 additions & 3 deletions backend/src/db/migrations/20240208234120_scim-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ScimToken))) {
await knex.schema.createTable(TableName.ScimToken, (t) => {
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
t.bigInteger("tokenTTL").defaultTo(15552000).notNullable(); // 180 days second
t.datetime("tokenLastUsedAt");
t.bigInteger("ttl").defaultTo(15552000).notNullable(); // 180 days second
t.string("description").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}

await createOnUpdateTrigger(knex, TableName.IdentityAccessToken);
await createOnUpdateTrigger(knex, TableName.ScimToken);
}

export async function down(knex: Knex): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from "./project-memberships";
export * from "./project-roles";
export * from "./projects";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";
export * from "./secret-approval-policies-approvers";
export * from "./secret-approval-request-secret-tags";
Expand Down
21 changes: 21 additions & 0 deletions backend/src/db/schemas/scim-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.

import { z } from "zod";

import { TImmutableDBKeys } from "./models";

export const ScimTokensSchema = z.object({
id: z.string(),
ttl: z.coerce.number().default(15552000),
description: z.string(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});

export type TScimTokens = z.infer<typeof ScimTokensSchema>;
export type TScimTokensInsert = Omit<TScimTokens, TImmutableDBKeys>;
export type TScimTokensUpdate = Partial<Omit<TScimTokens, TImmutableDBKeys>>;
233 changes: 192 additions & 41 deletions backend/src/ee/routes/v1/scim-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import jwt from "jsonwebtoken";
import { z } from "zod";
import { ScimTokensSchema } from "@app/db/schemas";

import { getConfig } from "@app/lib/config/env";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";



export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
Expand All @@ -27,29 +30,177 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
url: "/Users",
method: "GET",
schema: {
params: z.object({}),
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
}),
response: {
200: z.object({ // TODO: audit the response
Resources: z.array(z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})),
displayName: z.string().trim(),
active: z.boolean()
})),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
totalResults: z.number(),
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const res = await req.server.services.scim.listUsers({
offset: req.query.startIndex,
limit: req.query.count,
filter: req.query.filter
});
return res;
}
});

server.route({
url: "/Users/:userId",
method: "GET",
schema: {
params: z.object({
userId: z.string().trim()
}),
response: {
201: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const res = await req.server.services.scim.getUser(req.params.userId);
return res;
}
});

server.route({
url: "/Users",
method: "POST",
schema: {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})),
displayName: z.string().trim(),
// locale: z.string().trim(),
// externalId: z.string().trim(),
// groups: z.array(z.object({
// value: z.string().trim()
// })),
// password: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req, reply) => {
const user = await req.server.services.scim.createUser({
email: req.body.emails[0].value,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
orgId: req.permission.orgId as string
});

reply.code(201);
return user;
}
});

server.route({
url: "/Users/:userId",
method: "PATCH",
schema: {
body: z.object({}),
response: {
200: z.object({})
}
},
// onRequest: verifyAuth([]),
handler: async () => {
return {
hello: "world"
};
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
// TODO: update a user's attr
return {};
}
});

server.route({
url: "/tokens/organizations/:organizationId", // api/v1/scim/token/organizations/:organizationId
url: "/Users/:userId",
method: "PUT",
schema: {
body: z.object({}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
// TODO: update a user's profile
return {};
}
});

server.route({
url: "/scim-tokens",
method: "POST",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
organizationId: z.string().trim()
}),
body: z.object({
description: z.string().trim(),
organizationId: z.string().trim(),
description: z.string().trim().default(""),
ttl: z.number().min(0).default(0)
}),
response: {
Expand All @@ -58,53 +209,53 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
})
}
},
handler: async () => {
// TODO: create SCIM token logic
// TODO: create SCIM token controller

const appCfg = getConfig();
const scimToken = jwt.sign(
{
authTokenType: AuthTokenType.SCIM_TOKEN
},
appCfg.AUTH_SECRET,
{
// expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
}
); // TODO: add expiration
handler: async (req) => {
const { scimToken } = await server.services.scim.createScimToken({
organizationId: req.body.organizationId,
description: req.body.description,
ttl: req.body.ttl
});

return { scimToken };
}
});

server.route({
url: "/tokens/organizations/:organizationId", // api/v1/scim/token/organizations/:organizationId
url: "/scim-tokens",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
querystring: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
scimToken: z.string().trim()
scimTokens: z.array(ScimTokensSchema)
})
}
},
handler: async () => {
// TODO: put into service file

const appCfg = getConfig();
const scimToken = jwt.sign(
{
authTokenType: AuthTokenType.SCIM_TOKEN
},
appCfg.AUTH_SECRET,
{
// expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
}
); // TODO: add expiration
handler: async (req) => {
const scimTokens = await server.services.scim.getScimTokens(req.query.organizationId);
return { scimTokens };
}
});

server.route({
url: "/scim-tokens/:scimTokenId",
method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
scimTokenId: z.string().trim()
}),
response: {
200: z.object({
scimToken: ScimTokensSchema
})
}
},
handler: async (req) => {
const scimToken = await server.services.scim.deleteScimToken(req.params.scimTokenId);
return { scimToken };
}
});
Expand Down
8 changes: 6 additions & 2 deletions backend/src/ee/services/audit-log/audit-log-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {

export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor;
actor: UserActor | IdentityActor | ServiceActor | ScimIdpActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
Expand Down Expand Up @@ -120,7 +120,11 @@ export interface IdentityActor {
metadata: IdentityActorMetadata;
}

export type Actor = UserActor | ServiceActor | IdentityActor;
export interface ScimClientActor {
type: ActorType.SCIM_CLIENT;
}

export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;

interface GetSecretsEvent {
type: EventType.GET_SECRETS;
Expand Down
Loading

0 comments on commit c73ee49

Please sign in to comment.