1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-27 09:40:45 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
066cc94d3c fix: frontend/package.json & frontend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ZOD-5925617
2023-10-04 16:32:40 +00:00
167 changed files with 1226 additions and 6844 deletions
.env.example
.github
README.md
backend
cli/packages/cmd
docs
frontend
Dockerfilenext.config.jspackage-lock.jsonpackage.json
public
src
components
context/ProjectPermissionContext
hooks/api
layouts/AppLayout
pages
integrations/github
login
signup
views
IntegrationsPage/components/IntegrationsSection
Login
Login.tsx
components
InitialStep
MFAStep
PasswordStep
Project
AuditLogsPage/components
MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection
SecretApprovalPage
SecretMainPage
SecretOverviewPage
Settings
BillingSettingsPage/components/BillingCloudTab
PersonalSettingsPage
ProjectSettingsPage
img

@ -9,7 +9,6 @@ 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

5
.github/values.yaml vendored

@ -40,15 +40,12 @@ backend:
annotations: {}
type: ClusterIP
nodePort: ""
resources:
limits:
memory: 300Mi
backendEnvironmentVariables: null
## Mongo DB persistence
mongodb:
enabled: false
enabled: true
persistence:
enabled: false

@ -93,7 +93,6 @@ jobs:
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
@ -117,4 +116,3 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

@ -83,7 +83,6 @@ jobs:
tags: infisical/staging_deployment_frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
@ -106,7 +105,6 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest

@ -1,8 +1,9 @@
<h1 align="center">
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1>
<p align="center">
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
<p align="center"><b>Open-source, end-to-end encrypted secret management platform</b>: distribute secrets/configs across your team/infrastructure and prevent secret leaks.</p>
</p>
<h4 align="center">
@ -33,7 +34,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -43,11 +44,11 @@
</a>
</h4>
<img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
## Introduction
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.

@ -50,7 +50,6 @@
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
@ -64,7 +63,7 @@
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6",
"zod": "^3.22.3"
"zod": "^3.21.4"
},
"devDependencies": {
"@jest/globals": "^29.3.1",
@ -13728,17 +13727,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"dependencies": {
"passport-oauth2": "^1.4.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@ -16696,9 +16684,9 @@
}
},
"node_modules/zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@ -27175,14 +27163,6 @@
"passport-oauth2": "1.x.x"
}
},
"passport-gitlab2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
"requires": {
"passport-oauth2": "^1.4.0"
}
},
"passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@ -29404,9 +29384,9 @@
"dev": true
},
"zod": {
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
}
}
}

@ -41,7 +41,6 @@
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
@ -55,7 +54,7 @@
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6",
"zod": "^3.22.3"
"zod": "^3.21.4"
},
"name": "infisical-api",
"version": "1.0.0",

@ -1,5 +1,3 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
@ -28,7 +26,6 @@ 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;
@ -55,9 +52,6 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () => (await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () => (await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () => (await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";

@ -16,6 +16,8 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
export {
authController,
botController,
@ -34,5 +36,6 @@ export {
workspaceController,
secretScanningController,
webhookController,
secretImpsController
secretImpsController,
secretApprovalPolicyController
};

@ -1,22 +1,20 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { nanoid } from "nanoid";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
} from "../../ee/services/ProjectRoleService";
import { validateRequest } from "../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
import { BadRequestError } from "../../../utils/errors";
import { BadRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/secretApproval";
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, secretPath, approvers, environment, workspaceId, name }
body: { approvals, secretPath, approvers, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@ -27,7 +25,6 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) =>
const secretApproval = new SecretApprovalPolicy({
workspace: workspaceId,
name: name ?? `${environment}-${nanoid(3)}`,
secretPath,
environment,
approvals,
@ -42,7 +39,7 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) =>
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, approvers, secretPath, name },
body: { approvals, approvers, secretPath },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
@ -61,7 +58,6 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) =>
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
approvals,
approvers,
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
});
@ -111,18 +107,3 @@ export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
approvals: doc
});
};
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, secretPath }
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
);
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
return res.send({ policy: secretApprovalPolicy });
};

@ -34,10 +34,10 @@ import { Webhook } from "../../models";
* @returns
*/
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -46,12 +46,12 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project",
"required": true,
"type": "string"
}
}
/*
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -60,7 +60,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project",
"required": true,
"type": "string"
}
}
#swagger.requestBody = {
content: {
@ -88,7 +88,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"schema": {
"type": "object",
"properties": {
"message": {
@ -115,7 +115,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
},
"description": "Response after creating a new environment"
}
}
}
}
}
*/
@ -246,7 +246,7 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
* @returns
*/
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Rename workspace environment'
#swagger.description = 'Rename a specific environment within a workspace'
@ -317,7 +317,7 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
}
}
}
}
}
*/
const {
params: { workspaceId },
@ -394,11 +394,6 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await ServiceAccountWorkspacePermission.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
@ -452,10 +447,10 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
* @returns
*/
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Delete workspace environment'
#swagger.description = 'Delete a specific environment from a workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -488,7 +483,7 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"schema": {
"type": "object",
"properties": {
"message": {
@ -506,9 +501,9 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
}
}
*/
const {
params: { workspaceId },
@ -595,10 +590,10 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
// TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/*
/*
#swagger.summary = 'Get all accessible environments of a workspace'
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
@ -645,7 +640,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res:
}
}
}
}
}
*/
const {
params: { workspaceId }

@ -7,5 +7,5 @@ export {
authController,
secretsController,
signupController,
workspacesController
workspacesController,
}

@ -3,11 +3,10 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
import { Permission } from "../../models/serviceTokenDataV3";
import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
@ -18,115 +17,8 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { validateServiceTokenDataClientForWorkspace } 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";
import {
generateSecretApprovalRequest,
getSecretPolicyOfBoard
} from "../../ee/services/SecretApprovalService";
import { CommitType } from "../../ee/models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
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, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (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
@ -166,13 +58,31 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
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 secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -239,13 +149,21 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
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]
});
}
const secret = await SecretService.getSecret({
secretName,
@ -288,13 +206,21 @@ export const createSecretRaw = async (req: Request, res: Response) => {
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Create
});
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]
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -364,13 +290,21 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Edit
});
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]
});
}
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
@ -421,13 +355,21 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
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]
});
}
const { secret } = await SecretService.deleteSecret({
secretName,
@ -481,13 +423,31 @@ export const getSecrets = async (req: Request, res: Response) => {
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
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 secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -536,13 +496,21 @@ export const getSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Read
});
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]
});
}
const secret = await SecretService.getSecret({
secretName,
@ -586,44 +554,20 @@ export const createSecret = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Create
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.CREATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const secret = await SecretService.createSecret({
@ -690,50 +634,23 @@ 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" });
}
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Edit
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.UPDATE]: [
{
secretName,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const secret = await SecretService.updateSecret({
@ -781,34 +698,20 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: [
{
secretName
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const { secret } = await SecretService.deleteSecret({
@ -838,30 +741,20 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Create
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const createdSecrets = await SecretService.createSecretBatch({
@ -882,30 +775,20 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Edit
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const updatedSecrets = await SecretService.updateSecretBatch({
@ -926,30 +809,20 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
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]
});
}
const deletedSecrets = await SecretService.deleteSecretBatch({

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenDataV3 } from "../../models";
import { Secret } from "../../models";
import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
@ -101,17 +101,3 @@ 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
});
}

@ -8,8 +8,6 @@ import * as actionController from "./actionController";
import * as membershipController from "./membershipController";
import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
export {
secretController,
@ -21,7 +19,5 @@ export {
actionController,
membershipController,
cloudProductsController,
roleController,
secretApprovalPolicyController,
secretApprovalRequestController
roleController
};

@ -1,333 +0,0 @@
import { Request, Response } from "express";
import { getUserProjectPermissions } from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { Folder } from "../../../models";
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
import * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
import { Types } from "mongoose";
import { EEAuditLogService } from "../../services";
import { EventType } from "../../models";
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const approvalRequestCount = await SecretApprovalRequest.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId)
}
},
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{
$group: {
_id: "$status",
count: { $sum: 1 }
}
}
]);
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
return res.send({
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
});
};
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const {
query: { status, committer, workspaceId, environment, limit, offset }
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const query = {
workspace: new Types.ObjectId(workspaceId),
environment,
committer: committer ? new Types.ObjectId(committer) : undefined,
status
};
// to strip of undefined in query we use es6 spread to ignore those fields
Object.entries(query).forEach(
([key, value]) => value === undefined && delete query[key as keyof typeof query]
);
const approvalRequests = await SecretApprovalRequest.aggregate([
{
$match: query
},
{ $sort: { createdAt: -1 } },
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{ $skip: offset },
{ $limit: limit }
]);
if (!approvalRequests.length) return res.send({ approvals: [] });
const unqiueEnvs = environment ?? {
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
};
const approvalRootFolders = await Folder.find({
workspace: workspaceId,
environment: unqiueEnvs
}).lean();
const formatedApprovals = approvalRequests.map((el) => {
let secretPath = "/";
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
if (folders) {
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
}
return { ...el, secretPath };
});
return res.send({
approvals: formatedApprovals
});
};
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ policy: ISecretApprovalPolicy }>("policy")
.populate({
path: "commits.secretVersion",
populate: {
path: "tags"
}
})
.populate("commits.secret", "version")
.populate("commits.newVersion.tags")
.lean();
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
// allow to fetch only if its admin or is the committer or approver
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find(
(approverId) => approverId.toString() === membership._id.toString()
)
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
let secretPath = "/";
const approvalRootFolders = await Folder.findOne({
workspace: secretApprovalRequest.workspace,
environment: secretApprovalRequest.environment
}).lean();
if (approvalRootFolders) {
secretPath =
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
?.folderPath || "/";
}
return res.send({
approval: { ...secretApprovalRequest, secretPath }
});
};
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
({ member }) => member.toString() === membership._id.toString()
);
if (reviewerPos !== -1) {
secretApprovalRequest.reviewers[reviewerPos].status = status;
} else {
secretApprovalRequest.reviewers.push({ member: membership._id, status });
}
await secretApprovalRequest.save();
return res.send({ status });
};
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
{}
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
const approval = await performSecretApprovalRequestMerge(
id,
req.authData,
membership._id.toString()
);
return res.send({ approval });
};
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
if (secretApprovalRequest.hasMerged)
throw BadRequestError({ message: "Approval request has been merged" });
if (secretApprovalRequest.status === "close" && status === "close")
throw BadRequestError({ message: "Approval request is already closed" });
if (secretApprovalRequest.status === "open" && status === "open")
throw BadRequestError({ message: "Approval request is already open" });
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
id,
{ status, statusChangeBy: membership._id },
{ new: true }
);
if (status === "close") {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_CLOSED,
metadata: {
closedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
} else {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_REOPENED,
metadata: {
reopenedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
}
return res.send({ approval: updatedRequest });
};

@ -5,7 +5,6 @@ import {
Membership,
Secret,
ServiceTokenData,
ServiceTokenDataV3,
TFolderSchema,
User,
Workspace
@ -21,7 +20,6 @@ import {
SecretSnapshot,
SecretVersion,
ServiceActor,
ServiceActorV3,
TFolderRootVersionSchema,
TrustedIP,
UserActor
@ -685,7 +683,7 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
);
const query = {
workspace: new Types.ObjectId(workspaceId),
...(eventType
@ -700,13 +698,13 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
: {}),
...(actor
? {
"actor.type": actor.substring(0, actor.lastIndexOf("-")),
"actor.type": actor.split("-", 2)[0],
...(actor.split("-", 2)[0] === ActorType.USER
? {
"actor.metadata.userId": actor.substring(actor.lastIndexOf("-") + 1)
"actor.metadata.userId": actor.split("-", 2)[1]
}
: {
"actor.metadata.serviceId": actor.substring(actor.lastIndexOf("-") + 1)
"actor.metadata.serviceId": actor.split("-", 2)[1]
})
}
: {}),
@ -774,27 +772,9 @@ 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
actors: [...userActors, ...serviceActors]
});
};

@ -1,5 +0,0 @@
import * as serviceTokenDataController from "./serviceTokenDataController";
export {
serviceTokenDataController
}

@ -1,321 +0,0 @@
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,7 +1,6 @@
export enum ActorType {
USER = "user",
SERVICE = "service",
SERVICE_V3 = "service-v3"
USER = "user",
SERVICE = "service"
}
export enum UserAgentType {
@ -29,11 +28,8 @@ 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", // 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_SERVICE_TOKEN = "create-service-token",
DELETE_SERVICE_TOKEN = "delete-service-token",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@ -50,9 +46,5 @@ export enum EventType {
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",
SECRET_APPROVAL_MERGED = "secret-approval-merged",
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
}

@ -1,11 +1,4 @@
import {
ActorType,
EventType
} from "./enums";
import {
IServiceTokenV3Scope,
IServiceTokenV3TrustedIp
} from "../../../models/serviceTokenDataV3";
import { ActorType, EventType } from "./enums";
interface UserActorMetadata {
userId: string;
@ -27,15 +20,7 @@ export interface ServiceActor {
metadata: ServiceActorMetadata;
}
export interface ServiceActorV3 {
type: ActorType.SERVICE_V3;
metadata: ServiceActorMetadata;
}
export type Actor =
| UserActor
| ServiceActor
| ServiceActorV3;
export type Actor = UserActor | ServiceActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -225,39 +210,6 @@ 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: {
@ -427,49 +379,14 @@ interface UpdateUserRole {
}
interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
metadata: {
userId: string;
email: string;
deniedPermissions: {
environmentSlug: string;
ability: string;
}[]
}
}
interface SecretApprovalMerge {
type: EventType.SECRET_APPROVAL_MERGED;
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: {
mergedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalClosed {
type: EventType.SECRET_APPROVAL_CLOSED;
metadata: {
closedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalReopened {
type: EventType.SECRET_APPROVAL_REOPENED;
metadata: {
reopenedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST;
metadata: {
committedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
userId: string;
email: string;
deniedPermissions: {
environmentSlug: string;
ability: string;
}[];
};
}
@ -492,9 +409,6 @@ export type Event =
| DeleteTrustedIPEvent
| CreateServiceTokenEvent
| DeleteServiceTokenEvent
| CreateServiceTokenV3Event
| UpdateServiceTokenV3Event
| DeleteServiceTokenV3Event
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
@ -511,8 +425,4 @@ export type Event =
| UpdateSecretImportEvent
| DeleteSecretImportEvent
| UpdateUserRole
| UpdateUserDeniedPermissions
| SecretApprovalMerge
| SecretApprovalClosed
| SecretApprovalRequest
| SecretApprovalReopened;
| UpdateUserDeniedPermissions;

@ -1,203 +0,0 @@
import { Schema, Types, model } from "mongoose";
import { customAlphabet } from "nanoid";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
export enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
export enum CommitType {
DELETE = "delete",
UPDATE = "update",
CREATE = "create"
}
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
export interface ISecretApprovalSecChange {
_id: Types.ObjectId;
version: number;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentCiphertext?: string;
skipMultilineEncoding?: boolean;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
tags?: string[];
}
export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
| {
newVersion: ISecretApprovalSecChange;
op: CommitType.CREATE;
}
| {
// secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed
// on merge
secretVersion: J;
secret: T;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
op: CommitType.UPDATE;
}
| {
secret: T;
secretVersion: J;
op: CommitType.DELETE;
}
>;
export interface ISecretApprovalRequest {
_id: Types.ObjectId;
committer: Types.ObjectId;
slug: string;
statusChangeBy: Types.ObjectId;
reviewers: {
member: Types.ObjectId;
status: ApprovalStatus;
}[];
workspace: Types.ObjectId;
environment: string;
folderId: string;
hasMerged: boolean;
status: "open" | "close";
policy: Types.ObjectId;
commits: ISecretCommits;
conflicts: Array<{ secretId: string; op: CommitType }>;
}
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
version: {
type: Number,
default: 1,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
skipMultilineEncoding: {
type: Boolean,
required: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
default: ALGORITHM_AES_256_GCM
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8
},
tags: {
ref: "Tag",
type: [Schema.Types.ObjectId],
default: []
}
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
slug: {
type: String,
default: () => nanoId()
},
reviewers: {
type: [
{
member: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
},
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}
],
default: []
},
policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" },
hasMerged: { type: Boolean, default: false },
status: { type: String, enum: ["close", "open"], default: "open" },
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" },
commits: [
{
secret: { type: Types.ObjectId, ref: "Secret" },
newVersion: secretApprovalSecretChangeSchema,
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
op: { type: String, enum: [CommitType], required: true }
}
],
conflicts: {
type: [
{
secretId: { type: String, required: true },
op: { type: String, enum: [CommitType], required: true }
}
],
default: []
}
},
{
timestamps: true
}
);
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
"SecretApprovalRequest",
secretApprovalRequestSchema
);

@ -8,8 +8,6 @@ import action from "./action";
import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning";
import roles from "./role";
import secretApprovalPolicy from "./secretApprovalPolicy";
import secretApprovalRequest from "./secretApprovalRequest";
export {
secret,
@ -21,7 +19,5 @@ export {
action,
cloudProducts,
secretScanning,
roles,
secretApprovalPolicy,
secretApprovalRequest
roles
};

@ -1,55 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { secretApprovalRequestController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequests
);
router.get(
"/count",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestCount
);
router.get(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestDetails
);
router.post(
"/:id/merge",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.mergeSecretApprovalRequest
);
router.post(
"/:id/review",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalReviewStatus
);
router.post(
"/:id/status",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalRequestStatus
);
export default router;

@ -6,20 +6,58 @@ import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import { AuthMode } from "../../../variables";
router.get("/redirect/google", authLimiter, (req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
}
};
passport.authenticate("saml", options)(req, res, next);
});
router.post(
"/saml2/:ssoIdentifier",
passport.authenticate("saml", {

@ -1,5 +0,0 @@
import serviceTokenData from "./serviceTokenData";
export {
serviceTokenData
}

@ -1,39 +0,0 @@
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;

@ -1,12 +1,12 @@
import { Types } from "mongoose";
import * as Sentry from "@sentry/node";
import NodeCache from "node-cache";
import {
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "../../config";
import {
import {
licenseKeyRequest,
licenseServerKeyRequest,
refreshLicenseKeyToken,
@ -37,7 +37,6 @@ interface FeatureSet {
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
has_used_trial: boolean;
secretApproval: boolean;
}
/**
@ -47,7 +46,7 @@ interface FeatureSet {
* - Self-hosted enterprise: Fetch and update global feature set
*/
class EELicenseService {
private readonly _isLicenseValid: boolean; // TODO: deprecate
public instanceType: "self-hosted" | "enterprise-self-hosted" | "cloud" = "self-hosted";
@ -73,19 +72,18 @@ class EELicenseService {
samlSSO: false,
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false
has_used_trial: true
}
public localFeatureSet: NodeCache;
constructor() {
this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({
stdTTL: 60,
});
}
public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise<FeatureSet> {
try {
if (this.instanceType === "cloud") {
@ -98,7 +96,7 @@ class EELicenseService {
if (!organization) throw OrganizationNotFoundError();
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
if (workspaceId) {
url += `?workspaceId=${workspaceId}`;
}
@ -116,14 +114,14 @@ class EELicenseService {
return this.globalFeatureSet;
}
public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`);
await this.getPlan(organizationId, workspaceId);
}
}
public async delPlan(organizationId: Types.ObjectId) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId.toString()}-`);
@ -138,23 +136,23 @@ class EELicenseService {
if (licenseServerKey) {
// license server key is present -> validate it
const token = await refreshLicenseServerKeyToken()
if (token) {
this.instanceType = "cloud";
}
return;
}
if (licenseKey) {
// license key is present -> validate it
const token = await refreshLicenseKeyToken();
if (token) {
const { data: { currentPlan } } = await licenseKeyRequest.get(
`${await getLicenseServerUrl()}/api/license/v1/plan`
);
this.globalFeatureSet = currentPlan;
this.instanceType = "enterprise-self-hosted";
}

@ -1,656 +0,0 @@
import picomatch from "picomatch";
import { Types } from "mongoose";
import {
containsGlobPatterns,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper
} from "../../helpers/secrets";
import { Folder, ISecret, Secret } from "../../models";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy";
import {
CommitType,
ISecretApprovalRequest,
ISecretApprovalSecChange,
ISecretCommits,
SecretApprovalRequest
} from "../models/secretApprovalRequest";
import { BadRequestError } from "../../utils/errors";
import { getFolderByPath } from "../../services/FolderService";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables";
import TelemetryService from "../../services/TelemetryService";
import { EEAuditLogService, EESecretService } from "../services";
import { EventType, SecretVersion } from "../models";
import { AuthData } from "../../interfaces/middleware";
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
const getPolicyScore = (policy: ISecretApprovalPolicy) =>
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
// this will fetch the policy that gets priority for an environment and secret path
export const getSecretPolicyOfBoard = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment });
if (!policies) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
({ secretPath: policyPath }) =>
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
);
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
// if that is tie get by first createdAt
const policiesByPriority = policiesFilteredByPath.sort(
(a, b) => getPolicyScore(b) - getPolicyScore(a)
);
const finalPolicy = policiesByPriority.shift();
return finalPolicy;
};
const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => {
const latestSecretVersions = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
},
type: SECRET_SHARED
}
},
{
$sort: { version: -1 }
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // id of latest secret versionId
secret: { $first: "$$ROOT" }
}
}
]).exec();
// reduced with secret id and latest version as document
return latestSecretVersions.reduce(
(prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }),
{}
);
};
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
secretName: string;
};
type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "version">> & {
secretName: string;
newSecretName?: string;
};
type TGenerateSecretApprovalRequestArg = {
workspaceId: string;
environment: string;
secretPath: string;
policy: ISecretApprovalPolicy;
data: {
[CommitType.CREATE]?: TApprovalCreateSecret[];
[CommitType.UPDATE]?: TApprovalUpdateSecret[];
[CommitType.DELETE]?: { secretName: string }[];
};
commiterMembershipId: string;
authData: AuthData;
};
export const generateSecretApprovalRequest = async ({
workspaceId,
environment,
secretPath,
policy,
data,
commiterMembershipId,
authData
}: TGenerateSecretApprovalRequestArg) => {
// calculate folder id from secret path
let folderId = "root";
const rootFolder = await Folder.findOne({ workspace: workspaceId, environment });
if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" });
if (rootFolder) {
const folder = getFolderByPath(rootFolder.nodes, secretPath);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
// generate secret blindIndexes
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const commits: ISecretApprovalRequest["commits"] = [];
// -----
// for created secret approval change
const createdSecret = data[CommitType.CREATE];
if (createdSecret && createdSecret?.length) {
// validation checks whether secret exists for creation
const secretBlindIndexes = await Promise.all(
createdSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[createdSecret[i].secretName] = curr;
return prev;
}, {})
);
// check created secret exists
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
createdSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.exec();
if (exists) throw BadRequestError({ message: "Secrets already exist" });
commits.push(
...createdSecret.map((el) => ({
op: CommitType.CREATE as const,
newVersion: {
...el,
version: 0,
_id: new Types.ObjectId(),
secretBlindIndex: secretBlindIndexes[el.secretName]
}
}))
);
}
// ----
// updated secrets approval change
const updatedSecret = data[CommitType.UPDATE];
if (updatedSecret && updatedSecret?.length) {
// validation checks whether secret doesn't exists for update
const secretBlindIndexes = await Promise.all(
updatedSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[updatedSecret[i].secretName] = curr;
return prev;
}, {})
);
// check update secret exists
const oldSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select("+secretBlindIndex")
.lean()
.exec();
if (oldSecrets.length !== updatedSecret.length)
throw BadRequestError({ message: "Secrets already exist" });
// finally check updating blindindex exist
const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName));
const newSecretBlindIndexes = await Promise.all(
nameUpdatedSecrets.map(({ newSecretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName: newSecretName as string,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[nameUpdatedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const doesAnySecretExistWithNewIndex = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
});
if (doesAnySecretExistWithNewIndex.length)
throw BadRequestError({ message: "Secret with new name already exist" });
const oldSecretsGroupById = oldSecrets.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...updatedSecret.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.UPDATE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]._id,
newVersion: {
...el,
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
_id: new Types.ObjectId(),
version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1
}
};
})
);
}
// -----
// deleted secrets
const deletedSecrets = data[CommitType.DELETE];
if (deletedSecrets && deletedSecrets.length) {
const secretBlindIndexes = await Promise.all(
deletedSecrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[deletedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const secretsToDelete = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select({ secretBlindIndex: 1, _id: 1 })
.lean()
.exec();
if (secretsToDelete.length !== deletedSecrets.length)
throw BadRequestError({ message: "Deleted secrets not found" });
const oldSecretsGroupById = secretsToDelete.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...deletedSecrets.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.DELETE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]
};
})
);
}
const secretApprovalRequest = new SecretApprovalRequest({
workspace: workspaceId,
environment,
folderId,
policy,
commits,
committer: commiterMembershipId
});
await secretApprovalRequest.save();
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: commiterMembershipId,
secretApprovalRequestId: secretApprovalRequest._id.toString(),
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
return secretApprovalRequest;
};
// validation for a merge conditions happen in another function in controller
export const performSecretApprovalRequestMerge = async (
id: string,
authData: AuthData,
userMembershipId: string
) => {
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ commits: ISecretCommits<ISecret> }>({
path: "commits.secret",
select: "+secretBlindIndex",
populate: {
path: "tags"
}
})
.select("+commits.newVersion.secretBlindIndex");
if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" });
const workspaceId = secretApprovalRequest.workspace;
const environment = secretApprovalRequest.environment;
const folderId = secretApprovalRequest.folderId;
const postHogClient = await TelemetryService.getPostHogClient();
const conflicts: Array<{ secretId: string; op: CommitType }> = [];
const secretCreationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.CREATE
) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>;
if (secretCreationCommits.length) {
// the created secrets already exist thus creation conflict ones
const conflictedSecrets = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
{}
);
const nonConflictSecrets = secretCreationCommits.filter(
({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]
);
secretCreationCommits
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() });
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
algorithm,
keyEncoding,
tags
}
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: algorithm || ALGORITHM_AES_256_GCM,
keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding,
secretBlindIndex
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
tags: secret.tags,
skipMultilineEncoding: secret?.skipMultilineEncoding,
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
}
const secretUpdationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.UPDATE
) as Array<{
op: CommitType.UPDATE;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
secret: ISecret;
}>;
if (secretUpdationCommits.length) {
const conflictedByNewBlindIndex = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretUpdationCommits
.map(({ newVersion }) => newVersion?.secretBlindIndex)
.filter(Boolean)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
(prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev),
{}
);
secretUpdationCommits
.filter(
({ newVersion, secret }) =>
(newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) ||
!secret
)
.forEach((el) => {
conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() });
});
const nonConflictSecrets = secretUpdationCommits.filter(
({ newVersion, secret }) =>
Boolean(secret) &&
(newVersion?.secretBlindIndex
? !conflictGroupByBlindIndex[newVersion.secretBlindIndex]
: true)
);
await Secret.bulkWrite(
// id and version are stripped off
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags
},
secret
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secret.secretBlindIndex,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
})
)
);
await EESecretService.addSecretVersions({
secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => {
return new SecretVersion({
secret: secret._id,
version: secret.version + 1,
workspace: workspaceId,
type: SECRET_SHARED,
folder: folderId,
environment,
isDeleted: false,
secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex,
secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext,
secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext,
secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag,
secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext,
secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV,
secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag,
tags: newVersion?.tags ?? secret.tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding
});
})
});
}
const secretDeletionCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.DELETE
) as Array<{
op: CommitType.DELETE;
secret: ISecret;
}>;
if (secretDeletionCommits.length) {
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({
secretBlindIndex,
type: { $in: ["shared", "personal"] }
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
});
}
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
id,
{
conflicts,
hasMerged: true,
status: "close",
statusChangeBy: userMembershipId
},
{ new: true }
);
if (postHogClient) {
if (postHogClient) {
postHogClient.capture({
event: "secrets merged",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secretApprovalRequest.commits.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_MERGED,
metadata: {
mergedBy: userMembershipId,
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId
}
);
return updatedSecretApproval;
};

@ -1,54 +0,0 @@
import { z } from "zod";
export const GetSecretApprovalRuleList = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const GetSecretApprovalPolicyOfABoard = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
});
export const CreateSecretApprovalRule = z.object({
body: z
.object({
workspaceId: z.string(),
name: z.string().optional(),
environment: z.string(),
secretPath: z.string().optional().nullable(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const UpdateSecretApprovalRule = z.object({
params: z.object({
id: z.string()
}),
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z.string().optional().nullable()
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const DeleteSecretApprovalRule = z.object({
params: z.object({
id: z.string()
})
});

@ -1,49 +0,0 @@
import { z } from "zod";
import { ApprovalStatus } from "../models/secretApprovalRequest";
export const getSecretApprovalRequests = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
status: z.enum(["open", "close"]).optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
})
});
export const getSecretApprovalRequestCount = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const getSecretApprovalRequestDetails = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalReviewStatus = z.object({
body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}),
params: z.object({
id: z.string().trim()
})
});
export const mergeSecretApprovalRequest = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalRequestStatus = z.object({
params: z.object({
id: z.string().trim()
}),
body: z.object({
status: z.enum(["open", "close"])
})
});

@ -7,7 +7,6 @@ import {
ITokenVersion,
IUser,
ServiceTokenData,
ServiceTokenDataV3,
TokenVersion,
User,
} from "../models";
@ -24,14 +23,12 @@ import {
getJwtProviderAuthSecret,
getJwtRefreshLifetime,
getJwtRefreshSecret,
getJwtServiceTokenSecret
} from "../config";
import {
AuthMode
} from "../variables";
import {
ServiceTokenAuthData,
ServiceTokenV3AuthData,
UserAuthData
} from "../interfaces/middleware";
@ -50,9 +47,6 @@ 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"];
@ -71,7 +65,6 @@ 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")
@ -79,21 +72,15 @@ export const validateAuthMode = ({
if (tokenValue === null)
throw BadRequestError({ message: "Missing Authorization Body in the request header." });
const parts = tokenValue.split(".");
switch (parts[0]) {
switch (tokenValue.split(".", 1)[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." });
@ -224,73 +211,8 @@ export const getAuthSTDPayload = async ({
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
/**
* 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"])
}
// return serviceTokenDataToReturn;
}
/**
@ -460,15 +382,11 @@ export const createToken = ({
secret,
}: {
payload: any;
expiresIn?: string | number;
expiresIn: string | number;
secret: string;
}) => {
return jwt.sign(payload, secret, {
...(
(expiresIn !== undefined && expiresIn !== null)
? { expiresIn }
: {}
)
expiresIn,
});
};

@ -13,13 +13,11 @@ import {
Folder,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
Secret,
SecretBlindIndexData,
ServiceTokenData,
TFolderRootSchema
} from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import {
BadRequestError,
@ -55,50 +53,10 @@ 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(

@ -25,11 +25,8 @@ import {
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretApprovalRequest as v1SecretApprovalRequest,
secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import {
auth as v1AuthRouter,
bot as v1BotRouter,
@ -41,7 +38,7 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
sso as v1SSORouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretImps as v1SecretImpsRouter,
secret as v1SecretRouter,
secretsFolder as v1SecretsFolder,
@ -58,6 +55,7 @@ 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,
@ -156,7 +154,6 @@ 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);
@ -181,8 +178,6 @@ const main = async () => {
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
app.use("/api/v1/sso", v1SSORouter);
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
@ -195,7 +190,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);
@ -228,23 +223,24 @@ const main = async () => {
// await createTestUserForDevelopment();
setUpHealthEndpoint(server);
const serverCleanup = async () => {
await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.close();
process.exit(0);
};
}
process.on("SIGINT", function () {
server.close(async () => {
await serverCleanup();
await serverCleanup()
});
});
process.on("SIGTERM", function () {
server.close(async () => {
await serverCleanup();
await serverCleanup()
});
});

@ -87,15 +87,13 @@ const syncSecrets = async ({
integrationAuth,
secrets,
accessId,
accessToken,
appendices
accessToken
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
switch (integration.integration) {
case INTEGRATION_GCP_SECRET_MANAGER:
@ -155,8 +153,7 @@ const syncSecrets = async ({
await syncSecretsGitHub({
integration,
secrets,
accessToken,
appendices
accessToken
});
break;
case INTEGRATION_GITLAB:
@ -221,8 +218,7 @@ const syncSecrets = async ({
await syncSecretsCheckly({
integration,
secrets,
accessToken,
appendices
accessToken
});
break;
case INTEGRATION_QOVERY:
@ -1346,13 +1342,11 @@ const syncSecretsNetlify = async ({
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken,
appendices
accessToken
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
interface GitHubRepoKey {
key_id: string;
@ -1382,7 +1376,7 @@ const syncSecretsGitHub = async ({
).data;
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
let encryptedSecrets: GitHubSecretRes = (
const encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner,
repo: integration.app
@ -1395,15 +1389,6 @@ const syncSecretsGitHub = async ({
{}
);
encryptedSecrets = Object.keys(encryptedSecrets).reduce((result: {
[key: string]: GitHubSecret;
}, key) => {
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
result[key] = encryptedSecrets[key];
}
return result;
}, {});
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
@ -2089,15 +2074,13 @@ const syncSecretsSupabase = async ({
const syncSecretsCheckly = async ({
integration,
secrets,
accessToken,
appendices
accessToken
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
appendices?: { prefix: string, suffix: string };
}) => {
let getSecretsRes = (
const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
headers: {
Authorization: `Bearer ${accessToken}`,
@ -2113,15 +2096,6 @@ const syncSecretsCheckly = async ({
{}
);
getSecretsRes = Object.keys(getSecretsRes).reduce((result: {
[key: string]: string;
}, key) => {
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
result[key] = getSecretsRes[key];
}
return result;
}, {});
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {

@ -1,12 +1,10 @@
import { Types } from "mongoose";
import {
IServiceTokenData,
IServiceTokenDataV3,
IUser,
} from "../../models";
import {
ServiceActor,
ServiceActorV3,
UserActor,
UserAgentType
} from "../../ee/models";
@ -23,11 +21,6 @@ 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;
@ -35,5 +28,4 @@ export interface ServiceTokenAuthData extends BaseAuthData {
export type AuthData =
| UserAuthData
| ServiceTokenV3AuthData
| ServiceTokenAuthData;

@ -3,7 +3,6 @@ import { NextFunction, Request, Response } from "express";
import {
getAuthAPIKeyPayload,
getAuthSTDPayload,
getAuthSTDV3Payload,
getAuthUserPayload,
validateAuthMode,
} from "../helpers/auth";
@ -50,12 +49,6 @@ 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,
@ -68,7 +61,9 @@ const requireAuth = ({
req,
authTokenValue
});
// authPayload = authUserPayload.user;
req.user = authData.authPayload;
// req.tokenVersionId = authUserPayload.tokenVersionId; // TODO
break;
}

@ -14,19 +14,17 @@ export * from "./tag";
export * from "./folder";
export * from "./secretImports";
export * from "./secretBlindIndexData";
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 "./serviceToken";
export * from "./serviceAccount";
export * from "./serviceAccountKey";
export * from "./serviceAccountOrganizationPermission";
export * from "./serviceAccountWorkspacePermission";
export * from "./tokenData";
export * from "./user";
export * from "./userAction";
export * from "./workspace";
export * from "./serviceTokenData"; // TODO: deprecate
export * from "./serviceTokenData";
export * from "./apiKeyData";
export * from "./loginSRPDetail";
export * from "./tokenVersion";
export * from "./webhooks";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";
export * from "./webhooks";

@ -3,7 +3,6 @@ import { Schema, Types, model } from "mongoose";
export interface ISecretApprovalPolicy {
_id: Types.ObjectId;
workspace: Types.ObjectId;
name: string;
environment: string;
secretPath?: string;
approvers: Types.ObjectId[];
@ -24,9 +23,6 @@ const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
ref: "Membership"
}
],
name: {
type: String
},
environment: {
type: String,
required: true

@ -0,0 +1,68 @@
import { Schema, Types, model } from "mongoose";
import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion";
enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
enum CommitType {
DELETE = "delete",
UPDATE = "update",
CREATE = "create"
}
export interface ISecretApprovalRequest {
_id: Types.ObjectId;
committer: Types.ObjectId;
approvers: {
member: Types.ObjectId;
status: ApprovalStatus;
}[];
approvals: number;
hasMerged: boolean;
status: ApprovalStatus;
commits: {
secretVersion: Types.ObjectId;
newVersion: ISecretVersion;
op: CommitType;
}[];
}
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
approvers: [
{
member: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
},
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}
],
approvals: {
type: Number,
required: true
},
hasMerged: { type: Boolean, default: false },
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING },
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
commits: [
{
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
newVersion: SecretVersion,
op: { type: String, enum: [CommitType], required: true }
}
]
},
{
timestamps: true
}
);
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
"SecretApprovalRequest",
secretApprovalRequestSchema
);

@ -1,4 +1,3 @@
// TODO: deprecate
import { Schema, Types, model } from "mongoose";
export interface IServiceToken {
_id: Types.ObjectId;

@ -1,4 +1,3 @@
// TODO: deprecate
import { Document, Schema, Types, model } from "mongoose";
export interface IServiceTokenData extends Document {

@ -1,129 +0,0 @@
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);

@ -1,43 +0,0 @@
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);

@ -4,7 +4,6 @@ export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",

@ -60,8 +60,7 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
appendices: { prefix: integration.metadata?.secretPrefix || "", suffix: integration.metadata?.secretSuffix || "" }
accessToken: access.accessToken
});
}
})

@ -11,13 +11,13 @@ import key from "./key";
import inviteOrg from "./inviteOrg";
import secret from "./secret";
import serviceToken from "./serviceToken";
import sso from "./sso";
import password from "./password";
import integration from "./integration";
import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import webhooks from "./webhook";
import secretImps from "./secretImps";
import secretApprovalPolicy from "./secretApprovalPolicy";
export {
signup,
@ -39,5 +39,5 @@ export {
secretsFolder,
webhooks,
secretImps,
sso
secretApprovalPolicy
};

@ -1,8 +1,8 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { requireAuth } from "../../middleware";
import { secretApprovalPolicyController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
import { AuthMode } from "../../variables";
router.get(
"/",
@ -12,14 +12,6 @@ router.get(
secretApprovalPolicyController.getSecretApprovalPolicy
);
router.get(
"/board",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.getSecretApprovalPolicyOfBoard
);
router.post(
"/",
requireAuth({

@ -1,72 +0,0 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { authLimiter } from "../../helpers/rateLimiter";
import { ssoController } from "../../ee/controllers/v1";
router.get("/redirect/google", authLimiter, (req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/gitlab",
authLimiter,
(req, res, next) => {
passport.authenticate("gitlab", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/gitlab",
authLimiter,
passport.authenticate("gitlab", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
export default router;

@ -29,11 +29,11 @@ router.patch(
);
router.put(
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}),
usersController.updateAuthMethods,
"/me/auth-methods",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.updateAuthMethods
);
router.get(

@ -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, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
secretsController.getSecretsRaw
);
@ -15,7 +15,7 @@ router.get(
router.get(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -29,7 +29,7 @@ router.get(
router.post(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -43,7 +43,7 @@ router.post(
router.patch(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -57,7 +57,7 @@ router.patch(
router.delete(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -71,7 +71,7 @@ router.delete(
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -116,7 +116,7 @@ router.delete(
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -127,7 +127,7 @@ router.post(
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@ -138,7 +138,7 @@ router.get(
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@ -149,7 +149,7 @@ router.patch(
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"

@ -34,12 +34,4 @@ router.post(
// --
router.get(
"/:workspaceId/service-token",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
workspacesController.getWorkspaceServiceTokenData
);
export default router;

@ -8,25 +8,21 @@ import {
Organization,
ServiceAccount,
ServiceTokenData,
ServiceTokenDataV3,
User
} from "../models";
import { createToken } from "../helpers/auth";
import {
getClientIdGitHubLogin,
getClientIdGitLabLogin,
getClientIdGoogleLogin,
getClientSecretGitHubLogin,
getClientSecretGitLabLogin,
getClientSecretGoogleLogin,
getJwtProviderAuthLifetime,
getJwtProviderAuthSecret,
getSiteURL,
getUrlGitLabLogin
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -34,8 +30,6 @@ const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const GitLabStrategy = require("passport-gitlab2").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/**
@ -55,10 +49,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
@ -67,6 +57,7 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
* @returns
*/
const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
@ -76,11 +67,7 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof ServiceTokenDataV3) {
return { user: authData.authPayload.user };
return { user: authData.authPayload.user };0
}
}
@ -89,9 +76,6 @@ const initializePassport = async () => {
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
const urlGitLab = await getUrlGitLabLogin();
const clientIdGitLabLogin = await getClientIdGitLabLogin();
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({
@ -225,60 +209,6 @@ const initializePassport = async () => {
}
));
}
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
passport.use(new GitLabStrategy({
passReqToCallback: true,
clientID: clientIdGitLabLogin,
clientSecret: clientSecretGitLabLogin,
callbackURL: "/api/v1/sso/gitlab",
baseURL: urlGitLab
},
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
const email = profile.emails[0].value;
let user = await User.findOne({
email
}).select("+publicKey");
if (!user) {
user = await new User({
email: email,
authMethods: [AuthMethod.GITLAB],
firstName: profile.displayName,
lastName: ""
}).save();
}
let isLinkingRequired = false;
if (!user.authMethods.includes(AuthMethod.GITLAB)) {
isLinkingRequired = true;
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod: AuthMethod.GITLAB,
isUserCompleted,
isLinkingRequired,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
return done(null, profile);
}
));
}
passport.use("saml", new MultiSamlStrategy(
{

@ -1,6 +1,6 @@
import net from "net";
import { IPType } from "../../ee/models";
import { InternalServerError, UnauthorizedRequestError } from "../errors";
import { InternalServerError } from "../errors";
/**
* Return details of IP [ip]:
@ -98,39 +98,4 @@ 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"
});
}
}

@ -1,3 +1,4 @@
export * from "./secretApproval";
export * from "./user";
export * from "./workspace";
export * from "./bot";
@ -9,4 +10,3 @@ export * from "./organization";
export * from "./secrets";
export * from "./serviceAccount";
export * from "./serviceTokenData";
export * from "./serviceTokenDataV3";

@ -58,10 +58,6 @@ 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,10 +58,6 @@ 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,10 +46,6 @@ 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"
});
}
};

@ -0,0 +1,34 @@
import { z } from "zod";
export const GetSecretApprovalRuleList = z.object({
query: z.object({
workspaceId: z.string()
})
});
export const CreateSecretApprovalRule = z.object({
body: z.object({
workspaceId: z.string(),
environment: z.string(),
secretPath: z.string().optional().nullable(),
approvers: z.string().array().optional(),
approvals: z.number().min(1).default(1)
})
});
export const UpdateSecretApprovalRule = z.object({
params: z.object({
id: z.string()
}),
body: z.object({
approvers: z.string().array().optional(),
approvals: z.number().min(1).optional(),
secretPath: z.string().optional().nullable()
})
});
export const DeleteSecretApprovalRule = z.object({
params: z.object({
id: z.string()
})
});

@ -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().optional().default(""),
secretComment: z.string().trim(),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
}),

@ -1,119 +0,0 @@
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,7 +7,6 @@ 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
@ -58,11 +57,8 @@ export const validateClientForWorkspace = async ({
environment,
requiredPermissions
});
return { membership, workspace};
case ActorType.SERVICE_V3:
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization"
});
return {};
}
};
@ -304,9 +300,3 @@ export const NameWorkspaceSecretsV3 = z.object({
.array()
})
});
export const GetWorkspaceServiceTokenDataV3 = z.object({
params: z.object({
workspaceId: z.string().trim()
})
});

@ -1,7 +1,6 @@
export enum AuthMode {
JWT = "jwt",
SERVICE_TOKEN = "serviceToken",
SERVICE_TOKEN_V3 = "serviceTokenV3",
API_KEY = "apiKey"
}

@ -84,8 +84,7 @@ export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth
// integration apps endpoints
export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com";
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
export const GITLAB_URL = "https://gitlab.com";
export const INTEGRATION_GITLAB_API_URL = `${GITLAB_URL}/api`;
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
export const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";

@ -518,7 +518,7 @@ func browserCliLogin() (models.UserCredentials, error) {
SERVER_TIMEOUT := 60 * 10
//create listener
listener, err := net.Listen("tcp", "127.0.0.1:0")
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
return models.UserCredentials{}, err
}

@ -1,37 +0,0 @@
---
title: "GitLab SSO"
description: "Configure GitLab SSO for Infisical"
---
Using GitLab SSO on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it.
## Create an OAuth application in GitLab
Navigate to your user Settings > Applications to create a new GitLab application.
![sso gitlab config](/images/sso/gitlab/edit-profile.png)
![sso gitlab config](/images/sso/gitlab/new-app.png)
Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/api/v1/sso/gitlab`.
Note that only `read_user` is required as part of the **Scopes** configuration.
![sso gitlab config](/images/sso/gitlab/new-app-form.png)
<Note>
If you have a GitLab group, you can create an OAuth application under it
in your group Settings > Applications.
</Note>
## Add your OAuth application credentials to Infisical
Obtain the **Application ID** and **Secret** for your GitLab application.
![sso gitlab config](/images/sso/gitlab/credentials.png)
Back in your Infisical instance, add 2-3 new environment variables for the credentials of your GitLab application:
- `CLIENT_ID_GITLAB_LOGIN`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB_LOGIN`: The **Secret** of your GitLab application.
- (optional) `URL_GITLAB_LOGIN`: The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to `https://gitlab.com`.
Once added, restart your Infisical instance and log in with GitLab.

@ -19,7 +19,6 @@ your IdP cannot and will not have access to the decryption key needed to decrypt
- [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)

@ -3,63 +3,30 @@ 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 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
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.
You can manage service tokens in Project Settings > 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.
### 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 create a service token, head to Project Settings > Service Tokens as shown below and press **Create 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.
![token add](../../images/project-token-old-add.png)
![token add](../../images/project-token-add.png)
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:
### Service token permissions
![token add](../../images/service-token-permissions.png)
- 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.
![token add](../../images/project-token-old-permissions.png)
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.
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.
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.
**FAQ**
**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?">
<Accordion title="Examples of common Glob pattens">
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/`.
@ -68,4 +35,3 @@ of the `/common` path within the development environment of the project; the tok
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>

@ -1,105 +0,0 @@
---
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**.
![token add](../../images/project-token-add.png)
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 youre using Infisical Cloud, then it is available under the Pro Tier. If youre self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it.
</Warning>
![token add](../../images/project-token-permissions.png)
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>

Binary file not shown.

Before

(image error) Size: 1.2 MiB

After

(image error) Size: 272 KiB

Binary file not shown.

After

(image error) Size: 336 KiB

Binary file not shown.

After

(image error) Size: 370 KiB

Binary file not shown.

Before

(image error) Size: 1.4 MiB

Binary file not shown.

Before

(image error) Size: 1.5 MiB

Binary file not shown.

Before

(image error) Size: 1.4 MiB

Binary file not shown.

After

(image error) Size: 652 KiB

Binary file not shown.

Before

(image error) Size: 365 KiB

Binary file not shown.

Before

(image error) Size: 1.1 MiB

Binary file not shown.

Before

(image error) Size: 1.5 MiB

Binary file not shown.

Before

(image error) Size: 959 KiB

@ -107,7 +107,7 @@ build-job:
Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application:
- `CLIENT_ID_GITLAB`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB`: The **Secret** of your GitLab application.
- `CLIENT_SECRET_GITLAB`: The **Client Secret** of your GitLab application.
Once added, restart your Infisical instance and use the GitLab integration.

@ -1,72 +0,0 @@
---
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.

@ -126,7 +126,6 @@
"documentation/platform/sso/overview",
"documentation/platform/sso/google",
"documentation/platform/sso/github",
"documentation/platform/sso/gitlab",
"documentation/platform/sso/okta",
"documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud"

@ -155,15 +155,6 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitHub login
</ParamField>
<ParamField query="CLIENT_ID_GITLAB_LOGIN" type="string" default="none" optional>
OAuth2 client ID for GitLab login
</ParamField>
<ParamField query="CLIENT_SECRET_GITLAB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitLab login
</ParamField>
<ParamField query="URL_GITLAB_LOGIN" type="string" default="https://gitlab.com" optional>
URL of your self-hosted instance of GitLab where the OAuth application is registered
</ParamField>
</Tab>
<Tab title="Others">
#### JWT

@ -15,7 +15,6 @@ You can view specific documentation for how to set up each SSO authentication me
- [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)

@ -1,7 +1,6 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
FROM node:16-alpine AS deps
# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@ -14,6 +13,7 @@ COPY package.json package-lock.json next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
@ -52,8 +52,6 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG NEXT_INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION
COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts
COPY --from=builder /app/public ./public

@ -11,7 +11,7 @@ const ContentSecurityPolicy = `
style-src 'self' https://rsms.me 'unsafe-inline';
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:*;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://localhost:*;
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
media-src https://js.intercomcdn.com;
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;

@ -1,5 +1,5 @@
{
"name": "npm-proj-1695919945735-0.225773463026700768rr1Oh",
"name": "npm-proj-1696437151802-0.34068817643390914JgpU1g",
"lockfileVersion": 2,
"requires": true,
"packages": {
@ -93,7 +93,7 @@
"uuidv4": "^6.2.13",
"yaml": "^2.2.2",
"yup": "^0.32.11",
"zod": "^3.22.0",
"zod": "^3.22.3",
"zustand": "^4.4.1"
},
"devDependencies": {
@ -23692,9 +23692,9 @@
}
},
"node_modules/zod": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==",
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@ -40905,9 +40905,9 @@
}
},
"zod": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q=="
"version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="
},
"zustand": {
"version": "4.4.1",

@ -101,7 +101,7 @@
"uuidv4": "^6.2.13",
"yaml": "^2.2.2",
"yup": "^0.32.11",
"zod": "^3.22.0",
"zod": "^3.22.3",
"zustand": "^4.4.1"
},
"devDependencies": {

Binary file not shown.

Before

(image error) Size: 286 KiB

File diff suppressed because one or more lines are too long

@ -1,6 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { faAngleRight, faLock } from "@fortawesome/free-solid-svg-icons";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useOrganization, useWorkspace } from "@app/context";
@ -16,8 +16,6 @@ type Props = {
onEnvChange?: (slug: string) => void;
secretPath?: string;
isFolderMode?: boolean;
isProtectedBranch?: boolean;
protectionPolicyName?: string;
};
// TODO: make links clickable and clean up
@ -44,9 +42,7 @@ export default function NavHeader({
userAvailableEnvs = [],
onEnvChange,
isFolderMode,
secretPath = "/",
isProtectedBranch = false,
protectionPolicyName
secretPath = "/"
}: Props): JSX.Element {
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
@ -155,11 +151,6 @@ export default function NavHeader({
</div>
);
})}
{isProtectedBranch && (
<Tooltip content={`Protected by policy ${protectionPolicyName}`}>
<FontAwesomeIcon icon={faLock} className="text-primary ml-2" />
</Tooltip>
)}
</div>
);
}

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -9,94 +9,74 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "../v2";
export default function InitialSignupStep({
setIsSignupWithEmail
setIsSignupWithEmail,
}: {
setIsSignupWithEmail: (value: boolean) => void;
setIsSignupWithEmail: (value: boolean) => void
}) {
const { t } = useTranslation();
const router = useRouter();
const { t } = useTranslation();
const router = useRouter();
return (
<div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
{t("signup.initial-title")}
</h1>
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-12 w-full"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitHub
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/gitlab");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitLab
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with Email
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with SSO
</Button>
</div>
<div className="mt-6 w-1/4 min-w-[20rem] px-8 text-center text-xs text-bunker-400 lg:w-1/6">
{t("signup.create-policy")}
</div>
<div className="mt-2 flex flex-row text-xs text-bunker-400">
<Link href="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{t("signup.already-have-account")}
</span>
</Link>
</div>
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-12 w-full mx-0"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with GitHub
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with Email
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-12 w-full mx-0"
>
Continue with SSO
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>
{t("signup.create-policy")}
</div>
<div className="mt-2 text-bunker-400 text-xs flex flex-row">
<Link href="/login">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("signup.already-have-account")}</span>
</Link>
</div>
</div>
);
}

@ -11,9 +11,6 @@ type Props = {
isLoading?: boolean;
};
// refactor(akhilmhdh): both color and size variants are together need to split it
// colorSchema should handle all color class names
// variant should handle how the button padding and other types should be set
const buttonVariants = cva(
[
"button",
@ -70,7 +67,7 @@ const buttonVariants = cva(
{
colorSchema: "primary",
variant: "solid",
className: "text-black bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black"
className: "bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black"
},
{
colorSchema: "primary",
@ -109,12 +106,6 @@ const buttonVariants = cva(
variant: "outline",
className: "text-red hover:bg-red hover:text-black"
},
{
colorSchema: "danger",
variant: "outline_bg",
className:
"bg-mineshaft-600 border border-red-500 hover:bg-red/[0.1] hover:border-red/40 text-red-500"
},
{
colorSchema: "primary",
variant: "plain",

@ -23,8 +23,10 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
}, []);
return (
<div className="container mx-auto flex relative flex-col h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8">
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
<div className="container mx-auto flex relative flex-col h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8">
<div>
<img src="/images/loading/loading.gif" height={210} width={240} alt="loading animation" />
</div>
{text && isTextArray && (
<AnimatePresence exitBeforeEnter>
<motion.div
@ -38,7 +40,7 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
</motion.div>
</AnimatePresence>
)}
{text && !isTextArray && <div className="text-primary text-xs">{text}</div>}
{text && !isTextArray && <div className="text-primary text-sm">{text}</div>}
</div>
);
};

@ -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-xl -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-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
className
)}
>

@ -22,7 +22,7 @@ const sanitizeConf = {
const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
if (!content) return "missing";
if (!isVisible) return replaceContentWithDot(content);
const sanitizedContent = sanitizeHtml(

@ -20,8 +20,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval"
SecretRollback = "secret-rollback"
}
type SubjectFields = {
@ -44,7 +43,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]

@ -16,9 +16,6 @@ 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",

Some files were not shown because too many files have changed in this diff Show More