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 bcc2a0770..97dfdb9fa 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,6 +173,7 @@ export enum EventType { CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth", UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", + GET_TOKEN_IDENTITY_TOKEN_AUTH = "get-token-identity-token-auth", CREATE_SUB_ORGANIZATION = "create-sub-organization", UPDATE_SUB_ORGANIZATION = "update-sub-organization", @@ -1013,6 +1014,14 @@ interface GetTokensIdentityTokenAuthEvent { }; } +interface GetTokenIdentityTokenAuthEvent { + type: EventType.GET_TOKEN_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + tokenId: string; + }; +} + interface AddIdentityTokenAuthEvent { type: EventType.ADD_IDENTITY_TOKEN_AUTH; metadata: { @@ -4128,6 +4137,7 @@ export type Event = | CreateTokenIdentityTokenAuthEvent | UpdateTokenIdentityTokenAuthEvent | GetTokensIdentityTokenAuthEvent + | GetTokenIdentityTokenAuthEvent | AddIdentityTokenAuthEvent | UpdateIdentityTokenAuthEvent | GetIdentityTokenAuthEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 927694ef4..09adf2b8b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -577,6 +577,10 @@ export const TOKEN_AUTH = { offset: "The offset to start from. If you enter 10, it will start from the 10th token.", limit: "The number of tokens to return." }, + GET_TOKEN: { + identityId: "The ID of the machine identity to get the token for.", + tokenId: "The ID of the token to get metadata for." + }, CREATE_TOKEN: { identityId: "The ID of the machine identity to create the token for.", name: "The name of the token to create." diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index aafffdfdb..9906b6b5d 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -312,9 +312,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider response: { 200: z.object({ accessToken: z.string(), - expiresIn: z.coerce.number(), - accessTokenMaxTTL: z.coerce.number(), - tokenType: z.literal("Bearer") + tokenData: IdentityAccessTokensSchema }) } }, @@ -344,9 +342,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider return { accessToken, - tokenType: "Bearer" as const, - expiresIn: identityTokenAuth.accessTokenTTL, - accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL + tokenData: identityAccessToken }; } }); @@ -406,6 +402,59 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }); + server.route({ + method: "GET", + url: "/token-auth/identities/:identityId/tokens/:tokenId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], + description: "Get token for machine identity with Token Auth", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(TOKEN_AUTH.GET_TOKEN.identityId), + tokenId: z.string().describe(TOKEN_AUTH.GET_TOKEN.tokenId) + }), + response: { + 200: z.object({ + token: IdentityAccessTokensSchema + }) + } + }, + handler: async (req) => { + const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({ + identityId: req.params.identityId, + tokenId: req.params.tokenId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + isActorSuperAdmin: isSuperAdmin(req.auth) + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg.scopeOrgId, + event: { + type: EventType.GET_TOKEN_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: token.identityId, + tokenId: token.id + } + } + }); + + return { token }; + } + }); + server.route({ method: "PATCH", url: "/token-auth/tokens/:tokenId", diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index e6969da61..8c2c90711 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -31,6 +31,7 @@ import { TAttachTokenAuthDTO, TCreateTokenAuthTokenDTO, TGetTokenAuthDTO, + TGetTokenAuthTokenByIdDTO, TGetTokenAuthTokensDTO, TRevokeTokenAuthDTO, TRevokeTokenAuthTokenDTO, @@ -499,6 +500,51 @@ export const identityTokenAuthServiceFactory = ({ return { tokens, identityMembershipOrg }; }; + const getTokenAuthTokenById = async ({ + tokenId, + identityId, + isActorSuperAdmin, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TGetTokenAuthTokenByIdDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have Token Auth" + }); + } + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + + const token = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH + }); + + if (!token) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); + + return { token, identityMembershipOrg }; + }; + const updateTokenAuthToken = async ({ tokenId, name, @@ -642,6 +688,7 @@ export const identityTokenAuthServiceFactory = ({ revokeIdentityTokenAuth, createTokenAuthToken, getTokenAuthTokens, + getTokenAuthTokenById, updateTokenAuthToken, revokeTokenAuthToken }; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 16cd60db7..fdecc6d4c 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -40,6 +40,12 @@ export type TGetTokenAuthTokensDTO = { isActorSuperAdmin?: boolean; } & Omit; +export type TGetTokenAuthTokenByIdDTO = { + tokenId: string; + identityId: string; + isActorSuperAdmin?: boolean; +} & Omit; + export type TUpdateTokenAuthTokenDTO = { tokenId: string; name?: string; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index a0eb828e8..b7be75e1c 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -765,9 +765,7 @@ export type CreateTokenIdentityTokenAuthDTO = { export type CreateTokenIdentityTokenAuthRes = { accessToken: string; - tokenType: string; - expiresIn: number; - accessTokenMaxTTL: number; + tokenData: IdentityAccessToken; }; export type UpdateTokenIdentityTokenAuthDTO = {