mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Pass Okta SCIM 2.0 SPEC Test
This commit is contained in:
Vendored
+2
@@ -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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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 { 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 { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
@@ -105,6 +106,7 @@ declare module "fastify" {
|
|||||||
secretRotation: TSecretRotationServiceFactory;
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
snapshot: TSecretSnapshotServiceFactory;
|
snapshot: TSecretSnapshotServiceFactory;
|
||||||
saml: TSamlConfigServiceFactory;
|
saml: TSamlConfigServiceFactory;
|
||||||
|
scim: TScimServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
|
|||||||
Vendored
+8
@@ -83,6 +83,9 @@ import {
|
|||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
|
TScimTokens,
|
||||||
|
TScimTokensInsert,
|
||||||
|
TScimTokensUpdate,
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesApprovers,
|
TSecretApprovalPoliciesApprovers,
|
||||||
TSecretApprovalPoliciesApproversInsert,
|
TSecretApprovalPoliciesApproversInsert,
|
||||||
@@ -262,6 +265,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate
|
TIdentityProjectMembershipsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ScimToken]: Knex.CompositeTableType<
|
||||||
|
TScimTokens,
|
||||||
|
TScimTokensInsert,
|
||||||
|
TScimTokensUpdate
|
||||||
|
>;
|
||||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesInsert,
|
TSecretApprovalPoliciesInsert,
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (!(await knex.schema.hasTable(TableName.ScimToken))) {
|
if (!(await knex.schema.hasTable(TableName.ScimToken))) {
|
||||||
await knex.schema.createTable(TableName.ScimToken, (t) => {
|
await knex.schema.createTable(TableName.ScimToken, (t) => {
|
||||||
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
|
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
|
||||||
t.bigInteger("tokenTTL").defaultTo(15552000).notNullable(); // 180 days second
|
t.bigInteger("ttl").defaultTo(15552000).notNullable(); // 180 days second
|
||||||
t.datetime("tokenLastUsedAt");
|
t.string("description").notNullable();
|
||||||
t.uuid("orgId").notNullable();
|
t.uuid("orgId").notNullable();
|
||||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.IdentityAccessToken);
|
await createOnUpdateTrigger(knex, TableName.ScimToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export * from "./project-memberships";
|
|||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
export * from "./secret-approval-policies-approvers";
|
export * from "./secret-approval-policies-approvers";
|
||||||
export * from "./secret-approval-request-secret-tags";
|
export * from "./secret-approval-request-secret-tags";
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ScimTokensSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const registerScimRouter = async (server: FastifyZodProvider) => {
|
export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -27,29 +30,177 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/Users",
|
url: "/Users",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
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: {
|
response: {
|
||||||
200: z.object({})
|
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([]),
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
handler: async () => {
|
handler: async (req) => {
|
||||||
return {
|
const res = await req.server.services.scim.listUsers({
|
||||||
hello: "world"
|
offset: req.query.startIndex,
|
||||||
};
|
limit: req.query.count,
|
||||||
|
filter: req.query.filter
|
||||||
|
});
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/tokens/organizations/:organizationId", // api/v1/scim/token/organizations/:organizationId
|
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([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
// TODO: update a user's attr
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
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",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
|
||||||
organizationId: z.string().trim()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
body: z.object({
|
||||||
description: z.string().trim(),
|
organizationId: z.string().trim(),
|
||||||
|
description: z.string().trim().default(""),
|
||||||
ttl: z.number().min(0).default(0)
|
ttl: z.number().min(0).default(0)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -58,53 +209,53 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async (req) => {
|
||||||
// TODO: create SCIM token logic
|
const { scimToken } = await server.services.scim.createScimToken({
|
||||||
// TODO: create SCIM token controller
|
organizationId: req.body.organizationId,
|
||||||
|
description: req.body.description,
|
||||||
const appCfg = getConfig();
|
ttl: req.body.ttl
|
||||||
const scimToken = jwt.sign(
|
});
|
||||||
{
|
|
||||||
authTokenType: AuthTokenType.SCIM_TOKEN
|
|
||||||
},
|
|
||||||
appCfg.AUTH_SECRET,
|
|
||||||
{
|
|
||||||
// expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
|
|
||||||
}
|
|
||||||
); // TODO: add expiration
|
|
||||||
|
|
||||||
return { scimToken };
|
return { scimToken };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/tokens/organizations/:organizationId", // api/v1/scim/token/organizations/:organizationId
|
url: "/scim-tokens",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
querystring: z.object({
|
||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
scimToken: z.string().trim()
|
scimTokens: z.array(ScimTokensSchema)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async (req) => {
|
||||||
// TODO: put into service file
|
const scimTokens = await server.services.scim.getScimTokens(req.query.organizationId);
|
||||||
|
return { scimTokens };
|
||||||
const appCfg = getConfig();
|
}
|
||||||
const scimToken = jwt.sign(
|
});
|
||||||
{
|
|
||||||
authTokenType: AuthTokenType.SCIM_TOKEN
|
|
||||||
},
|
|
||||||
appCfg.AUTH_SECRET,
|
|
||||||
{
|
|
||||||
// expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
|
|
||||||
}
|
|
||||||
); // TODO: add expiration
|
|
||||||
|
|
||||||
|
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 };
|
return { scimToken };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {
|
|||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
actor: UserActor | IdentityActor | ServiceActor;
|
actor: UserActor | IdentityActor | ServiceActor | ScimIdpActor;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
@@ -120,7 +120,11 @@ export interface IdentityActor {
|
|||||||
metadata: IdentityActorMetadata;
|
metadata: IdentityActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor;
|
export interface ScimClientActor {
|
||||||
|
type: ActorType.SCIM_CLIENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
type: EventType.GET_SECRETS;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: true,
|
||||||
scim: false,
|
scim: true,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export type TFeatureSet = {
|
|||||||
customAlerts: false;
|
customAlerts: false;
|
||||||
auditLogs: false;
|
auditLogs: false;
|
||||||
auditLogsRetentionDays: 0;
|
auditLogsRetentionDays: 0;
|
||||||
samlSSO: false;
|
samlSSO: true;
|
||||||
scim: false;
|
scim: true;
|
||||||
status: null;
|
status: null;
|
||||||
trial_end: null;
|
trial_end: null;
|
||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||||
|
|
||||||
|
export const scimDALFactory = (db: TDbClient) => {
|
||||||
|
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||||
|
return scimTokenOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
|
OrgMembershipStatus
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import {
|
||||||
|
TCreateScimTokenDTO,
|
||||||
|
TListScimUsersDTO,
|
||||||
|
TListScimUsersRes,
|
||||||
|
TCreateScimUserDTO,
|
||||||
|
TScimTokenJwtPayload
|
||||||
|
} from "./scim-types";
|
||||||
|
import {
|
||||||
|
createScimUser,
|
||||||
|
TScimUser,
|
||||||
|
} from "@app/lib/scim";
|
||||||
|
import { UnauthorizedError, ScimRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
type TScimServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
scimDAL: TScimDALFactory; // TODO: pick
|
||||||
|
userDAL: TUserDALFactory; // TODO: pick
|
||||||
|
orgDAL: TOrgDALFactory; // TODO: pick
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
|
|
||||||
|
export const scimServiceFactory = ({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
permissionService
|
||||||
|
}: TScimServiceFactoryDep) => {
|
||||||
|
const createScimToken = async ({
|
||||||
|
organizationId,
|
||||||
|
description,
|
||||||
|
ttl
|
||||||
|
}: TCreateScimTokenDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// TODO: permission stuff
|
||||||
|
|
||||||
|
const scimTokenData = await scimDAL.create({
|
||||||
|
orgId: organizationId,
|
||||||
|
description,
|
||||||
|
ttl
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimToken = jwt.sign(
|
||||||
|
{
|
||||||
|
scimTokenId: scimTokenData.id,
|
||||||
|
authTokenType: AuthTokenType.SCIM_TOKEN
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
return { scimToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScimTokens = async (organizationId: string) => {
|
||||||
|
const scimTokens = await scimDAL.find({ orgId: organizationId });
|
||||||
|
return scimTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteScimToken = async (scimTokenId: string) => {
|
||||||
|
const scimToken = await scimDAL.deleteById(scimTokenId);
|
||||||
|
return scimToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scim server endpoints
|
||||||
|
|
||||||
|
const listUsers = async ({
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
filter
|
||||||
|
}: TListScimUsersDTO): Promise<TListScimUsersRes> => {
|
||||||
|
|
||||||
|
const parseFilter = (filter: string | undefined) => {
|
||||||
|
if (!filter) return {};
|
||||||
|
const [parsedName, parsedValue] = filter.split("eq").map(s => s.trim());
|
||||||
|
|
||||||
|
let attributeName = parsedName;
|
||||||
|
if (parsedName === "userName") { // note
|
||||||
|
attributeName = "email";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [attributeName]: parsedValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOpts = {
|
||||||
|
...(offset && { offset }),
|
||||||
|
...(limit && { limit }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await userDAL.find(parseFilter(filter), findOpts);
|
||||||
|
|
||||||
|
let resources: TScimUser[] = [];
|
||||||
|
|
||||||
|
let scimResource: TListScimUsersRes = { // note: type
|
||||||
|
Resources: [],
|
||||||
|
itemsPerPage: limit,
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
startIndex: offset,
|
||||||
|
totalResults: users.length
|
||||||
|
};
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
let scimUser = createScimUser({
|
||||||
|
userId: user.id,
|
||||||
|
firstName: user.firstName as string,
|
||||||
|
lastName: user.lastName as string,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
resources.push(scimUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
scimResource.Resources = resources;
|
||||||
|
|
||||||
|
return scimResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUser = async (userId: string) => {
|
||||||
|
// TODO: check out SCIM-specific errors
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await userDAL.findById(userId);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
interface PostgresError extends Error {
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbError = error as PostgresError;
|
||||||
|
|
||||||
|
if (dbError.error.code === "22P02") throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
return createScimUser({
|
||||||
|
userId: user.id,
|
||||||
|
firstName: user.firstName as string,
|
||||||
|
lastName: user.lastName as string,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = async ({
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
orgId
|
||||||
|
}: TCreateScimUserDTO) => {
|
||||||
|
let user = await userDAL.findOne({
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) throw new ScimRequestError({
|
||||||
|
detail: "User already exists in the database",
|
||||||
|
status: 409
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await orgDAL.createMembership({
|
||||||
|
inviteEmail: email,
|
||||||
|
orgId,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
});
|
||||||
|
return newUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
return createScimUser({
|
||||||
|
userId: user.id,
|
||||||
|
firstName: user.firstName as string,
|
||||||
|
lastName: user.lastName as string,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
||||||
|
// TODO: check expiry
|
||||||
|
|
||||||
|
const scimToken = await scimDAL.findById(token.scimTokenId);
|
||||||
|
if (!scimToken) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createScimToken,
|
||||||
|
getScimTokens,
|
||||||
|
deleteScimToken,
|
||||||
|
listUsers,
|
||||||
|
getUser,
|
||||||
|
createUser,
|
||||||
|
fnValidateScimToken
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { TScimUser } from "@app/lib/scim";
|
||||||
|
|
||||||
|
export type TCreateScimTokenDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
description: string;
|
||||||
|
ttl: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add org permissions
|
||||||
|
// & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TListScimUsersDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
filter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TListScimUsersRes = { // check naming here
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
|
||||||
|
totalResults: number;
|
||||||
|
Resources: TScimUser[];
|
||||||
|
itemsPerPage: number;
|
||||||
|
startIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateScimUserDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
orgId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateScimUserRes = {
|
||||||
|
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TScimTokenJwtPayload = {
|
||||||
|
scimTokenId: string;
|
||||||
|
authTokenType: string;
|
||||||
|
};
|
||||||
@@ -58,3 +58,20 @@ export class BadRequestError extends Error {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ScimRequestError extends Error {
|
||||||
|
name: string;
|
||||||
|
schemas: string[];
|
||||||
|
detail: string;
|
||||||
|
status: number;
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, detail, status }: { message?: string; name?: string; error?: unknown, detail: string, status: number }) {
|
||||||
|
super(detail ?? "The request is invalid");
|
||||||
|
this.name = name || "ScimRequestError";
|
||||||
|
this.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"];
|
||||||
|
this.error = error;
|
||||||
|
this.detail = detail;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { TScimUser } from "./types";
|
||||||
|
|
||||||
|
export const createScimUser = ({
|
||||||
|
userId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
}): TScimUser => {
|
||||||
|
let scimUser = {
|
||||||
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||||
|
"id": userId,
|
||||||
|
"userName": email,
|
||||||
|
"displayName": `${firstName} ${lastName}`,
|
||||||
|
"name": {
|
||||||
|
"givenName": firstName,
|
||||||
|
"middleName": null,
|
||||||
|
"familyName": lastName
|
||||||
|
},
|
||||||
|
"emails":
|
||||||
|
[{
|
||||||
|
"primary": true,
|
||||||
|
"value": email,
|
||||||
|
"type": "work"
|
||||||
|
}],
|
||||||
|
"active": true,
|
||||||
|
"groups": [],
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "User",
|
||||||
|
"location": null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return scimUser;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { TScimUser } from "./types";
|
||||||
|
export {
|
||||||
|
createScimUser
|
||||||
|
} from "./fns";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
export type TScimUser = {
|
||||||
|
schemas: string[];
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
displayName: string;
|
||||||
|
name: {
|
||||||
|
givenName: string;
|
||||||
|
middleName: null;
|
||||||
|
familyName: string;
|
||||||
|
};
|
||||||
|
emails: {
|
||||||
|
primary: boolean;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
}[];
|
||||||
|
active: boolean;
|
||||||
|
groups: string[];
|
||||||
|
meta: {
|
||||||
|
resourceType: string;
|
||||||
|
location: null;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -63,6 +63,10 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
|
|||||||
identityId: req.auth.identityId
|
identityId: req.auth.identityId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
payload.actor = {
|
||||||
|
type: ActorType.SCIM_CLIENT
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestError({ message: "Missing logic for other actor" });
|
throw new BadRequestError({ message: "Missing logic for other actor" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
|
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
|
||||||
|
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
|
||||||
|
|
||||||
export type TAuthMode =
|
export type TAuthMode =
|
||||||
| {
|
| {
|
||||||
@@ -38,7 +39,9 @@ export type TAuthMode =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
authMode: AuthMode.SCIM_TOKEN;
|
authMode: AuthMode.SCIM_TOKEN;
|
||||||
actor: ActorType.SCIM_IDP;
|
actor: ActorType.SCIM_CLIENT;
|
||||||
|
scimTokenId: string;
|
||||||
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||||
@@ -78,8 +81,8 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
|||||||
case AuthTokenType.SCIM_TOKEN:
|
case AuthTokenType.SCIM_TOKEN:
|
||||||
return {
|
return {
|
||||||
authMode: AuthMode.SCIM_TOKEN,
|
authMode: AuthMode.SCIM_TOKEN,
|
||||||
token: decodedToken,
|
token: decodedToken as TScimTokenJwtPayload,
|
||||||
actor: ActorType.SCIM_IDP
|
actor: ActorType.SCIM_CLIENT
|
||||||
} as const;
|
} as const;
|
||||||
default:
|
default:
|
||||||
return { authMode: null, token: null } as const;
|
return { authMode: null, token: null } as const;
|
||||||
@@ -125,7 +128,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMode.SCIM_TOKEN: {
|
case AuthMode.SCIM_TOKEN: {
|
||||||
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor };
|
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
|
||||||
|
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const injectPermission = fp(async (server) => {
|
|||||||
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
||||||
} else if (req.auth.actor === ActorType.SERVICE) {
|
} else if (req.auth.actor === ActorType.SERVICE) {
|
||||||
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError, ScimRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
@@ -21,6 +21,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
error: "PermissionDenied",
|
error: "PermissionDenied",
|
||||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof ScimRequestError) {
|
||||||
|
void res.status(error.status).send({
|
||||||
|
schemas: error.schemas,
|
||||||
|
status: error.status,
|
||||||
|
detail: error.detail
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.send(error);
|
void res.send(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snaps
|
|||||||
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
|
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { scimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
@@ -155,6 +157,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
|
const scimDAL = scimDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@@ -188,6 +191,13 @@ export const registerRoutes = async (
|
|||||||
trustedIpDAL,
|
trustedIpDAL,
|
||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
const scimService = scimServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
const auditLogQueue = auditLogQueueServiceFactory({
|
const auditLogQueue = auditLogQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
@@ -486,6 +496,7 @@ export const registerRoutes = async (
|
|||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
|
scim: scimService,
|
||||||
secretBlindIndex: secretBlindIndexService,
|
secretBlindIndex: secretBlindIndexService,
|
||||||
telemetry: telemetryService
|
telemetry: telemetryService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export enum ActorType { // would extend to AWS, Azure, ...
|
|||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity",
|
IDENTITY = "identity",
|
||||||
Machine = "machine",
|
Machine = "machine",
|
||||||
SCIM_IDP = "scimIdp"
|
SCIM_CLIENT = "scimClient"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthModeJwtTokenPayload = {
|
export type AuthModeJwtTokenPayload = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export { useGetScimTokens } from "./queries";
|
||||||
export {
|
export {
|
||||||
useGetScimToken
|
useCreateScimToken,
|
||||||
} from "./queries";
|
useDeleteScimToken
|
||||||
|
} from "./mutations";
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import {
|
||||||
|
CreateScimTokenDTO,
|
||||||
|
CreateScimTokenRes,
|
||||||
|
DeleteScimTokenDTO
|
||||||
|
} from "./types";
|
||||||
|
import { scimKeys } from "./queries";
|
||||||
|
|
||||||
|
export const useCreateScimToken = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<CreateScimTokenRes, {}, CreateScimTokenDTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
description,
|
||||||
|
ttl
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/scim/scim-tokens", {
|
||||||
|
organizationId,
|
||||||
|
description,
|
||||||
|
ttl
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteScimToken = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<CreateScimTokenRes, {}, DeleteScimTokenDTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
organizationId,
|
||||||
|
scimTokenId
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/scim/scim-tokens/${scimTokenId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { ScimTokenData } from "./types";
|
||||||
|
|
||||||
import { GetScimTokenRes } from "./types";
|
export const scimKeys = {
|
||||||
|
getScimTokens: (orgId: string) => [{ orgId }, "organization-scim-token"] as const,
|
||||||
const scimKeys = {
|
|
||||||
getScimToken: (orgId: string) => [{ orgId }, "organization-scim-token"] as const,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetScimToken = (organizationId: string) => {
|
export const useGetScimTokens = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: scimKeys.getScimToken(organizationId),
|
queryKey: scimKeys.getScimTokens(organizationId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (organizationId === "") {
|
if (organizationId === "") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: { scimToken } } = await apiRequest.get<GetScimTokenRes>(`/api/v1/scim/token/organizations/${organizationId}`);
|
const { data: { scimTokens } } = await apiRequest.get<{ scimTokens: ScimTokenData[] }>(`/api/v1/scim/scim-tokens?organizationId=${organizationId}`);
|
||||||
return scimToken;
|
|
||||||
|
return scimTokens;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
export type GetScimTokenRes = {
|
export type ScimTokenData = {
|
||||||
scimToken: string;
|
id: string;
|
||||||
|
ttl: number;
|
||||||
|
description: string;
|
||||||
|
tokenSuffix: string;
|
||||||
|
orgId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateScimTokenDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
description?: string;
|
||||||
|
ttl?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeleteScimTokenDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
scimTokenId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateScimTokenRes = {
|
||||||
|
scimToken: string;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export type SubscriptionPlan = {
|
|||||||
workspacesUsed: number;
|
workspacesUsed: number;
|
||||||
environmentLimit: number;
|
environmentLimit: number;
|
||||||
samlSSO: boolean;
|
samlSSO: boolean;
|
||||||
|
scim: boolean;
|
||||||
status:
|
status:
|
||||||
| "incomplete"
|
| "incomplete"
|
||||||
| "incomplete_expired"
|
| "incomplete_expired"
|
||||||
|
|||||||
+27
-64
@@ -7,41 +7,41 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
IconButton,
|
IconButton,
|
||||||
Switch
|
Switch,
|
||||||
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
// OrgPermissionActions,
|
// OrgPermissionActions,
|
||||||
// OrgPermissionSubjects,
|
// OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
// useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
// import { usePopUp } from "@app/hooks/usePopUp";
|
import { ScimTokenModal } from "./ScimTokenModal";
|
||||||
import { useGetScimToken } from "@app/hooks/api";
|
|
||||||
|
|
||||||
// TODO: add permissioning for enteprise SCIM
|
// TODO: add permissioning for enteprise SCIM
|
||||||
|
|
||||||
export const OrgScimSection = () => {
|
export const OrgScimSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
// const { createNotification } = useNotificationContext();
|
// const { createNotification } = useNotificationContext();
|
||||||
// const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
// const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
// "upgradePlan"
|
"scimToken",
|
||||||
// ] as const);
|
"deleteScimToken",
|
||||||
|
"upgradePlan"
|
||||||
const { data: scimToken } = useGetScimToken(currentOrg?.id ?? "");
|
] as const);
|
||||||
|
|
||||||
const [scimEnabled, setScimEnabled] = useState(false); // sync this with backend
|
const [scimEnabled, setScimEnabled] = useState(false); // sync this with backend
|
||||||
const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false);
|
|
||||||
|
|
||||||
// TODO: get SCIM stuf
|
|
||||||
|
|
||||||
const addScimTokenBtnClick = () => {
|
const addScimTokenBtnClick = () => {
|
||||||
|
if (subscription?.scim) {
|
||||||
|
handlePopUpOpen("scimToken");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSCIMToggle = (value: boolean) => {
|
const handleSCIMToggle = (value: boolean) => {
|
||||||
// TODO
|
|
||||||
try {
|
try {
|
||||||
setScimEnabled(value);
|
setScimEnabled(value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -49,11 +49,6 @@ export const OrgScimSection = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(scimToken ?? "");
|
|
||||||
setIsAPIKeyCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-8 flex items-center">
|
<div className="mb-8 flex items-center">
|
||||||
@@ -64,7 +59,7 @@ export const OrgScimSection = () => {
|
|||||||
// isDisabled={!isAllowed}
|
// isDisabled={!isAllowed}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
>
|
>
|
||||||
Add SCIM Token
|
Manage SCIM Tokens
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -75,48 +70,16 @@ export const OrgScimSection = () => {
|
|||||||
>
|
>
|
||||||
Enable SCIM Provisioning
|
Enable SCIM Provisioning
|
||||||
</Switch>
|
</Switch>
|
||||||
{scimEnabled && (
|
<ScimTokenModal
|
||||||
<div>
|
popUp={popUp}
|
||||||
<div className="mt-8 mb-8">
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
<h3 className="text-sm text-mineshaft-400">SCIM URL</h3>
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
<p className="text-md text-gray-400">{`${window.origin}/api/v1/scim`}</p>
|
/>
|
||||||
</div>
|
<UpgradePlanModal
|
||||||
{/* <h3 className="mb-2 mt-8 text-sm text-mineshaft-400">SCIM URL</h3> */}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
{/* <div className="mb-8 max-w-xl flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
<p className="mr-4 break-all">{`${window.origin}/api/v1/scim`}</p>
|
text="You can use SCIM Provisioning if you switch to Infisical's Pro plan."
|
||||||
<IconButton
|
/>
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Click to copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div> */}
|
|
||||||
{scimToken && (
|
|
||||||
<>
|
|
||||||
<h3 className="mb-2 text-sm text-mineshaft-400">SCIM Bearer Token</h3>
|
|
||||||
<div className="max-w-xl flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{scimToken}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Click to copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useGetScimTokens,
|
||||||
|
useCreateScimToken,
|
||||||
|
useDeleteScimToken
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
|
||||||
|
// TODO: turn TTL into a select component
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
description: yup.string(),
|
||||||
|
ttl: yup.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["scimToken", "deleteScimToken"]>;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteScimToken"]>,
|
||||||
|
data?: {
|
||||||
|
scimTokenId: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<
|
||||||
|
["scimToken", "deleteScimToken"]
|
||||||
|
>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScimTokenModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpOpen,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
|
||||||
|
const [isScimUrlCopied, setIsScimUrlCopied] = useToggle(false);
|
||||||
|
const [isScimTokenCopied, setIsScimTokenCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetScimTokens(currentOrg?.id ?? "");
|
||||||
|
const { mutateAsync: createScimTokenMutateAsync } = useCreateScimToken();
|
||||||
|
const { mutateAsync: deleteScimTokenMutateAsync } = useDeleteScimToken();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
description: "",
|
||||||
|
ttl: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isScimUrlCopied) {
|
||||||
|
timer = setTimeout(() => setIsScimUrlCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScimTokenCopied) {
|
||||||
|
timer = setTimeout(() => setIsScimTokenCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isScimTokenCopied, isScimUrlCopied]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ description, ttl }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
|
const { scimToken } = await createScimTokenMutateAsync({
|
||||||
|
organizationId: currentOrg.id,
|
||||||
|
description,
|
||||||
|
ttl: Number(ttl)
|
||||||
|
});
|
||||||
|
|
||||||
|
setToken(scimToken);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created SCIM token",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create SCIM token",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteScimTokenSubmit = async (scimTokenId: string) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
|
await deleteScimTokenMutateAsync({
|
||||||
|
organizationId: currentOrg.id,
|
||||||
|
scimTokenId
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: find alt way
|
||||||
|
|
||||||
|
// if (token.startsWith(clientSecretPrefix)) {
|
||||||
|
// reset();
|
||||||
|
// setToken("");
|
||||||
|
// }
|
||||||
|
|
||||||
|
handlePopUpToggle("deleteScimToken", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted SCIM token",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete SCIM token",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasToken = Boolean(token);
|
||||||
|
const scimUrl = `${window.origin}/api/v1/scim`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.scimToken?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("scimToken", isOpen);
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`Manage SCIM credentials`}>
|
||||||
|
<h2 className="mb-4">SCIM URL</h2>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{scimUrl}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(scimUrl);
|
||||||
|
setIsScimUrlCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isScimUrlCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-4">New SCIM Token</h2>
|
||||||
|
{hasToken ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p>We will only show this token once</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{token}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(token);
|
||||||
|
setIsScimTokenCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isScimTokenCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description (optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Description" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL (seconds - optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<h2 className="mb-4">SCIM Tokens</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th>Expires At</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="org-scim-tokens" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
ttl,
|
||||||
|
createdAt
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let expiresAt;
|
||||||
|
if (ttl > 0) {
|
||||||
|
expiresAt = new Date(new Date(createdAt).getTime() + ttl * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
|
||||||
|
<Td>{description === "" ? "-" : description}</Td>
|
||||||
|
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
|
||||||
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd HH:mm:ss")}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("deleteScimToken", {
|
||||||
|
scimTokenId: id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<EmptyState
|
||||||
|
title="No SCIM tokens have been created yet"
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteScimToken.isOpen}
|
||||||
|
title={"Are you sure want to delete the SCIM token?"}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("scimToken", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
|
||||||
|
const deleteScimTokenData = popUp?.deleteScimToken?.data as {
|
||||||
|
scimTokenId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return onDeleteScimTokenSubmit(deleteScimTokenData.scimTokenId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user