From 698a268b5f3c9929246994753359ddb805c057e1 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 25 Sep 2023 13:24:28 +0100 Subject: [PATCH] Add permissions and audit logging to service tokens v3 --- .../v3/serviceTokenDataController.ts | 117 ++++++++++++++++-- backend/src/ee/models/auditLog/enums.ts | 7 +- backend/src/ee/models/auditLog/types.ts | 36 ++++++ backend/src/interfaces/middleware/index.ts | 3 +- backend/src/models/serviceTokenDataV3.ts | 2 +- backend/src/validation/integration.ts | 4 + backend/src/validation/integrationAuth.ts | 4 + backend/src/validation/organization.ts | 4 + backend/src/validation/workspace.ts | 8 +- .../ServiceTokenSection.tsx | 2 +- .../ServiceTokenV3Section.tsx | 34 +++-- .../ServiceTokenV3Table.tsx | 83 ++++++++----- 12 files changed, 246 insertions(+), 58 deletions(-) diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts index 947cbda68..fe7d9fc9d 100644 --- a/backend/src/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -4,9 +4,23 @@ import { ServiceTokenDataV3, ServiceTokenDataV3Key } from "../../models"; +import { + Scope +} from "../../models/serviceTokenDataV3"; +import { + EventType +} from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/serviceTokenV3"; import { createToken } from "../../helpers/auth"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; /** * Create service token data @@ -26,6 +40,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { nonce // for ServiceTokenDataV3Key } } = await validateRequest(reqValidator.CreateServiceTokenV3, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.ServiceTokens + ); let expiresAt; if (expiresIn) { @@ -33,12 +52,13 @@ export const createServiceTokenData = async (req: Request, res: Response) => { expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); } + const isActive = false; const serviceTokenData = await new ServiceTokenDataV3({ name, workspace: new Types.ObjectId(workspaceId), publicKey, scopes, - isActive: false, + isActive, expiresAt }).save(); @@ -58,6 +78,22 @@ export const createServiceTokenData = async (req: Request, res: Response) => { secret: "hello" // TODO: replace with real secret }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_SERVICE_TOKEN_V3, + metadata: { + name, + isActive, + scopes: scopes as Array, + expiresAt + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.status(200).send({ serviceTokenData, serviceToken: `proj_token.${token}` @@ -81,13 +117,29 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { } } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); + let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId); + + if (!serviceTokenData) throw ResourceNotFoundError({ + message: "Service token not found" + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + serviceTokenData.workspace.toString() + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.ServiceTokens + ); + let expiresAt; if (expiresIn) { expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); } - const serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( + serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate( serviceTokenDataId, { name, @@ -99,6 +151,26 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { new: true } ); + + if (!serviceTokenData) throw BadRequestError({ + message: "Failed to update service token" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_SERVICE_TOKEN_V3, + metadata: { + name, + isActive, + scopes: scopes as Array, + expiresAt + } + }, + { + workspaceId: serviceTokenData.workspace + } + ); return res.status(200).send({ serviceTokenData @@ -116,13 +188,42 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { params: { serviceTokenDataId } } = await validateRequest(reqValidator.DeleteServiceTokenV3, req); - const serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId); + let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId); + if (!serviceTokenData) throw ResourceNotFoundError({ + message: "Service token not found" + }); - if (serviceTokenData) { - await ServiceTokenDataV3Key.findOneAndDelete({ - serviceTokenData: serviceTokenData._id - }); - } + const { permission } = await getUserProjectPermissions( + req.user._id, + serviceTokenData.workspace.toString() + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.ServiceTokens + ); + + serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId); + + if (!serviceTokenData) throw BadRequestError({ + message: "Failed to delete service token" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_SERVICE_TOKEN_V3, + metadata: { + name: serviceTokenData.name, + isActive: serviceTokenData.isActive, + scopes: serviceTokenData.scopes as Array, + expiresAt: serviceTokenData.expiresAt + } + }, + { + workspaceId: serviceTokenData.workspace + } + ); return res.status(200).send({ serviceTokenData diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 5024c8bf5..8bf0a02c7 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -26,8 +26,11 @@ export enum EventType { ADD_TRUSTED_IP = "add-trusted-ip", UPDATE_TRUSTED_IP = "update-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip", - CREATE_SERVICE_TOKEN = "create-service-token", - DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_SERVICE_TOKEN = "create-service-token", // v2 + DELETE_SERVICE_TOKEN = "delete-service-token", // v2 + CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3 + UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3 + DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3 CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 44ab909a6..1b3b9915f 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -2,6 +2,9 @@ import { ActorType, EventType } from "./enums"; +import { + Scope +} from "../../../models/serviceTokenDataV3"; interface UserActorMetadata { userId: string; @@ -194,6 +197,36 @@ interface DeleteServiceTokenEvent { } } +interface CreateServiceTokenV3Event { + type: EventType.CREATE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + } +} + +interface UpdateServiceTokenV3Event { + type: EventType.UPDATE_SERVICE_TOKEN_V3; + metadata: { + name?: string; + isActive?: boolean; + scopes?: Array; + expiresAt?: Date; + } +} + +interface DeleteServiceTokenV3Event { + type: EventType.DELETE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + } +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -390,6 +423,9 @@ export type Event = | DeleteTrustedIPEvent | CreateServiceTokenEvent | DeleteServiceTokenEvent + | CreateServiceTokenV3Event + | UpdateServiceTokenV3Event + | DeleteServiceTokenV3Event | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index f873eff26..5146d25cd 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -6,6 +6,7 @@ import { } from "../../models"; import { ServiceActor, + ServiceActorV3, UserActor, UserAgentType } from "../../ee/models"; @@ -23,7 +24,7 @@ export interface UserAuthData extends BaseAuthData { } export interface ServiceTokenV3AuthData extends BaseAuthData { - actor: ServiceActor; + actor: ServiceActorV3; authPayload: IServiceTokenDataV3; } diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index e30420e04..fa0aca079 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -5,7 +5,7 @@ enum Permission { READ_WRITE = "readWrite" } -interface Scope { +export interface Scope { environment: string; secretPath: string; permission: Permission; diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index 7795b0084..365168db3 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -58,6 +58,10 @@ export const validateClientForIntegration = async ({ throw UnauthorizedRequestError({ message: "Failed service token authorization for integration" }); + case ActorType.SERVICE_V3: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration" + }); } }; diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index af73257c6..093976913 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -58,6 +58,10 @@ const validateClientForIntegrationAuth = async ({ throw UnauthorizedRequestError({ message: "Failed service token authorization for integration authorization" }); + case ActorType.SERVICE_V3: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration authorization" + }); } }; diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 9aa13cbb6..ce93db9b0 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -46,6 +46,10 @@ export const validateClientForOrganization = async ({ throw UnauthorizedRequestError({ message: "Failed service token authorization for organization" }); + case ActorType.SERVICE_V3: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for organization" + }); } }; diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index b2d41cff8..32c295bc4 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -7,6 +7,7 @@ import { WorkspaceNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { z } from "zod"; import { EventType, UserAgentType } from "../ee/models"; +import { UnauthorizedRequestError } from "../utils/errors"; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -56,8 +57,11 @@ export const validateClientForWorkspace = async ({ environment, requiredPermissions }); - - return {}; + break; + case ActorType.SERVICE_V3: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for organization" + }); } }; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx index b560165f2..94a185871 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -50,7 +50,7 @@ export const ServiceTokenSection = withProjectPermission(

- {t("section.token.service-tokens")} + Service Tokens

{ +export const ServiceTokenV3Section = withProjectPermission( + () => { const { createNotification } = useNotificationContext(); const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -46,16 +50,24 @@ export const ServiceTokenV3Section = () => {

- Service Tokens 2.0 + (New) Service Tokens

- + {(isAllowed) => ( + + )} +
{ />
); -} \ No newline at end of file + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens } +); \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index fcade0890..2d5b07fb1 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -2,6 +2,7 @@ import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, @@ -15,7 +16,7 @@ import { THead, Tr } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context"; import { useGetWorkspaceServiceTokenDataV3, useUpdateServiceTokenV3 @@ -96,7 +97,7 @@ export const ServiceTokenV3Table = ({ - {isLoading && } + {isLoading && } {!isLoading && data && data.length > 0 && @@ -140,43 +141,59 @@ export const ServiceTokenV3Table = ({ {formatDate(createdAt)} {expiresAt ? formatDate(expiresAt) : "-"} - { - handlePopUpOpen("serviceTokenV3", { - serviceTokenDataId: _id, - name, - scopes, - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - > - - - { - handlePopUpOpen("deleteServiceTokenV3", { - serviceTokenDataId: _id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - > - - + + {(isAllowed) => ( + { + handlePopUpOpen("serviceTokenV3", { + serviceTokenDataId: _id, + name, + scopes, + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + isDisabled={!isAllowed} + > + + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteServiceTokenV3", { + serviceTokenDataId: _id, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + ); })} {!isLoading && data && data?.length === 0 && ( - +