diff --git a/.env.example b/.env.example index 1f3f64591..e98401462 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 +JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200 JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201 # JWT lifetime diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 9fb096469..ebe73292e 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -110,14 +110,14 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); permissionCheckFn = (env: string, secPath: string) => isValidScopeV3({ authPayload: req.authData.authPayload as IServiceTokenDataV3, environment: env, secretPath: secPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); break; } @@ -213,7 +213,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); break; } @@ -285,7 +285,7 @@ export const createSecretRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -384,7 +384,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -464,7 +464,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -559,14 +559,14 @@ export const getSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); permissionCheckFn = (env: string, secPath: string) => isValidScopeV3({ authPayload: req.authData.authPayload as IServiceTokenDataV3, environment: env, secretPath: secPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); break; } @@ -644,7 +644,7 @@ export const getSecretByName = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ, Permission.READ_WRITE] + requiredPermissions: [Permission.READ] }); break; } @@ -717,7 +717,7 @@ export const createSecret = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -816,7 +816,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -892,7 +892,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -950,7 +950,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -999,7 +999,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } @@ -1048,7 +1048,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - acceptedPermissions: [Permission.READ_WRITE] + requiredPermissions: [Permission.WRITE] }); break; } diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts index 452fbf62c..16505be06 100644 --- a/backend/src/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -94,6 +94,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { user, workspace: new Types.ObjectId(workspaceId), publicKey, + usageCount: 0, scopes, isActive, expiresAt diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 52d95c9d5..733bf80e0 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -64,7 +64,7 @@ class EELicenseService { secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: true, + rbac: false, customRateLimits: false, customAlerts: false, auditLogs: false, diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index bf7e7d868..861301fb6 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -249,7 +249,8 @@ export const getAuthSTDPayload = async ({ isActive: true }, { - lastUsed: new Date() + lastUsed: new Date(), + $inc: { usageCount: 1 } }, { new: true diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 1c91d68e2..ac4563f25 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -69,12 +69,12 @@ export const isValidScopeV3 = ({ authPayload, environment, secretPath, - acceptedPermissions + requiredPermissions }: { authPayload: IServiceTokenDataV3, environment: string, secretPath: string, - acceptedPermissions: Permission[] + requiredPermissions: Permission[] }) => { const { scopes } = authPayload; @@ -84,7 +84,7 @@ export const isValidScopeV3 = ({ scope.environment === environment ); - if (validScope && !acceptedPermissions.includes(validScope.permission)) { + if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) { return false; } diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index fe09a5a2e..1a7277022 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -2,13 +2,13 @@ import { Document, Schema, Types, model } from "mongoose"; export enum Permission { READ = "read", - READ_WRITE = "readWrite" + WRITE = "write" } export interface Scope { environment: string; secretPath: string; - permission: Permission; + permissions: Permission[]; } export interface IServiceTokenDataV3 extends Document { @@ -19,6 +19,7 @@ export interface IServiceTokenDataV3 extends Document { publicKey: string; isActive: boolean; lastUsed?: Date; + usageCount: number; expiresAt?: Date; scopes: Array; } @@ -51,6 +52,11 @@ const serviceTokenDataV3Schema = new Schema( type: Date, required: false }, + usageCount: { + type: Number, + default: 0, + required: true + }, expiresAt: { type: Date, required: false, @@ -68,9 +74,10 @@ const serviceTokenDataV3Schema = new Schema( default: "/", required: true }, - permission: { - type: String, - enum: [Permission.READ, Permission.READ_WRITE], + permissions: { + type: [String], + enum: [Permission.READ, Permission.WRITE], + default: [Permission.READ], required: true } } diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts index 2aa5c476a..9760d1af1 100644 --- a/backend/src/validation/serviceTokenDataV3.ts +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -20,13 +20,13 @@ import { isValidScopeV3 } from "../helpers"; workspaceId, environment, secretPath = "/", - acceptedPermissions + requiredPermissions }: { serviceTokenData: IServiceTokenDataV3; workspaceId: Types.ObjectId; environment?: string; secretPath?: string; - acceptedPermissions: Permission[]; + requiredPermissions: Permission[]; }) => { if (!serviceTokenData.workspace.equals(workspaceId)) { @@ -41,7 +41,7 @@ import { isValidScopeV3 } from "../helpers"; authPayload: serviceTokenData, environment, secretPath, - acceptedPermissions + requiredPermissions }); if (!isValid) throw UnauthorizedRequestError({ @@ -57,7 +57,7 @@ export const CreateServiceTokenV3 = z.object({ publicKey: z.string().trim(), scopes: z .object({ - permission: z.enum(["read", "readWrite"]), + permissions: z.enum(["read", "write"]).array(), environment: z.string().trim(), secretPath: z.string().trim() }) @@ -78,7 +78,7 @@ export const UpdateServiceTokenV3 = z.object({ isActive: z.boolean().optional(), scopes: z .object({ - permission: z.enum(["read", "readWrite"]), + permissions: z.enum(["read", "write"]).array(), environment: z.string().trim(), secretPath: z.string().trim() }) diff --git a/frontend/src/hooks/api/serviceTokens/enums.ts b/frontend/src/hooks/api/serviceTokens/enums.ts new file mode 100644 index 000000000..317bdb0f6 --- /dev/null +++ b/frontend/src/hooks/api/serviceTokens/enums.ts @@ -0,0 +1,4 @@ +export enum Permission { + READ = "read", + WRITE = "write" +} \ No newline at end of file diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index d26fca99c..6ef513e4c 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -1,3 +1,5 @@ +import { Permission } from "./enums"; + export type ServiceTokenScope = { environment: string; secretPath: string; @@ -37,7 +39,7 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken }; // --- v3 export type ServiceTokenV3Scope = { - permission: string; + permissions: Permission[]; environment: string; secretPath: string; }; @@ -48,6 +50,7 @@ export type ServiceTokenDataV3 = { workspace: string; isActive: boolean; lastUsed?: string; + usageCount: number; scopes: ServiceTokenV3Scope[]; expiresAt?: string; createdAt: string; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index f0e03217b..7e073f010 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -32,6 +32,9 @@ import { useGetUserWsKey, useUpdateServiceTokenV3 } from "@app/hooks/api"; +import { + Permission +} from "@app/hooks/api/serviceTokens/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; const expirations = [ @@ -43,7 +46,13 @@ const expirations = [ { label: "12 months", value: "31104000" } ]; -const permissionValues: Array<"read" | "readWrite"> = ["read", "readWrite"]; +const permissionsMap: { + "read": Permission[], + "readWrite": Permission[] +} = { + "read": [Permission.READ], + "readWrite": [Permission.READ, Permission.WRITE], +} const schema = yup.object({ name: yup.string().required("ST V3 name is required"), @@ -51,7 +60,7 @@ const schema = yup.object({ scopes: yup .array( yup.object({ - permission: yup.string().oneOf(permissionValues, "Invalid permission").required().label("Permission"), + permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission") as yup.SchemaOf<"read" | "readWrite", object>, environment: yup.string().max(50).required().label("Environment"), secretPath: yup .string() @@ -112,7 +121,18 @@ export const AddServiceTokenV3Modal = ({ if (serviceTokenData) { reset({ name: serviceTokenData.name, - scopes: serviceTokenData.scopes + scopes: serviceTokenData.scopes.map((scope) => { + let permission = "read"; + if (scope.permissions.includes(Permission.WRITE)) { + permission = "readWrite"; + } + + return ({ + environment: scope.environment, + secretPath: scope.secretPath, + permission + }) + }) }); } else { reset({ @@ -140,12 +160,21 @@ export const AddServiceTokenV3Modal = ({ scopes: any; }; + // convert read/readWrite permission => ["read", "write"] format + const reformattedScopes = scopes.map((scope) => { + return ({ + environment: scope.environment, + secretPath: scope.secretPath, + permissions: permissionsMap[scope.permission] + }); + }) + if (serviceTokenData) { // update await updateMutateAsync({ serviceTokenDataId: serviceTokenData.serviceTokenDataId, name, - scopes, + scopes: reformattedScopes, expiresIn: expiresIn === "" ? undefined : Number(expiresIn) }); } else { @@ -176,7 +205,7 @@ export const AddServiceTokenV3Modal = ({ name, workspaceId: currentWorkspace._id, publicKey, - scopes, + scopes: reformattedScopes, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), encryptedKey: ciphertext, nonce diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index 7c964612c..28a4e237b 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -21,9 +21,8 @@ import { useGetWorkspaceServiceTokenDataV3, useUpdateServiceTokenV3 } from "@app/hooks/api"; -import { - ServiceTokenV3Scope -} from "@app/hooks/api/serviceTokens/types" +import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types" +import { Permission } from "@app/hooks/api/serviceTokens/enums" import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -90,6 +89,7 @@ export const ServiceTokenV3Table = ({ Name Status Scopes + # Times Used Last Used Created At Expires At @@ -106,6 +106,7 @@ export const ServiceTokenV3Table = ({ name, isActive, lastUsed, + usageCount, scopes, createdAt, expiresAt @@ -135,16 +136,25 @@ export const ServiceTokenV3Table = ({ {scopes.map((scope) => { + let permissionText = "read" + if ( + scope.permissions.includes(Permission.WRITE) && + scope.permissions.includes(Permission.READ) + ) { + permissionText = "readWrite"; + } + return (

- {scope.permission} + {scope.permissions} {` @${scope.environment} - ${scope.secretPath}`}

); })} + {usageCount} {lastUsed ? formatDate(lastUsed) : "-"} {formatDate(createdAt)} {expiresAt ? formatDate(expiresAt) : "-"}