diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 8a0d2aef9..52fda0ba4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -65,6 +65,7 @@ export enum EventType { ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", + REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth", LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth", ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth", UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth", @@ -72,6 +73,7 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id", LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth", ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth", UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth", @@ -434,6 +436,13 @@ interface GetIdentityUniversalAuthEvent { }; } +interface DeleteIdentityUniversalAuthEvent { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityKubernetesAuthEvent { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH; metadata: { @@ -493,6 +502,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent { }; } +interface GetIdentityUniversalAuthClientSecretByIdEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID; + metadata: { + identityId: string; + clientSecretId: string; + }; +} + interface RevokeIdentityUniversalAuthClientSecretEvent { type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET; metadata: { @@ -1003,6 +1020,7 @@ export type Event = | LoginIdentityUniversalAuthEvent | AddIdentityUniversalAuthEvent | UpdateIdentityUniversalAuthEvent + | DeleteIdentityUniversalAuthEvent | GetIdentityUniversalAuthEvent | LoginIdentityKubernetesAuthEvent | AddIdentityKubernetesAuthEvent @@ -1010,6 +1028,7 @@ export type Event = | GetIdentityKubernetesAuthEvent | CreateIdentityUniversalAuthClientSecretEvent | GetIdentityUniversalAuthClientSecretsEvent + | GetIdentityUniversalAuthClientSecretByIdEvent | RevokeIdentityUniversalAuthClientSecretEvent | LoginIdentityGcpAuthEvent | AddIdentityGcpAuthEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3ac4e5734..5e8c85286 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -72,6 +72,9 @@ export const UNIVERSAL_AUTH = { RETRIEVE: { identityId: "The ID of the identity to retrieve." }, + REVOKE: { + identityId: "The ID of the identity to revoke." + }, UPDATE: { identityId: "The ID of the identity to update.", clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.", @@ -90,6 +93,10 @@ export const UNIVERSAL_AUTH = { LIST_CLIENT_SECRETS: { identityId: "The ID of the identity to list client secrets for." }, + GET_CLIENT_SECRET: { + identityId: "The ID of the identity to get the client secret from.", + clientSecretId: "The ID of the client secret to get details." + }, REVOKE_CLIENT_SECRET: { identityId: "The ID of the identity to revoke the client secret from.", clientSecretId: "The ID of the client secret to revoke." diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-ua.ts index 670f52416..d2d60d112 100644 --- a/backend/src/server/routes/v1/identity-ua.ts +++ b/backend/src/server/routes/v1/identity-ua.ts @@ -134,7 +134,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.attachUa({ + const identityUniversalAuth = await server.services.identityUa.attachUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -219,7 +219,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.updateUa({ + const identityUniversalAuth = await server.services.identityUa.updateUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -272,7 +272,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.getIdentityUa({ + const identityUniversalAuth = await server.services.identityUa.getIdentityUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -295,6 +295,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/universal-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete Universal Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityUniversalAuth: IdentityUniversalAuthsSchema + }) + } + }, + handler: async (req) => { + const identityUniversalAuth = await server.services.identityUa.revokeIdentityUniversalAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityUniversalAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identityId + } + } + }); + + return { identityUniversalAuth }; + } + }); + server.route({ method: "POST", url: "/universal-auth/identities/:identityId/client-secrets", @@ -325,14 +372,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - ...req.body - }); + const { clientSecret, clientSecretData, orgId } = + await server.services.identityUa.createUniversalAuthClientSecret({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + ...req.body + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -374,13 +422,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId - }); + const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUniversalAuthClientSecrets( + { + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + } + ); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -396,6 +446,56 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get Universal Auth Client Secret for identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.identityId), + clientSecretId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.clientSecretId) + }), + response: { + 200: z.object({ + clientSecretData: sanitizedClientSecretSchema + }) + } + }, + handler: async (req) => { + const clientSecretData = await server.services.identityUa.getUniversalAuthClientSecretById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + clientSecretId: req.params.clientSecretId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: clientSecretData.orgId, + event: { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: clientSecretData.identityId, + clientSecretId: clientSecretData.id + } + } + }); + + return { clientSecretData }; + } + }); + server.route({ method: "POST", url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke", @@ -421,7 +521,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const clientSecretData = await server.services.identityUa.revokeUaClientSecret({ + const clientSecretData = await server.services.identityUa.revokeUniversalAuthClientSecret({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 5e940871b..3d9f0c3cb 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -25,7 +25,9 @@ import { TCreateUaClientSecretDTO, TGetUaClientSecretsDTO, TGetUaDTO, + TGetUniversalAuthClientSecretByIdDTO, TRevokeUaClientSecretDTO, + TRevokeUaDTO, TUpdateUaDTO } from "./identity-ua-types"; @@ -136,7 +138,7 @@ export const identityUaServiceFactory = ({ return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg }; }; - const attachUa = async ({ + const attachUniversalAuth = async ({ accessTokenMaxTTL, identityId, accessTokenNumUsesLimit, @@ -227,7 +229,7 @@ export const identityUaServiceFactory = ({ return { ...identityUa, orgId: identityMembershipOrg.orgId }; }; - const updateUa = async ({ + const updateUniversalAuth = async ({ accessTokenMaxTTL, identityId, accessTokenNumUsesLimit, @@ -312,7 +314,7 @@ export const identityUaServiceFactory = ({ return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; }; - const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { + const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) @@ -333,7 +335,50 @@ export const identityUaServiceFactory = ({ return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; - const createUaClientSecret = async ({ + const revokeIdentityUniversalAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeUaDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) + throw new BadRequestError({ + message: "The identity does not have universal auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke universal auth of identity with more privileged role" + }); + + const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { + const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityUniversalAuth; + }; + + const createUniversalAuthClientSecret = async ({ actor, actorId, actorOrgId, @@ -396,7 +441,7 @@ export const identityUaServiceFactory = ({ }; }; - const getUaClientSecrets = async ({ + const getUniversalAuthClientSecrets = async ({ actor, actorId, actorOrgId, @@ -442,7 +487,47 @@ export const identityUaServiceFactory = ({ return { clientSecrets, orgId: identityMembershipOrg.orgId }; }; - const revokeUaClientSecret = async ({ + const getUniversalAuthClientSecretById = async ({ + identityId, + actorId, + actor, + actorOrgId, + actorAuthMethod, + clientSecretId + }: TGetUniversalAuthClientSecretByIdDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) + throw new BadRequestError({ + message: "The identity does not have universal auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to read identity client secret of project with more privileged role" + }); + + const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId); + return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; + }; + + const revokeUniversalAuthClientSecret = async ({ identityId, actorId, actor, @@ -475,7 +560,7 @@ export const identityUaServiceFactory = ({ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); if (!hasPriviledge) throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + message: "Failed to revoke identity client secret with more privileged role" }); const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { @@ -486,11 +571,13 @@ export const identityUaServiceFactory = ({ return { login, - attachUa, - updateUa, - getIdentityUa, - createUaClientSecret, - getUaClientSecrets, - revokeUaClientSecret + attachUniversalAuth, + updateUniversalAuth, + getIdentityUniversalAuth, + revokeIdentityUniversalAuth, + createUniversalAuthClientSecret, + getUniversalAuthClientSecrets, + revokeUniversalAuthClientSecret, + getUniversalAuthClientSecretById }; }; diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 2cc4762a8..2045c2143 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -22,6 +22,10 @@ export type TGetUaDTO = { identityId: string; } & Omit; +export type TRevokeUaDTO = { + identityId: string; +} & Omit; + export type TCreateUaClientSecretDTO = { identityId: string; description: string; @@ -37,3 +41,8 @@ export type TRevokeUaClientSecretDTO = { identityId: string; clientSecretId: string; } & Omit; + +export type TGetUniversalAuthClientSecretByIdDTO = { + identityId: string; + clientSecretId: string; +} & Omit; diff --git a/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx new file mode 100644 index 000000000..477ee875c --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Client Secret By ID" +openapi: "GET /api/v1/auth/universal-auth/identities/{identityId}/client-secrets/{clientSecretId}" +--- diff --git a/docs/api-reference/endpoints/universal-auth/revoke.mdx b/docs/api-reference/endpoints/universal-auth/revoke.mdx new file mode 100644 index 000000000..e2a19e93c --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/universal-auth/identities/{identityId}" +--- diff --git a/docs/mint.json b/docs/mint.json index 9e07b01b1..9a50bd079 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -428,9 +428,11 @@ "api-reference/endpoints/universal-auth/attach", "api-reference/endpoints/universal-auth/retrieve", "api-reference/endpoints/universal-auth/update", + "api-reference/endpoints/universal-auth/revoke", "api-reference/endpoints/universal-auth/create-client-secret", "api-reference/endpoints/universal-auth/list-client-secrets", "api-reference/endpoints/universal-auth/revoke-client-secret", + "api-reference/endpoints/universal-auth/get-client-secret-by-id", "api-reference/endpoints/universal-auth/renew-access-token", "api-reference/endpoints/universal-auth/revoke-access-token" ]