Merge pull request #1027 from Infisical/service-token-v3
Service Token V3
@@ -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
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIG
|
||||
export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue;
|
||||
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue;
|
||||
export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue;
|
||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
||||
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
|
||||
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
||||
|
||||
@@ -395,9 +395,9 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, 'imports.environment': oldEnvironmentSlug },
|
||||
{ $set: { 'imports.$[element].environment': environmentSlug } },
|
||||
{ arrayFilters: [{ 'element.environment': oldEnvironmentSlug }] },
|
||||
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
|
||||
{ $set: { "imports.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
|
||||
);
|
||||
|
||||
await ServiceAccountWorkspacePermission.updateMany(
|
||||
|
||||
@@ -7,5 +7,5 @@ export {
|
||||
authController,
|
||||
secretsController,
|
||||
signupController,
|
||||
workspacesController,
|
||||
workspacesController
|
||||
}
|
||||
|
||||
@@ -3,10 +3,21 @@ import { Types } from "mongoose";
|
||||
import { EventService, SecretService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import {
|
||||
containsGlobPatterns,
|
||||
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 +28,99 @@ 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";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { AuthData } from "../../interfaces/middleware";
|
||||
|
||||
const checkSecretsPermission = async ({
|
||||
authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction
|
||||
}: {
|
||||
authData: AuthData;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretAction: ProjectPermissionActions; // CRUD
|
||||
}): Promise<(env: string, secPath: string) => boolean> => {
|
||||
|
||||
let STV2RequiredPermissions = [];
|
||||
let STV3RequiredPermissions: Permission[] = [];
|
||||
|
||||
switch (secretAction) {
|
||||
case ProjectPermissionActions.Create:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Read:
|
||||
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.READ];
|
||||
break;
|
||||
case ProjectPermissionActions.Edit:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Delete:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
}
|
||||
|
||||
switch (authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
const { permission } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
secretAction,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
return (env: string, secPath: string) =>
|
||||
permission.can(
|
||||
secretAction,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
}
|
||||
case ActorType.SERVICE: {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: STV2RequiredPermissions
|
||||
});
|
||||
return () => true;
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData,
|
||||
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
});
|
||||
return (env: string, secPath: string) =>
|
||||
isValidScopeV3({
|
||||
authPayload: authData.authPayload as IServiceTokenDataV3,
|
||||
environment: env,
|
||||
secretPath: secPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
@@ -58,31 +160,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
if (!environment || !workspaceId)
|
||||
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(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
permissionCheckFn = () => true;
|
||||
}
|
||||
const permissionCheckFn = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@@ -148,22 +232,14 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
query: { secretPath, environment, workspaceId, type, include_imports },
|
||||
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]
|
||||
});
|
||||
}
|
||||
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
@@ -206,21 +282,13 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
} = 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
@@ -290,21 +358,13 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
|
||||
} = 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
@@ -355,21 +415,13 @@ 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
@@ -423,31 +475,13 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
|
||||
}
|
||||
|
||||
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(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
permissionCheckFn = () => true;
|
||||
}
|
||||
const permissionCheckFn = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@@ -495,22 +529,14 @@ export const getSecretByName = async (req: Request, res: Response) => {
|
||||
query: { secretPath, environment, workspaceId, type, include_imports },
|
||||
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]
|
||||
});
|
||||
}
|
||||
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
@@ -553,22 +579,14 @@ 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]
|
||||
});
|
||||
}
|
||||
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
secretName,
|
||||
@@ -633,26 +651,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
},
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
|
||||
|
||||
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext))
|
||||
|
||||
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
|
||||
throw BadRequestError({ message: "Missing encrypted key" });
|
||||
|
||||
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]
|
||||
});
|
||||
}
|
||||
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@@ -698,21 +709,13 @@ 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
@@ -740,22 +743,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, 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]
|
||||
});
|
||||
}
|
||||
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
const createdSecrets = await SecretService.createSecretBatch({
|
||||
secretPath,
|
||||
@@ -775,21 +770,13 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
const updatedSecrets = await SecretService.updateSecretBatch({
|
||||
secretPath,
|
||||
@@ -809,21 +796,13 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, 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]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
const deletedSecrets = await SecretService.deleteSecretBatch({
|
||||
secretPath,
|
||||
@@ -836,4 +815,4 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
secrets: deletedSecrets
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Secret } from "../../models";
|
||||
import { Secret, ServiceTokenDataV3 } from "../../models";
|
||||
import { SecretService } from "../../services";
|
||||
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
@@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
message: "Successfully named workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Membership,
|
||||
Secret,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
TFolderSchema,
|
||||
User,
|
||||
Workspace
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ServiceActor,
|
||||
ServiceActorV3,
|
||||
TFolderRootVersionSchema,
|
||||
TrustedIP,
|
||||
UserActor
|
||||
@@ -683,7 +685,7 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.AuditLogs
|
||||
);
|
||||
|
||||
|
||||
const query = {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
...(eventType
|
||||
@@ -698,13 +700,13 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
|
||||
: {}),
|
||||
...(actor
|
||||
? {
|
||||
"actor.type": actor.split("-", 2)[0],
|
||||
"actor.type": actor.substring(0, actor.lastIndexOf("-")),
|
||||
...(actor.split("-", 2)[0] === ActorType.USER
|
||||
? {
|
||||
"actor.metadata.userId": actor.split("-", 2)[1]
|
||||
"actor.metadata.userId": actor.substring(actor.lastIndexOf("-") + 1)
|
||||
}
|
||||
: {
|
||||
"actor.metadata.serviceId": actor.split("-", 2)[1]
|
||||
"actor.metadata.serviceId": actor.substring(actor.lastIndexOf("-") + 1)
|
||||
})
|
||||
}
|
||||
: {}),
|
||||
@@ -772,9 +774,27 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
}));
|
||||
|
||||
const serviceV3Actors: ServiceActorV3[] = (
|
||||
await ServiceTokenDataV3.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
).map((serviceTokenData) => ({
|
||||
type: ActorType.SERVICE_V3,
|
||||
metadata: {
|
||||
serviceId: serviceTokenData._id.toString(),
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
}));
|
||||
|
||||
const actors = [
|
||||
...userActors,
|
||||
...serviceActors,
|
||||
...serviceV3Actors
|
||||
];
|
||||
|
||||
return res.status(200).send({
|
||||
actors: [...userActors, ...serviceActors]
|
||||
actors
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
5
backend/src/ee/controllers/v3/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||
|
||||
export {
|
||||
serviceTokenDataController
|
||||
}
|
||||
321
backend/src/ee/controllers/v3/serviceTokenDataController.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
ServiceTokenDataV3,
|
||||
ServiceTokenDataV3Key,
|
||||
Workspace
|
||||
} from "../../../models";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
import {
|
||||
ActorType,
|
||||
EventType
|
||||
} from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/serviceTokenDataV3";
|
||||
import { createToken } from "../../../helpers/auth";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { getJwtServiceTokenSecret } from "../../../config";
|
||||
|
||||
/**
|
||||
* Return project key for service token
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
|
||||
const key = await ServiceTokenDataV3Key.findOne({
|
||||
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
|
||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||
|
||||
if (!key) throw ResourceNotFoundError({
|
||||
message: "Failed to find project key for service token"
|
||||
});
|
||||
|
||||
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
|
||||
|
||||
return res.status(200).send({
|
||||
key: {
|
||||
_id,
|
||||
workspace,
|
||||
encryptedKey,
|
||||
publicKey,
|
||||
nonce
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create service token data
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
name,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
encryptedKey, // for ServiceTokenDataV3Key
|
||||
nonce // for ServiceTokenDataV3Key
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
||||
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
let user;
|
||||
if (req.authData.actor.type === ActorType.USER) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
const isActive = true;
|
||||
const serviceTokenData = await new ServiceTokenDataV3({
|
||||
name,
|
||||
user,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
publicKey,
|
||||
usageCount: 0,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
scopes,
|
||||
isActive,
|
||||
expiresAt
|
||||
}).save();
|
||||
|
||||
await new ServiceTokenDataV3Key({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceTokenData: serviceTokenData._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
_id: serviceTokenData._id.toString()
|
||||
},
|
||||
expiresIn,
|
||||
secret: await getJwtServiceTokenSecret()
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3,
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
serviceToken: `stv3.${token}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service token data with id [serviceTokenDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { serviceTokenDataId },
|
||||
body: {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
}
|
||||
} = 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
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(serviceTokenData.workspace);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
let reformattedTrustedIps;
|
||||
if (trustedIps) {
|
||||
reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
||||
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
|
||||
});
|
||||
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
}
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenDataId,
|
||||
{
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt
|
||||
},
|
||||
{
|
||||
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: serviceTokenData.name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { serviceTokenDataId }
|
||||
} = await validateRequest(reqValidator.DeleteServiceTokenV3, 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.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<IServiceTokenV3Scope>,
|
||||
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt: serviceTokenData.expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service"
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
SERVICE_V3 = "service-v3"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
@@ -11,40 +12,43 @@ export enum UserAgentType {
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
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_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
||||
}
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||
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",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ActorType, EventType } from "./enums";
|
||||
import {
|
||||
ActorType,
|
||||
EventType
|
||||
} from "./enums";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
@@ -20,7 +27,15 @@ export interface ServiceActor {
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor;
|
||||
export interface ServiceActorV3 {
|
||||
type: ActorType.SERVICE_V3;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor
|
||||
| ServiceActorV3;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@@ -210,6 +225,39 @@ interface DeleteServiceTokenEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenV3Event {
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateServiceTokenV3Event {
|
||||
type: EventType.UPDATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenV3Event {
|
||||
type: EventType.DELETE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@@ -379,50 +427,53 @@ interface UpdateUserRole {
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[];
|
||||
};
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions;
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateServiceTokenV3Event
|
||||
| UpdateServiceTokenV3Event
|
||||
| DeleteServiceTokenV3Event
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions;
|
||||
5
backend/src/ee/routes/v3/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import serviceTokenData from "./serviceTokenData";
|
||||
|
||||
export {
|
||||
serviceTokenData
|
||||
}
|
||||
39
backend/src/ee/routes/v3/serviceTokenData.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { AuthMode } from "../../../variables";
|
||||
import { serviceTokenDataController } from "../../controllers/v3";
|
||||
|
||||
router.get(
|
||||
"/me/key",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenDataKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.updateServiceTokenData
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.deleteServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ITokenVersion,
|
||||
IUser,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
TokenVersion,
|
||||
User,
|
||||
} from "../models";
|
||||
@@ -23,12 +24,14 @@ import {
|
||||
getJwtProviderAuthSecret,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret,
|
||||
getJwtServiceTokenSecret
|
||||
} from "../config";
|
||||
import {
|
||||
AuthMode
|
||||
} from "../variables";
|
||||
import {
|
||||
ServiceTokenAuthData,
|
||||
ServiceTokenV3AuthData,
|
||||
UserAuthData
|
||||
} from "../interfaces/middleware";
|
||||
|
||||
@@ -47,6 +50,9 @@ export const validateAuthMode = ({
|
||||
headers: { [key: string]: string | string[] | undefined },
|
||||
acceptedAuthModes: AuthMode[]
|
||||
}) => {
|
||||
|
||||
// TODO: update this to accept service token v3
|
||||
|
||||
const apiKey = headers["x-api-key"];
|
||||
const authHeader = headers["authorization"];
|
||||
|
||||
@@ -65,6 +71,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")
|
||||
@@ -72,15 +79,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 "stv3":
|
||||
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." });
|
||||
@@ -211,8 +224,73 @@ export const getAuthSTDPayload = async ({
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
userAgentType: getUserAgentType(req.headers["user-agent"])
|
||||
}
|
||||
}
|
||||
|
||||
// return serviceTokenDataToReturn;
|
||||
/**
|
||||
* Return service token data V3 payload corresponding to service token [authTokenValue]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - service token value
|
||||
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||
*/
|
||||
export const getAuthSTDV3Payload = async ({
|
||||
req,
|
||||
authTokenValue,
|
||||
}: {
|
||||
req: Request,
|
||||
authTokenValue: string;
|
||||
}): Promise<ServiceTokenV3AuthData> => {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, await getJwtServiceTokenSecret())
|
||||
);
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(decodedToken._id),
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
lastUsed: new Date(),
|
||||
$inc: { usageCount: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate"
|
||||
});
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenData._id,
|
||||
{
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
actor: {
|
||||
type: ActorType.SERVICE_V3,
|
||||
metadata: {
|
||||
serviceId: serviceTokenData._id.toString(),
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
},
|
||||
authPayload: serviceTokenData,
|
||||
ipAddress: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
userAgentType: getUserAgentType(req.headers["user-agent"])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,11 +460,15 @@ export const createToken = ({
|
||||
secret,
|
||||
}: {
|
||||
payload: any;
|
||||
expiresIn: string | number;
|
||||
expiresIn?: string | number;
|
||||
secret: string;
|
||||
}) => {
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn,
|
||||
...(
|
||||
(expiresIn !== undefined && expiresIn !== null)
|
||||
? { expiresIn }
|
||||
: {}
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
Folder,
|
||||
ISecret,
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
TFolderRootSchema
|
||||
} from "../models";
|
||||
import { Permission } from "../models/serviceTokenDataV3";
|
||||
import { EventType, SecretVersion } from "../ee/models";
|
||||
import {
|
||||
BadRequestError,
|
||||
@@ -53,10 +55,50 @@ import picomatch from "picomatch";
|
||||
import path from "path";
|
||||
import { getAnImportedSecret } from "../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Validate scope for service token v3
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScopeV3 = ({
|
||||
authPayload,
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authPayload: IServiceTokenDataV3,
|
||||
environment: string,
|
||||
secretPath: string,
|
||||
requiredPermissions: Permission[]
|
||||
}) => {
|
||||
const { scopes } = authPayload;
|
||||
|
||||
const validScope = scopes.find(
|
||||
(scope) =>
|
||||
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
|
||||
scope.environment === environment
|
||||
);
|
||||
|
||||
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(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(
|
||||
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
roles as v1RoleRouter,
|
||||
secretScanning as v1SecretScanningRouter
|
||||
} from "./ee/routes/v1";
|
||||
import {
|
||||
serviceTokenData as v3ServiceTokenDataRouter
|
||||
} from "./ee/routes/v3";
|
||||
import {
|
||||
auth as v1AuthRouter,
|
||||
bot as v1BotRouter,
|
||||
@@ -56,7 +59,6 @@ import {
|
||||
organizations as v2OrganizationsRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceAccounts as v2ServiceAccountsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
signup as v2SignupRouter,
|
||||
tags as v2TagsRouter,
|
||||
@@ -155,6 +157,7 @@ const main = async () => {
|
||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||
app.use("/api/v1/sso", eeSSORouter);
|
||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
|
||||
|
||||
// v1 routes
|
||||
app.use("/api/v1/signup", v1SignupRouter);
|
||||
@@ -192,7 +195,7 @@ const main = async () => {
|
||||
app.use("/api/v2/secret", v2SecretRouter); // deprecate
|
||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
// app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use("/api/v3/auth", v3AuthRouter);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
} from "../../models";
|
||||
import {
|
||||
ServiceActor,
|
||||
ServiceActorV3,
|
||||
UserActor,
|
||||
UserAgentType
|
||||
} from "../../ee/models";
|
||||
@@ -21,6 +23,11 @@ export interface UserAuthData extends BaseAuthData {
|
||||
authPayload: IUser;
|
||||
}
|
||||
|
||||
export interface ServiceTokenV3AuthData extends BaseAuthData {
|
||||
actor: ServiceActorV3;
|
||||
authPayload: IServiceTokenDataV3;
|
||||
}
|
||||
|
||||
export interface ServiceTokenAuthData extends BaseAuthData {
|
||||
actor: ServiceActor;
|
||||
authPayload: IServiceTokenData;
|
||||
@@ -28,4 +35,5 @@ export interface ServiceTokenAuthData extends BaseAuthData {
|
||||
|
||||
export type AuthData =
|
||||
| UserAuthData
|
||||
| ServiceTokenV3AuthData
|
||||
| ServiceTokenAuthData;
|
||||
@@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from "express";
|
||||
import {
|
||||
getAuthAPIKeyPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthSTDV3Payload,
|
||||
getAuthUserPayload,
|
||||
validateAuthMode,
|
||||
} from "../helpers/auth";
|
||||
@@ -49,6 +50,12 @@ const requireAuth = ({
|
||||
});
|
||||
req.serviceTokenData = authData.authPayload;
|
||||
break;
|
||||
case AuthMode.SERVICE_TOKEN_V3:
|
||||
authData = await getAuthSTDV3Payload({
|
||||
req,
|
||||
authTokenValue
|
||||
});
|
||||
break;
|
||||
case AuthMode.API_KEY:
|
||||
authData = await getAuthAPIKeyPayload({
|
||||
req,
|
||||
@@ -61,9 +68,7 @@ const requireAuth = ({
|
||||
req,
|
||||
authTokenValue
|
||||
});
|
||||
// authPayload = authUserPayload.user;
|
||||
req.user = authData.authPayload;
|
||||
// req.tokenVersionId = authUserPayload.tokenVersionId; // TODO
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,19 @@ export * from "./tag";
|
||||
export * from "./folder";
|
||||
export * from "./secretImports";
|
||||
export * from "./secretBlindIndexData";
|
||||
export * from "./serviceToken";
|
||||
export * from "./serviceAccount";
|
||||
export * from "./serviceAccountKey";
|
||||
export * from "./serviceAccountOrganizationPermission";
|
||||
export * from "./serviceAccountWorkspacePermission";
|
||||
export * from "./serviceToken"; // TODO: deprecate
|
||||
export * from "./serviceAccount"; // TODO: deprecate
|
||||
export * from "./serviceAccountKey"; // TODO: deprecate
|
||||
export * from "./serviceAccountOrganizationPermission"; // TODO: deprecate
|
||||
export * from "./serviceAccountWorkspacePermission"; // TODO: deprecate
|
||||
export * from "./tokenData";
|
||||
export * from "./user";
|
||||
export * from "./userAction";
|
||||
export * from "./workspace";
|
||||
export * from "./serviceTokenData";
|
||||
export * from "./serviceTokenData"; // TODO: deprecate
|
||||
export * from "./apiKeyData";
|
||||
export * from "./loginSRPDetail";
|
||||
export * from "./tokenVersion";
|
||||
export * from "./webhooks";
|
||||
export * from "./webhooks";
|
||||
export * from "./serviceTokenDataV3";
|
||||
export * from "./serviceTokenDataV3Key";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
export interface IServiceToken {
|
||||
_id: Types.ObjectId;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IServiceTokenData extends Document {
|
||||
|
||||
129
backend/src/models/serviceTokenDataV3.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { IPType } from "../ee/models";
|
||||
|
||||
export enum Permission {
|
||||
READ = "read",
|
||||
WRITE = "write"
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3Scope {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3TrustedIp {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}
|
||||
|
||||
export interface IServiceTokenDataV3 extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
isActive: boolean;
|
||||
lastUsed?: Date;
|
||||
usageCount: number;
|
||||
expiresAt?: Date;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
|
||||
const serviceTokenDataV3Schema = new Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
required: false
|
||||
},
|
||||
usageCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
required: false,
|
||||
expires: 0
|
||||
},
|
||||
scopes: {
|
||||
type: [
|
||||
{
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: [Permission.READ, Permission.WRITE],
|
||||
default: [Permission.READ],
|
||||
required: true
|
||||
}
|
||||
}
|
||||
],
|
||||
required: true
|
||||
},
|
||||
trustedIps: {
|
||||
type: [
|
||||
{
|
||||
ipAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
IPType.IPV4,
|
||||
IPType.IPV6
|
||||
],
|
||||
required: true
|
||||
},
|
||||
prefix: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
],
|
||||
default: [{
|
||||
ipAddress: "0.0.0.0",
|
||||
type: IPType.IPV4.toString(),
|
||||
prefix: 0
|
||||
}],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceTokenDataV3 = model<IServiceTokenDataV3>("ServiceTokenDataV3", serviceTokenDataV3Schema);
|
||||
43
backend/src/models/serviceTokenDataV3Key.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IServiceTokenDataV3Key extends Document {
|
||||
_id: Types.ObjectId;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Types.ObjectId;
|
||||
serviceTokenData: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceTokenDataV3KeySchema = new Schema(
|
||||
{
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
nonce: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sender: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceTokenDataV3",
|
||||
required: true,
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceTokenDataV3Key = model<IServiceTokenDataV3Key>("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema);
|
||||
@@ -7,5 +7,5 @@ export {
|
||||
auth,
|
||||
secrets,
|
||||
signup,
|
||||
workspaces,
|
||||
workspaces
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
|
||||
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
|
||||
);
|
||||
@@ -15,7 +15,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"
|
||||
@@ -29,7 +29,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"
|
||||
@@ -43,7 +43,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"
|
||||
@@ -57,7 +57,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"
|
||||
@@ -71,7 +71,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"
|
||||
@@ -116,7 +116,7 @@ router.delete(
|
||||
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"
|
||||
@@ -127,7 +127,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"
|
||||
@@ -138,7 +138,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"
|
||||
@@ -149,7 +149,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"
|
||||
|
||||
@@ -34,4 +34,12 @@ router.post(
|
||||
|
||||
// --
|
||||
|
||||
router.get(
|
||||
"/:workspaceId/service-token",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
workspacesController.getWorkspaceServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Organization,
|
||||
ServiceAccount,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
User
|
||||
} from "../models";
|
||||
import { createToken } from "../helpers/auth";
|
||||
@@ -54,6 +55,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 };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -62,7 +67,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||
* @returns
|
||||
*/
|
||||
const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||
|
||||
if (authData.authPayload instanceof User) {
|
||||
return { user: authData.authPayload._id };
|
||||
}
|
||||
@@ -72,7 +76,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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import net from "net";
|
||||
import { IPType } from "../../ee/models";
|
||||
import { InternalServerError } from "../errors";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../errors";
|
||||
|
||||
/**
|
||||
* Return details of IP [ip]:
|
||||
@@ -98,4 +98,39 @@ export const isValidIpOrCidr = (ip: string): boolean => {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ipAddress - IP address to check
|
||||
* @param {Object[]} obj.trustedIps - IPs to trust in blocklist
|
||||
*/
|
||||
export const checkIPAgainstBlocklist = ({
|
||||
ipAddress,
|
||||
trustedIps
|
||||
}: {
|
||||
ipAddress: string;
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}[]
|
||||
}) => {
|
||||
const blockList = new net.BlockList();
|
||||
|
||||
for (const trustedIp of trustedIps) {
|
||||
if (trustedIp.prefix !== undefined) {
|
||||
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
|
||||
} else {
|
||||
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
|
||||
}
|
||||
}
|
||||
|
||||
const { type } = extractIPDetails(ipAddress);
|
||||
const check = blockList.check(ipAddress, type);
|
||||
|
||||
if (!check) throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from "./organization";
|
||||
export * from "./secrets";
|
||||
export * from "./serviceAccount";
|
||||
export * from "./serviceTokenData";
|
||||
export * from "./serviceTokenDataV3";
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ export const CreateSecretRawV3 = z.object({
|
||||
secretValue: z
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
|
||||
secretComment: z.string().trim(),
|
||||
secretComment: z.string().trim().optional().default(""),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
|
||||
}),
|
||||
|
||||
119
backend/src/validation/serviceTokenDataV3.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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";
|
||||
import { AuthData } from "../interfaces/middleware";
|
||||
import { checkIPAgainstBlocklist } from "../utils/ip";
|
||||
|
||||
/**
|
||||
* 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 ({
|
||||
authData,
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath = "/",
|
||||
requiredPermissions
|
||||
}: {
|
||||
authData: AuthData;
|
||||
serviceTokenData: IServiceTokenDataV3;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
requiredPermissions: Permission[];
|
||||
}) => {
|
||||
|
||||
// validate ST V3 IP address
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress: authData.ipAddress,
|
||||
trustedIps: serviceTokenData.trustedIps
|
||||
});
|
||||
|
||||
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,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
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({
|
||||
permissions: z.enum(["read", "write"]).array(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
trustedIps: z
|
||||
.object({
|
||||
ipAddress: 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({
|
||||
permissions: z.enum(["read", "write"]).array(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
trustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
expiresIn: z.number().optional()
|
||||
}),
|
||||
});
|
||||
|
||||
export const DeleteServiceTokenV3 = z.object({
|
||||
params: z.object({
|
||||
serviceTokenDataId: z.string()
|
||||
}),
|
||||
});
|
||||
@@ -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
|
||||
@@ -57,8 +58,11 @@ export const validateClientForWorkspace = async ({
|
||||
environment,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
return {};
|
||||
return { membership, workspace};
|
||||
case ActorType.SERVICE_V3:
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for organization"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,3 +304,9 @@ export const NameWorkspaceSecretsV3 = z.object({
|
||||
.array()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetWorkspaceServiceTokenDataV3 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum AuthMode {
|
||||
JWT = "jwt",
|
||||
SERVICE_TOKEN = "serviceToken",
|
||||
SERVICE_TOKEN_V3 = "serviceTokenV3",
|
||||
API_KEY = "apiKey"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,30 +3,63 @@ title: "Service token"
|
||||
description: "Infisical service tokens allows you to programmatically interact with Infisical"
|
||||
---
|
||||
|
||||
Service tokens play an integral role in allowing programmatic interactions with an Infisical project, functioning as digital token that open access to specific project resources such as secrets.
|
||||
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
|
||||
Each service token can be provisioned scoped access to select environment(s) and path(s) within them.
|
||||
|
||||
When you generate a service token, you can define its access level, not only by specifying the paths and environments it can interact with, but also by determining the level of mutation it can perform, such as read-only, write, or both.
|
||||
## Service Tokens
|
||||
|
||||
This level of control not only ensures maximum flexibility but also significantly enhances security as it allows you to define fine grained access to project resources.
|
||||
You can manage service tokens in Project Settings > Service Tokens.
|
||||
|
||||
### Service Token (Current)
|
||||
|
||||
Service Token (ST) is the current widely-used authentication method for managing secrets.
|
||||
|
||||
<Note>
|
||||
We're soon releasing ST V3, a revised version of this Service Token, so stay tuned.
|
||||
</Note>
|
||||
|
||||
Here's a few pointers to get you acquainted with it:
|
||||
|
||||
- When you create a ST, you get a token prefixed with `st`. The part after the last `.` delimiter is a symmetric key; everything
|
||||
before it is an access token. When authenticating with the Infisical API, it is important to send in only the access token portion
|
||||
of the token.
|
||||
- ST supports expiration; it gets deleted automatically upon expiration.
|
||||
- ST supports provisioning `read` and/or `write` permissions broadly applied to all accessible environment(s) and path(s).
|
||||
- ST is not editable.
|
||||
|
||||
## Creating a service token
|
||||
|
||||
To generate the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
|
||||
To create a service token, head to Project Settings > Service Tokens as shown below and press **Create token**.
|
||||
|
||||

|
||||

|
||||
|
||||
### Service token permissions
|
||||

|
||||
Now input any token configuration details such as which environment(s) and path(s) you'd like to provision
|
||||
the token access to. Here's some guidance for each field:
|
||||
|
||||
- Name: A friendly name for the token.
|
||||
- Scopes: The environment(s) and path(s) the token should have access to.
|
||||
- Permissions: You can indicate whether or not the token should have `read/write` access to the paths.
|
||||
Also, note that Infisical supports [glob patterns](https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns/) when defining access scopes to path(s).
|
||||
- Expiration: The time when this token should be rendered inactive.
|
||||
|
||||
Service tokens can be scoped to multiple environments and paths. To add a new permission, choose the environment you want to give access to and then choose the path you'd like to give access to within that environment.
|
||||

|
||||
|
||||
Permissions for paths are powered by [Glob pattern](https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns/). This means you can create advanced folder permissions with a simple Glob patterns.
|
||||
In the above screenshot, you can see that we are creating a token token with `read` access to all subfolders at any depth
|
||||
of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address.
|
||||
|
||||
**Examples of common Glob pattens**
|
||||
**FAQ**
|
||||
|
||||
<Accordion title="Examples of common Glob pattens">
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why is the Infisical API rejecting my service token?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
- The service token has expired.
|
||||
- The service token is insufficently permissioned to interact with the secrets in the given environment and path.
|
||||
- You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE.
|
||||
- (If using ST V3) The service token has not been activated yet.
|
||||
- (If using ST V3) The service token is being used from an untrusted IP.
|
||||
</Accordion>
|
||||
<Accordion title="Can you provide examples for using glob patterns?">
|
||||
1. `/**`: This pattern matches all folders at any depth in the directory structure. For example, it would match folders like `/folder1/`, `/folder1/subfolder/`, and so on.
|
||||
|
||||
2. `/*`: This pattern matches all immediate subfolders in the current directory. It does not match any folders at a deeper level. For example, it would match folders like `/folder1/`, `/folder2/`, but not `/folder1/subfolder/`.
|
||||
@@ -35,3 +68,4 @@ Permissions for paths are powered by [Glob pattern](https://www.malikbrowne.com/
|
||||
|
||||
4. `/folder1/*`: This pattern matches all immediate subfolders within the `/folder1/` directory. It does not match any folders outside of `/folder1/`, nor does it match any subfolders within those immediate subfolders. For example, it would match folders like `/folder1/subfolder1/`, `/folder1/subfolder2/`, but not `/folder2/subfolder/`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
105
docs/documentation/platform/token3.mdx
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "Service token"
|
||||
description: "Infisical service tokens allows you to programmatically interact with Infisical"
|
||||
---
|
||||
|
||||
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
|
||||
Each service token can be provisioned scoped access to select environment(s) and path(s) within them.
|
||||
|
||||
## Service Tokens
|
||||
|
||||
Infisical currently offers Service Token V3 and Service Token; you can manage both types of tokens in Project Settings > Service Tokens.
|
||||
|
||||
### Service Token V3 (Beta)
|
||||
|
||||
Service Token V3 (ST V3) is a new and improved authentication method that is in beta.
|
||||
|
||||
<Note>
|
||||
Currently, the Service Token V3 authentication method can only be used with the latest [Node SDK](https://github.com/Infisical/infisical-node) and [Python SDK](https://github.com/Infisical/infisical-python).
|
||||
You can also make an API call with it to create, read, update, or delete secrets.
|
||||
|
||||
We will be releasing compatibility for it with the CLI and K8s operator in the coming month.
|
||||
|
||||
That said, we recommend using ST V3 whenever possible.
|
||||
</Note>
|
||||
|
||||
Here's a few pointers to get you acquainted with it:
|
||||
|
||||
- When you create a ST V3, you export a `JSON` file containing 3 components: `publicKey`, `privateKey`, and `serviceToken` where
|
||||
`serviceToken` is a JWT token prefixed with `stv3`. The token provides access to the Infisical API and the public-private key
|
||||
pairs are to support cryptographic operations for the client whenever E2EE is needed.
|
||||
- ST V3 supports IP allowlisting; this means you can restrict the usage of a ST V3 to a specific IP or CIDR range.
|
||||
- ST V3 supports provisioning granular `read` or `readWrite` access down to each path.
|
||||
- ST V3 supports toggling on/off active states, so you can render a ST V3 inactive without deleting it.
|
||||
- ST V3 supports expiration, so, if specified, a token will automatically turn inactive after a period of time.
|
||||
- ST V3 tracks most recent usage; it also keeps track of each token's usage count.
|
||||
- ST V3 is editable.
|
||||
|
||||
### Service Token (Current)
|
||||
|
||||
Service Token (ST) is the current widely-used authentication method.
|
||||
|
||||
<Note>
|
||||
We recently released ST V3, a revised version of this Service Token, which you can read about above.
|
||||
|
||||
Whenever possible, you should use ST V3 because we will be deprecating ST sometime Q4 2023.
|
||||
</Note>
|
||||
|
||||
Here's a few pointers to get you acquainted with it:
|
||||
|
||||
- When you create a ST, you get a token prefixed with `st`. The part after the last `.` delimiter is a symmetric key; everything
|
||||
before it is an access token. When authenticating with the Infisical API, it is important to send in only the access token portion
|
||||
of the token.
|
||||
- ST supports expiration; it gets deleted automatically upon expiration.
|
||||
- ST supports provisioning `read` and/or `write` permissions broadly applied to all accessible environment(s) and path(s).
|
||||
- ST is not editable.
|
||||
|
||||
## Creating a service token
|
||||
|
||||
To create a service token, head to Project Settings > Service Tokens as shown below and press **Create token**.
|
||||
|
||||

|
||||
|
||||
Now input any token configuration details such as which environment(s) and path(s) you'd like to provision
|
||||
the token access to. Here's some guidance for each field:
|
||||
|
||||
- Name: A friendly name for the token.
|
||||
- Scopes: The environment(s) and path(s) the token should have access to.
|
||||
If using ST V3, you can also indicate whether or not the token should have `read` or `readWrite` access to each path.
|
||||
Also, note that Infisical supports [glob patterns](https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns/) when defining access scopes to path(s).
|
||||
- Trusted IPs: The IPs or CIDR ranges that the token can be used from. By default, each token is given the `0.0.0.0/0` entry representing all possible IPv4 addresses.
|
||||
- Expiration: The time when this token should be rendered inactive.
|
||||
|
||||
<Warning>
|
||||
Restricting token usage to specific trusted IPs is a paid feature.
|
||||
|
||||
If you’re using Infisical Cloud, then it is available under the Pro Tier. If you’re self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it.
|
||||
</Warning>
|
||||
|
||||

|
||||
|
||||
In the above screenshot, you can see that we are creating a token token with `read` access to all subfolders at any depth
|
||||
of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address.
|
||||
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Why is the Infisical API rejecting my service token?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
- The service token has expired.
|
||||
- The service token is insufficently permissioned to interact with the secrets in the given environment and path.
|
||||
- You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE.
|
||||
- (If using ST V3) The service token has not been activated yet.
|
||||
- (If using ST V3) The service token is being used from an untrusted IP.
|
||||
</Accordion>
|
||||
<Accordion title="Can you provide examples for using glob patterns?">
|
||||
1. `/**`: This pattern matches all folders at any depth in the directory structure. For example, it would match folders like `/folder1/`, `/folder1/subfolder/`, and so on.
|
||||
|
||||
2. `/*`: This pattern matches all immediate subfolders in the current directory. It does not match any folders at a deeper level. For example, it would match folders like `/folder1/`, `/folder2/`, but not `/folder1/subfolder/`.
|
||||
|
||||
3. `/*/*`: This pattern matches all subfolders at a depth of two levels in the current directory. It does not match any folders at a shallower or deeper level. For example, it would match folders like `/folder1/subfolder/`, `/folder2/subfolder/`, but not `/folder1/` or `/folder1/subfolder/subsubfolder/`.
|
||||
|
||||
4. `/folder1/*`: This pattern matches all immediate subfolders within the `/folder1/` directory. It does not match any folders outside of `/folder1/`, nor does it match any subfolders within those immediate subfolders. For example, it would match folders like `/folder1/subfolder1/`, `/folder1/subfolder2/`, but not `/folder2/subfolder/`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 370 KiB |
BIN
docs/images/project-token-old-add.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/images/project-token-old-permissions.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/images/project-token-permissions.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 652 KiB |
72
docs/internals/service-tokens-new.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "Service tokens"
|
||||
description: "Understanding service tokens and their best practices"
|
||||
---
|
||||
|
||||
Many clients use service tokens to authenticate and read/write secrets from/to Infisical; they can be created in your project settings.
|
||||
|
||||
On this page, we discuss Service Token V3, the new and improved authentication method.
|
||||
|
||||
## Anatomy
|
||||
|
||||
A service token in Infisical exports a `JSON` file containing 3 components: `publicKey`, `privateKey`, and `serviceToken` where
|
||||
`serviceToken` is a JWT token prefixed with `proj_token`. The token provides access to the Infisical API and the public-private key
|
||||
pairs are to support cryptographic operations for the client whenever E2EE is needed.
|
||||
|
||||
### Database model
|
||||
|
||||
The storage backend model for a token contains the following information:
|
||||
|
||||
- ID: The token identifier.
|
||||
- Expiration: The date at which point the token is invalid.
|
||||
- Project: The project that the token is part of.
|
||||
- Status: The active/inactive state of a token.
|
||||
- Scopes: The project environment(s) and path(s) that the token has access to as well as `read` or `readWrite` permissions for them.
|
||||
- Trusted IPs: The specific (IPv4 or IPv6) IPs or CIDR ranges that the token can be used from.
|
||||
- Last used: The date at which point the token was last used.
|
||||
- Usage count: The number of times that the token has been used.
|
||||
|
||||
### Token
|
||||
|
||||
As mentioned before, a service token consists of three components, exported as a `JSON`, used for authentication and cryptographic purposes.
|
||||
|
||||
Consider the following `JSON`:
|
||||
|
||||
```
|
||||
{
|
||||
"publicKey": "...",
|
||||
"privateKey": "...",
|
||||
"serviceToken": "stv3..."
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `serviceToken` component can be used to authenticate with the API, by including it in the `Authorization` header under `Bearer <serviceToken>` and retrieve (encrypted) secrets as well as a project key back. Meanwhile, the `privateKey` (in the `JSON`), and `publicKey` (returned in the encrypted project key response) can be used to decrypt the project key used to decrypt the secrets.
|
||||
|
||||
Note that when using service tokens via select client methods like SDK or CLI, cryptographic operations are abstracted for you that is the token is parsed and encryption/decryption operations are handled. If using service tokens with the REST API and end-to-end encryption enabled, then you will have to handle the encryption/decryption operations yourself.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Permissions
|
||||
|
||||
You should consider the [principle of least privilege(PoLP)](https://en.wikipedia.org/wiki/Principle_of_least_privilege) when setting which environment(s) and path(s)
|
||||
should be accessible by a service token; you should also consider whether or not it needs `read` or `readWrite` access.
|
||||
|
||||
For example, if the client using the token only requires `read` access to the secrets in the `/config` path of the staging environment, then you should scope the token to the `/config` path of that environment only with `read` permission.
|
||||
|
||||
### Status & Expiration
|
||||
|
||||
We recommend considering whether or not a service token should be able to access secrets indefinitely or within a finite lifetime such as until 6 months or 1 year from now
|
||||
|
||||
### Network access
|
||||
|
||||
We recommend configuring the IP allowlist configuration of each service token to restrict its usage to specific IP addresses or CIDR-notated range of addresses.
|
||||
|
||||
### Storage
|
||||
|
||||
Since service tokens grant access to your secrets, we recommend storing them securely across your development cycle whether it be in a .env file in local development or as an environment variable of your deployment platform.
|
||||
|
||||
### Rotation
|
||||
|
||||
We recommend periodically rotating the service token, even in the absence of compromise. Since service tokens are capable of decrypting project keys used to decrypt secrets, they should be rotated before approximately 2^32 encryptions have been performed; this follows the guidance set forth by [NIST publication 800-38D](https://csrc.nist.gov/pubs/sp/800/38/d/final).
|
||||
|
||||
Note that Infisical keeps track of the number of times that service tokens are used and will alert you when you have reached 90% of the recommended capacity.
|
||||
@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
"fixed top-1/2 left-1/2 z-30 dark:[color-scheme:dark] max-h-screen thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
"fixed top-1/2 left-1/2 z-30 dark:[color-scheme:dark] max-h-screen thin-scrollbar max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service"
|
||||
SERVICE = "service",
|
||||
SERVICE_V3 = "service-v3"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
@@ -24,8 +25,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",
|
||||
|
||||
@@ -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;
|
||||
@@ -25,9 +35,15 @@ export interface ServiceActor {
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActorV3 {
|
||||
type: ActorType.SERVICE_V3;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor;
|
||||
| ServiceActor
|
||||
| ServiceActorV3;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@@ -190,6 +206,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 +433,9 @@ export type Event =
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateServiceTokenV3Event
|
||||
| UpdateServiceTokenV3Event
|
||||
| DeleteServiceTokenV3Event
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
|
||||
4
frontend/src/hooks/api/serviceTokens/enums.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum Permission {
|
||||
READ = "read",
|
||||
WRITE = "write"
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from "./queries";
|
||||
export {
|
||||
useCreateServiceToken,
|
||||
useCreateServiceTokenV3,
|
||||
useDeleteServiceToken,
|
||||
useDeleteServiceTokenV3,
|
||||
useGetUserWsServiceTokens,
|
||||
useUpdateServiceTokenV3} from "./queries";
|
||||
|
||||
@@ -2,12 +2,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import {
|
||||
CreateServiceTokenDataV3DTO,
|
||||
CreateServiceTokenDataV3Res,
|
||||
CreateServiceTokenDTO,
|
||||
CreateServiceTokenRes,
|
||||
DeleteServiceTokenDataV3DTO,
|
||||
DeleteServiceTokenRes,
|
||||
ServiceToken
|
||||
} from "./types";
|
||||
ServiceToken,
|
||||
ServiceTokenDataV3,
|
||||
UpdateServiceTokenDataV3DTO} from "./types";
|
||||
|
||||
const serviceTokenKeys = {
|
||||
getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, "service-tokens"] as const
|
||||
@@ -32,12 +37,11 @@ export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServic
|
||||
}
|
||||
|
||||
// mutation
|
||||
export const useCreateServiceToken = () => {
|
||||
export const useCreateServiceToken = () => { // TODO: deprecate
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
|
||||
mutationFn: async (body) => {
|
||||
console.log("useCreateServiceToken");
|
||||
const { data } = await apiRequest.post("/api/v2/service-token/", body);
|
||||
data.serviceToken += `.${body.randomBytes}`;
|
||||
return data;
|
||||
@@ -53,7 +57,6 @@ export const useDeleteServiceToken = () => {
|
||||
|
||||
return useMutation<DeleteServiceTokenRes, {}, string>({
|
||||
mutationFn: async (serviceTokenId) => {
|
||||
console.log("useDeleteServiceToken");
|
||||
const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
|
||||
return data;
|
||||
},
|
||||
@@ -62,3 +65,58 @@ export const useDeleteServiceToken = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateServiceTokenV3 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<CreateServiceTokenDataV3Res, {}, CreateServiceTokenDataV3DTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post("/api/v3/service-token/", body);
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ serviceTokenData }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(serviceTokenData.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateServiceTokenV3 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<ServiceTokenDataV3, {}, UpdateServiceTokenDataV3DTO>({
|
||||
mutationFn: async ({
|
||||
serviceTokenDataId,
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
}) => {
|
||||
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
},
|
||||
onSuccess: ({ workspace }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteServiceTokenV3 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<ServiceTokenDataV3, {}, DeleteServiceTokenDataV3DTO>({
|
||||
mutationFn: async ({
|
||||
serviceTokenDataId
|
||||
}) => {
|
||||
const { data: { serviceTokenData } } = await apiRequest.delete(`/api/v3/service-token/${serviceTokenDataId}`);
|
||||
return serviceTokenData;
|
||||
},
|
||||
onSuccess: ({ workspace }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Permission } from "./enums";
|
||||
|
||||
export type ServiceTokenScope = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@@ -33,3 +35,65 @@ export type CreateServiceTokenRes = {
|
||||
};
|
||||
|
||||
export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
|
||||
|
||||
// --- v3
|
||||
|
||||
export type ServiceTokenV3Scope = {
|
||||
permissions: Permission[];
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type ServiceTokenV3TrustedIp = {
|
||||
_id: string;
|
||||
ipAddress: string;
|
||||
type: "ipv4" | "ipv6";
|
||||
prefix?: number;
|
||||
}
|
||||
|
||||
export type ServiceTokenDataV3 = {
|
||||
_id: string;
|
||||
name: string;
|
||||
workspace: string;
|
||||
isActive: boolean;
|
||||
lastUsed?: string;
|
||||
usageCount: number;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
expiresAt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateServiceTokenDataV3DTO = {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
publicKey: string;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
expiresIn?: number;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export type CreateServiceTokenDataV3Res = {
|
||||
serviceToken: string;
|
||||
serviceTokenData: ServiceTokenDataV3;
|
||||
}
|
||||
|
||||
export type UpdateServiceTokenDataV3DTO = {
|
||||
serviceTokenDataId: string;
|
||||
isActive?: boolean;
|
||||
name?: string;
|
||||
scopes?: ServiceTokenV3Scope[];
|
||||
trustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
expiresIn?: number;
|
||||
}
|
||||
|
||||
export type DeleteServiceTokenDataV3DTO = {
|
||||
serviceTokenDataId: string;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceServiceTokenDataV3,
|
||||
useGetWorkspaceUsers,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
import { TIntegration } from "../integrations/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { ServiceTokenDataV3 } from "../serviceTokens/types";
|
||||
import { TWorkspaceUser } from "../users/types";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
@@ -32,7 +33,8 @@ export const workspaceKeys = {
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
|
||||
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceServiceTokenDataV3: (workspaceId: string) => [{ workspaceId }, "workspace-service-token-data-v3"] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -361,3 +363,19 @@ export const useUpdateUserWorkspaceRole = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceServiceTokenDataV3(workspaceId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { serviceTokenData }
|
||||
} = await apiRequest.get<{ serviceTokenData: ServiceTokenDataV3[] }>(
|
||||
`/api/v3/workspaces/${workspaceId}/service-token`
|
||||
);
|
||||
|
||||
return serviceTokenData;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
@@ -50,6 +50,12 @@ export const LogsFilter = ({
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
case ActorType.SERVICE_V3:
|
||||
return (
|
||||
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-v3-filter-${actor.metadata.serviceId}`}>
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SelectItem value="actor-none" key="actor-none">
|
||||
|
||||
@@ -93,6 +93,4 @@ export const LogsTable = ({
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: retrieve count
|
||||
}
|
||||
@@ -29,6 +29,13 @@ export const LogsTableRow = ({
|
||||
<p>Service token</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.SERVICE_V3:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Service token V3</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Td />
|
||||
@@ -160,6 +167,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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
@@ -37,68 +38,55 @@ export const APIKeyTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
|
||||
const formattedDate = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer className="">
|
||||
<Table>
|
||||
<THead>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Name</Th>
|
||||
<Th className="flex-1">Last active</Th>
|
||||
<Th className="flex-1">Created</Th>
|
||||
<Th className="flex-1">Expiration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`api-key-${_id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{format(new Date(lastUsed), "yyyy-MM-dd")}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>{format(new Date(expiresAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteAPIKeyDataClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Th className="flex-1">Name</Th>
|
||||
<Th className="flex-1">Last active</Th>
|
||||
<Th className="flex-1">Created</Th>
|
||||
<Th className="flex-1">Expiration</Th>
|
||||
<Th className="w-5" />
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No API Keys on file" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`api-key-${_id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{formatDate(lastUsed)}</Td>
|
||||
<Td>{formatDate(createdAt)}</Td>
|
||||
<Td>{formatDate(expiresAt)}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteAPIKeyDataClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No API Keys on file" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,18 +161,18 @@ export const AddAPIKeyModal = ({
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
|
||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
||||
import { WebhooksTab } from "./components/WebhooksTab";
|
||||
@@ -17,12 +15,9 @@ const tabs = [
|
||||
export const ProjectSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||
<div className="w-full max-w-screen-lg">
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<div className="flex justify-center w-full h-full bg-bunker-800 text-white">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ServiceTokenSection } from "../ServiceTokenSection";
|
||||
// import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
|
||||
|
||||
export const ProjectServiceTokensTab = () => {
|
||||
return (
|
||||
<ServiceTokenSection />
|
||||
<>
|
||||
{/* <ServiceTokenV3Section /> */}
|
||||
<ServiceTokenSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -88,7 +88,10 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
scopes: [{ secretPath: "/", environment: currentWorkspace?.environments?.[0]?.slug }]
|
||||
scopes: [{
|
||||
secretPath: "/",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,7 +136,7 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
plaintext: key,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
|
||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ServiceTokenSection = withProjectPermission(
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
{t("section.token.service-tokens")}
|
||||
Service Tokens
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Token Name</Th>
|
||||
<Th>Envrionment - Secret Path</Th>
|
||||
<Th>Environment - Secret Path</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus,faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import {
|
||||
useCreateServiceTokenV3,
|
||||
useGetUserWsKey,
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
Permission
|
||||
} from "@app/hooks/api/serviceTokens/enums";
|
||||
import {
|
||||
ServiceTokenV3Scope,
|
||||
ServiceTokenV3TrustedIp
|
||||
} from "@app/hooks/api/serviceTokens/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirations = [
|
||||
{ label: "Never", value: undefined },
|
||||
{ label: "1 day", value: "86400" },
|
||||
{ label: "7 days", value: "604800" },
|
||||
{ label: "1 month", value: "2592000" },
|
||||
{ label: "6 months", value: "15552000" },
|
||||
{ label: "12 months", value: "31104000" }
|
||||
];
|
||||
|
||||
const permissionsMap: {
|
||||
[key: string]: Permission[]
|
||||
} = {
|
||||
"read": [Permission.READ],
|
||||
"readWrite": [Permission.READ, Permission.WRITE],
|
||||
}
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required("ST V3 name is required"),
|
||||
expiresIn: yup.string(),
|
||||
scopes: yup
|
||||
.array(
|
||||
yup.object({
|
||||
permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"),
|
||||
environment: yup.string().max(50).required().label("Environment"),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.default("/")
|
||||
.label("Secret Path")
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Scope"),
|
||||
trustedIps: yup
|
||||
.array(
|
||||
yup.object({
|
||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Trusted IP")
|
||||
}).required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["serviceTokenV3", "upgradePlan"]>;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["serviceTokenV3", "upgradePlan"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddServiceTokenV3Modal = ({
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
secretPath: "/",
|
||||
}],
|
||||
trustedIps: [{
|
||||
ipAddress: "0.0.0.0/0"
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
serviceTokenDataId: string;
|
||||
name: string;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
};
|
||||
|
||||
if (serviceTokenData) {
|
||||
reset({
|
||||
name: serviceTokenData.name,
|
||||
scopes: serviceTokenData.scopes.map(({
|
||||
environment,
|
||||
secretPath,
|
||||
permissions
|
||||
}: ServiceTokenV3Scope) => {
|
||||
let permission = "read";
|
||||
if (permissions.includes(Permission.WRITE)) {
|
||||
permission = "readWrite";
|
||||
}
|
||||
|
||||
return ({
|
||||
environment,
|
||||
secretPath,
|
||||
permission
|
||||
})
|
||||
}),
|
||||
trustedIps: serviceTokenData.trustedIps.map(({
|
||||
ipAddress,
|
||||
prefix
|
||||
}: ServiceTokenV3TrustedIp) => {
|
||||
return ({
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
});
|
||||
})
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
secretPath: "/",
|
||||
}],
|
||||
trustedIps: [{
|
||||
ipAddress: "0.0.0.0/0"
|
||||
}]
|
||||
});
|
||||
}
|
||||
}, [popUp?.serviceTokenV3?.data]);
|
||||
|
||||
const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" });
|
||||
const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" });
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
expiresIn,
|
||||
scopes,
|
||||
trustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
serviceTokenDataId: string;
|
||||
name: string;
|
||||
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: reformattedScopes,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
|
||||
});
|
||||
} else {
|
||||
// create
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const { serviceToken } = await createMutateAsync({
|
||||
name,
|
||||
workspaceId: currentWorkspace._id,
|
||||
publicKey,
|
||||
scopes: reformattedScopes,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
});
|
||||
|
||||
const downloadData = {
|
||||
publicKey,
|
||||
privateKey,
|
||||
serviceToken
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = href;
|
||||
link.download = `infisical_${name}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("serviceTokenV3", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.serviceTokenV3?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("serviceTokenV3", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="My ST V3"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{tokenScopes.map(({ id }, index) => (
|
||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.permission`}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Permission" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-36"
|
||||
>
|
||||
<SelectItem value="read" key="st-v3-read">
|
||||
Read
|
||||
</SelectItem>
|
||||
<SelectItem value="readWrite" key="st-v3-write">
|
||||
Read & Write
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.environment`}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Environment" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-36"
|
||||
>
|
||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.secretPath`}
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Secrets Path" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => remove(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
append({
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||
secretPath: "/"
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
{tokenTrustedIps.map(({ id }, index) => (
|
||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`trustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Trusted IP" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue="15552000"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirations.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp?.upgradePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { faPlus } 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 {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useDeleteServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
|
||||
import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
|
||||
|
||||
export const ServiceTokenV3Section = withProjectPermission(
|
||||
() => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"serviceTokenV3",
|
||||
"deleteServiceTokenV3",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onDeleteServiceTokenDataSubmit = async (serviceTokenDataId: string) => {
|
||||
try {
|
||||
await deleteMutateAsync({
|
||||
serviceTokenDataId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted service token v3",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteServiceTokenV3");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service token v3",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Service Tokens V3 (Beta)
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("serviceTokenV3")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<ServiceTokenV3Table
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddServiceTokenV3Modal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteServiceTokenV3.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteServiceTokenV3?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteServiceTokenV3", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteServiceTokenDataSubmit(
|
||||
(popUp?.deleteServiceTokenV3?.data as { serviceTokenDataId: string })?.serviceTokenDataId
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
);
|
||||
@@ -0,0 +1,230 @@
|
||||
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Switch,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetWorkspaceServiceTokenDataV3,
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import { Permission } from "@app/hooks/api/serviceTokens/enums"
|
||||
import { ServiceTokenV3Scope, ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>,
|
||||
data?: {
|
||||
serviceTokenDataId?: string;
|
||||
name?: string;
|
||||
scopes?: ServiceTokenV3Scope[];
|
||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ServiceTokenV3Table = ({
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || "");
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
|
||||
const handleToggleServiceTokenDataStatus = async ({
|
||||
serviceTokenDataId,
|
||||
isActive
|
||||
}: {
|
||||
serviceTokenDataId: string;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
try {
|
||||
await updateMutateAsync({
|
||||
serviceTokenDataId,
|
||||
isActive
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Scopes</Th>
|
||||
<Th>Trusted IPs</Th>
|
||||
{/* <Th># Times Used</Th> */}
|
||||
<Th>Last Used</Th>
|
||||
<Th>Created At</Th>
|
||||
<Th>Expires At</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={7} innerKey="service-tokens" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({
|
||||
_id,
|
||||
name,
|
||||
isActive,
|
||||
lastUsed,
|
||||
// usageCount,
|
||||
scopes,
|
||||
trustedIps,
|
||||
createdAt,
|
||||
expiresAt
|
||||
}) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id={`enable-service-token-${_id}`}
|
||||
onCheckedChange={(value) => handleToggleServiceTokenDataStatus({
|
||||
serviceTokenDataId: _id,
|
||||
isActive: value
|
||||
})}
|
||||
isChecked={isActive}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{scopes.map((scope) => {
|
||||
let permissionText = "read"
|
||||
if (
|
||||
scope.permissions.includes(Permission.WRITE) &&
|
||||
scope.permissions.includes(Permission.READ)
|
||||
) {
|
||||
permissionText = "readWrite";
|
||||
}
|
||||
|
||||
return (
|
||||
<p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}>
|
||||
<span className="font-bold">
|
||||
{permissionText}
|
||||
</span>
|
||||
{` @${scope.environment} - ${scope.secretPath}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
{trustedIps.map(({
|
||||
_id: trustedIpId,
|
||||
ipAddress,
|
||||
prefix
|
||||
}) => {
|
||||
return (
|
||||
<p key={`service-token-${_id}-}-trusted-ip-${trustedIpId}`}>
|
||||
{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
{/* <Td>{usageCount}</Td> */}
|
||||
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("serviceTokenV3", {
|
||||
serviceTokenDataId: _id,
|
||||
name,
|
||||
scopes,
|
||||
trustedIps
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteServiceTokenV3", {
|
||||
serviceTokenDataId: _id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7}>
|
||||
<EmptyState title="No service token v3 on file" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||