Scope switch cases into blocks for secrets v3

This commit is contained in:
Tuan Dang
2023-09-26 10:56:08 +01:00
parent 45b85ab962
commit 4683dc7869
19 changed files with 620 additions and 244 deletions

View File

@@ -3,10 +3,22 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
import {
containsGlobPatterns,
isValidScope,
isValidScopeV3,
repackageSecretToRaw
} from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IServiceTokenData } from "../../models";
import {
Folder,
IServiceTokenData,
IServiceTokenDataV3
} from "../../models";
import {
Permission
} from "../../models/serviceTokenDataV3";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
@@ -17,8 +29,12 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import { validateServiceTokenDataClientForWorkspace } from "../../validation";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models";
/**
* Return secrets for workspace with id [workspaceId] and environment
@@ -59,30 +75,52 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
});
break;
}
}
const secrets = await SecretService.getSecrets({
@@ -150,20 +188,35 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ]
});
break;
}
}
const secret = await SecretService.getSecret({
@@ -198,20 +251,35 @@ export const createSecretRaw = async (req: Request, res: Response) => {
body: { secretPath, environment, workspaceId, type, secretValue, secretComment }
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const key = await BotService.getWorkspaceKeyWithBot({
@@ -281,20 +349,35 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
body: { secretValue, environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const key = await BotService.getWorkspaceKeyWithBot({
@@ -345,20 +428,35 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const { secret } = await SecretService.deleteSecret({
@@ -414,30 +512,53 @@ export const getSecrets = async (req: Request, res: Response) => {
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
});
break;
}
}
const secrets = await SecretService.getSecrets({
@@ -486,21 +607,36 @@ export const getSecretByName = async (req: Request, res: Response) => {
query: { secretPath, environment, workspaceId, type },
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ]
});
break;
}
}
const secret = await SecretService.getSecret({
@@ -543,20 +679,35 @@ export const createSecret = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const secret = await SecretService.createSecret({
@@ -613,22 +764,37 @@ export const updateSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -665,20 +831,35 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
switch (req.authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
});
break;
}
}
const { secret } = await SecretService.deleteSecret({

View File

@@ -8,10 +8,11 @@ import {
Scope
} from "../../models/serviceTokenDataV3";
import {
ActorType,
EventType
} from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenV3";
import * as reqValidator from "../../validation/serviceTokenDataV3";
import { createToken } from "../../helpers/auth";
import {
ProjectPermissionActions,
@@ -52,9 +53,15 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
const isActive = false;
const serviceTokenData = await new ServiceTokenDataV3({
name,
user,
workspace: new Types.ObjectId(workspaceId),
publicKey,
scopes,
@@ -161,7 +168,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
{
type: EventType.UPDATE_SERVICE_TOKEN_V3,
metadata: {
name,
name: serviceTokenData.name,
isActive,
scopes: scopes as Array<Scope>,
expiresAt

View File

@@ -70,6 +70,7 @@ export const validateAuthMode = ({
if (typeof authHeader === "string") {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null]
if (tokenType === null)
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
if (tokenType.toLowerCase() !== "bearer")
@@ -77,18 +78,21 @@ export const validateAuthMode = ({
if (tokenValue === null)
throw BadRequestError({ message: "Missing Authorization Body in the request header." });
switch (tokenValue.split(".", 1)[0]) {
const parts = tokenValue.split(".");
switch (parts[0]) {
case "st":
authMode = AuthMode.SERVICE_TOKEN;
authTokenValue = tokenValue;
break;
case "proj_token":
authMode = AuthMode.SERVICE_TOKEN_V3;
authTokenValue = parts.slice(1).join('.');
break;
default:
authMode = AuthMode.JWT;
authTokenValue = tokenValue;
}
authTokenValue = tokenValue;
}
if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." });
@@ -235,29 +239,39 @@ export const getAuthSTDPayload = async ({
authTokenValue: string;
}): Promise<ServiceTokenV3AuthData> => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, "hello") // TODO: change this
jwt.verify(authTokenValue, "hello") // TODO: replace with real secret
);
// perhaps turn this one into a find one and update call?
const serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
_id: new Types.ObjectId(decodedToken._id),
isActive: true
});
if (!serviceTokenData) {
throw UnauthorizedRequestError({
message: "Failed to authenticate" // standardize auth error messages
});
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenDataV3.findByIdAndDelete(serviceTokenData._id);
// TODO: test expired token
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
isActive: false
},
{
new: true
}
);
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
}
return {
actor: {
type: ActorType.SERVICE_V3, // should this be servicev3 bc the shape of it is different?
type: ActorType.SERVICE_V3,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name

View File

@@ -10,11 +10,16 @@ import {
Folder,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
Secret,
SecretBlindIndexData,
ServiceTokenData,
TFolderRootSchema
} from "../models";
import {
Scope,
Permission
} from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import {
BadRequestError,
@@ -49,10 +54,50 @@ import { getFolderByPath, getFolderIdFromServiceToken } from "../services/Folder
import picomatch from "picomatch";
import path from "path";
/**
* Validate scope for service token v3
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScopeV3 = ({
authPayload,
environment,
secretPath,
acceptedPermissions
}: {
authPayload: IServiceTokenDataV3,
environment: string,
secretPath: string,
acceptedPermissions: Permission[]
}) => {
const { scopes } = authPayload;
const validScope = scopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
if (validScope && !acceptedPermissions.includes(validScope.permission)) {
return false;
}
return Boolean(validScope);
}
/**
* Validate scope for service token v2
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScope = (
authPayload: IServiceTokenData,
environment: string,
secretPath: string
secretPath: string,
) => {
const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find(

View File

@@ -1,6 +1,6 @@
import { Document, Schema, Types, model } from "mongoose";
enum Permission {
export enum Permission {
READ = "read",
READ_WRITE = "readWrite"
}
@@ -15,6 +15,7 @@ export interface IServiceTokenDataV3 extends Document {
_id: Types.ObjectId;
name: string;
workspace: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
isActive: boolean;
lastUsed?: Date;
@@ -33,6 +34,11 @@ const serviceTokenDataV3Schema = new Schema(
ref: "Workspace",
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
publicKey: {
type: String,
required: true

View File

@@ -13,7 +13,7 @@ import {
router.get(
"/raw",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
secretsController.getSecretsRaw
);
@@ -21,7 +21,7 @@ router.get(
router.get(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -35,7 +35,7 @@ router.get(
router.post(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -49,7 +49,7 @@ router.post(
router.patch(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -63,7 +63,7 @@ router.patch(
router.delete(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -77,7 +77,7 @@ router.delete(
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -88,7 +88,7 @@ router.get(
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -99,7 +99,7 @@ router.post(
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -110,7 +110,7 @@ router.get(
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -121,7 +121,7 @@ router.patch(
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"

View File

@@ -8,6 +8,7 @@ import {
Organization,
ServiceAccount,
ServiceTokenData,
ServiceTokenDataV3,
User
} from "../models";
import { createToken } from "../helpers/auth";
@@ -49,6 +50,10 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
@@ -57,7 +62,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
* @returns
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
@@ -67,7 +71,11 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };0
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { user: authData.authPayload.user };
}
}

View File

@@ -9,4 +9,4 @@ export * from "./organization";
export * from "./secrets";
export * from "./serviceAccount";
export * from "./serviceTokenData";
export * from "./serviceTokenV3";
export * from "./serviceTokenDataV3";

View File

@@ -254,7 +254,7 @@ export const CreateSecretRawV3 = z.object({
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secretValue: z.string().trim(),
secretComment: z.string().trim(),
secretComment: z.string().trim().optional().default(""),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
}),
params: z.object({

View File

@@ -0,0 +1,96 @@
import { Types } from "mongoose";
import { IServiceTokenDataV3 } from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { z } from "zod";
import { UnauthorizedRequestError } from "../utils/errors";
import { isValidScopeV3 } from "../helpers";
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} acceptedPermissions - accepted permissions as part of the endpoint
*/
export const validateServiceTokenDataV3ClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
secretPath = "/",
acceptedPermissions
}: {
serviceTokenData: IServiceTokenDataV3;
workspaceId: Types.ObjectId;
environment?: string;
secretPath?: string;
acceptedPermissions: Permission[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: "Failed service token authorization for the given workspace"
});
}
if (environment) {
const isValid = isValidScopeV3({
authPayload: serviceTokenData,
environment,
secretPath,
acceptedPermissions
});
if (!isValid) throw UnauthorizedRequestError({
message: "Failed service token authorization for the given workspace"
});
}
};
export const CreateServiceTokenV3 = z.object({
body: z.object({
name: z.string().trim(),
workspaceId: z.string().trim(),
publicKey: z.string().trim(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1),
expiresIn: z.number().optional(),
encryptedKey: z.string().trim(),
nonce: z.string().trim()
})
});
export const UpdateServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
body: z.object({
name: z.string().trim().optional(),
isActive: z.boolean().optional(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1)
.optional(),
expiresIn: z.number().optional()
}),
});
export const DeleteServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
});

View File

@@ -1,46 +0,0 @@
import { z } from "zod";
export const CreateServiceTokenV3 = z.object({
body: z.object({
name: z.string().trim(),
workspaceId: z.string().trim(),
publicKey: z.string().trim(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1),
expiresIn: z.number().optional(),
encryptedKey: z.string().trim(),
nonce: z.string().trim()
})
});
export const UpdateServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
body: z.object({
name: z.string().trim().optional(),
isActive: z.boolean().optional(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1)
.optional(),
expiresIn: z.number().optional()
}),
});
export const DeleteServiceTokenV3 = z.object({
params: z.object({
serviceTokenDataId: z.string()
}),
});

View File

@@ -57,7 +57,7 @@ export const validateClientForWorkspace = async ({
environment,
requiredPermissions
});
break;
return { membership, workspace};
case ActorType.SERVICE_V3:
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization"

View File

@@ -16,6 +16,9 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_TRUSTED_IP]: "Delete trusted IP",
[EventType.CREATE_SERVICE_TOKEN]: "Create service token",
[EventType.DELETE_SERVICE_TOKEN]: "Delete service token",
[EventType.CREATE_SERVICE_TOKEN_V3]: "Create (new) service token",
[EventType.UPDATE_SERVICE_TOKEN_V3]: "Update (new) service token",
[EventType.DELETE_SERVICE_TOKEN_V3]: "Delete (new) service token",
[EventType.CREATE_ENVIRONMENT]: "Create environment",
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
[EventType.DELETE_ENVIRONMENT]: "Delete environment",

View File

@@ -24,8 +24,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",

View File

@@ -4,6 +4,17 @@ import {
UserAgentType
} from "./enums";
enum Permission {
READ = "read",
READ_WRITE = "readWrite"
}
interface Scope {
environment: string;
secretPath: string;
permission: Permission;
}
interface UserActorMetadata {
userId: string;
email: string;
@@ -14,7 +25,6 @@ interface ServiceActorMetadata {
name: string;
}
interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@@ -190,6 +200,36 @@ interface DeleteServiceTokenEvent {
}
}
interface CreateServiceTokenV3Event {
type: EventType.CREATE_SERVICE_TOKEN_V3;
metadata: {
name: string;
isActive: boolean;
scopes: Array<Scope>;
expiresAt?: Date;
}
}
interface UpdateServiceTokenV3Event {
type: EventType.UPDATE_SERVICE_TOKEN_V3;
metadata: {
name?: string;
isActive?: boolean;
scopes?: Array<Scope>;
expiresAt?: Date;
}
}
interface DeleteServiceTokenV3Event {
type: EventType.DELETE_SERVICE_TOKEN_V3;
metadata: {
name: string;
isActive: boolean;
scopes: Array<Scope>;
expiresAt?: Date;
}
}
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@@ -387,6 +427,9 @@ export type Event =
| DeleteTrustedIPEvent
| CreateServiceTokenEvent
| DeleteServiceTokenEvent
| CreateServiceTokenV3Event
| UpdateServiceTokenV3Event
| DeleteServiceTokenV3Event
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent

View File

@@ -93,6 +93,4 @@ export const LogsTable = ({
)}
</TableContainer>
);
}
// TODO: retrieve count
}

View File

@@ -160,6 +160,24 @@ export const LogsTableRow = ({
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.CREATE_SERVICE_TOKEN_V3:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.UPDATE_SERVICE_TOKEN_V3:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_SERVICE_TOKEN_V3:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.CREATE_ENVIRONMENT:
return (
<Td>

View File

@@ -420,7 +420,7 @@ export const AddServiceTokenV3Modal = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel

View File

@@ -64,7 +64,7 @@ export const ServiceTokenV3Section = withProjectPermission(
onClick={() => handlePopUpOpen("serviceTokenV3")}
isDisabled={!isAllowed}
>
Create ST V3
Create token
</Button>
)}
</ProjectPermissionCan>