Adjust ST V3

This commit is contained in:
Tuan Dang
2023-10-05 14:53:59 +01:00
40 changed files with 905 additions and 943 deletions
@@ -93,6 +93,7 @@ jobs:
tags: infisical/frontend:test tags: infisical/frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/frontend:test
@@ -116,3 +117,4 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
+2
View File
@@ -83,6 +83,7 @@ jobs:
tags: infisical/staging_deployment_frontend:test tags: infisical/staging_deployment_frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
@@ -105,6 +106,7 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
gamma-deployment: gamma-deployment:
name: Deploy to gamma name: Deploy to gamma
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3 -4
View File
@@ -1,9 +1,8 @@
<h1 align="center"> <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"> <img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1> </h1>
<p align="center"> <p align="center">
<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 align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
</p> </p>
<h4 align="center"> <h4 align="center">
@@ -44,11 +43,11 @@
</a> </a>
</h4> </h4>
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" /> <img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
## Introduction ## Introduction
**[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. **[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.
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. 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.
+27 -7
View File
@@ -50,6 +50,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
@@ -63,7 +64,7 @@
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6", "winston-loki": "^6.0.6",
"zod": "^3.21.4" "zod": "^3.22.3"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
@@ -13727,6 +13728,17 @@
"node": ">= 0.4.0" "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": { "node_modules/passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -16684,9 +16696,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.21.4", "version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -27163,6 +27175,14 @@
"passport-oauth2": "1.x.x" "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": { "passport-google-oauth20": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -29384,9 +29404,9 @@
"dev": true "dev": true
}, },
"zod": { "zod": {
"version": "3.21.4", "version": "3.22.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="
} }
} }
} }
+2 -1
View File
@@ -41,6 +41,7 @@
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
@@ -54,7 +55,7 @@
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.6", "winston-loki": "^6.0.6",
"zod": "^3.21.4" "zod": "^3.22.3"
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",
+5
View File
@@ -1,3 +1,5 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node"; import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({ export const client = new InfisicalClient({
@@ -53,6 +55,9 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue; 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 getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_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 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"; export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
@@ -34,10 +34,10 @@ import { Webhook } from "../../models";
* @returns * @returns
*/ */
export const createWorkspaceEnvironment = async (req: Request, res: Response) => { export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Create environment' #swagger.summary = 'Create environment'
#swagger.description = 'Create environment' #swagger.description = 'Create environment'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": []
}] }]
@@ -46,12 +46,12 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project", "description": "ID of project",
"required": true, "required": true,
"type": "string" "type": "string"
} }
/* /*
#swagger.summary = 'Create environment' #swagger.summary = 'Create environment'
#swagger.description = 'Create environment' #swagger.description = 'Create environment'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": []
}] }]
@@ -60,7 +60,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
"description": "ID of project", "description": "ID of project",
"required": true, "required": true,
"type": "string" "type": "string"
} }
#swagger.requestBody = { #swagger.requestBody = {
content: { content: {
@@ -88,7 +88,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = { #swagger.responses[200] = {
content: { content: {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"message": { "message": {
@@ -115,7 +115,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
}, },
"description": "Response after creating a new environment" "description": "Response after creating a new environment"
} }
} }
} }
} }
*/ */
@@ -246,7 +246,7 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
* @returns * @returns
*/ */
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => { export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Rename workspace environment' #swagger.summary = 'Rename workspace environment'
#swagger.description = 'Rename a specific environment within a workspace' #swagger.description = 'Rename a specific environment within a workspace'
@@ -317,7 +317,7 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
} }
} }
} }
} }
*/ */
const { const {
params: { workspaceId }, params: { workspaceId },
@@ -394,6 +394,11 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await ServiceAccountWorkspacePermission.updateMany( await ServiceAccountWorkspacePermission.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
@@ -447,10 +452,10 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => { export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete workspace environment' #swagger.summary = 'Delete workspace environment'
#swagger.description = 'Delete a specific environment from a workspace' #swagger.description = 'Delete a specific environment from a workspace'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": []
}] }]
@@ -483,7 +488,7 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
#swagger.responses[200] = { #swagger.responses[200] = {
content: { content: {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"message": { "message": {
@@ -501,9 +506,9 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
}, },
"description": "Response after deleting an environment from a workspace" "description": "Response after deleting an environment from a workspace"
} }
} }
} }
} }
*/ */
const { const {
params: { workspaceId }, params: { workspaceId },
@@ -590,10 +595,10 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
// TODO(akhilmhdh) after rbac this can be completely removed // TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => { export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Get all accessible environments of a workspace' #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.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{ #swagger.security = [{
"apiKeyAuth": [] "apiKeyAuth": []
}] }]
@@ -640,7 +645,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res:
} }
} }
} }
} }
*/ */
const { const {
params: { workspaceId } params: { workspaceId }
+183 -443
View File
@@ -34,6 +34,93 @@ import {
} from "../../validation"; } from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
import { UnauthorizedRequestError } from "../../utils/errors";
import { AuthData } from "../../interfaces/middleware";
const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<(env: string, secPath: string) => boolean> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return () => true;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
});
}
default: {
throw UnauthorizedRequestError();
}
}
}
/** /**
* Return secrets for workspace with id [workspaceId] and environment * Return secrets for workspace with id [workspaceId] and environment
@@ -73,54 +160,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
if (!environment || !workspaceId) if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" }); throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret const permissionCheckFn = await checkSecretsPermission({
switch (req.authData.actor.type) { authData: req.authData,
case ActorType.USER: { workspaceId,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); environment,
ForbiddenError.from(permission).throwUnlessCan( secretPath,
ProjectPermissionActions.Read, secretAction: ProjectPermissionActions.Read
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) });
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = () => true;
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.READ]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: [Permission.READ]
});
break;
}
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -186,38 +232,14 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
query: { secretPath, environment, workspaceId, type, include_imports }, query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req); } = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Read, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Read
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.READ]
});
break;
}
}
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -260,37 +282,13 @@ export const createSecretRaw = async (req: Request, res: Response) => {
} }
} = await validateRequest(reqValidator.CreateSecretRawV3, req); } = await validateRequest(reqValidator.CreateSecretRawV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Create, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Create
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -360,37 +358,13 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding } body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Edit, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Edit
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -441,37 +415,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId } body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Delete, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Delete
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -525,55 +475,13 @@ export const getSecrets = async (req: Request, res: Response) => {
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
} }
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret const permissionCheckFn = await checkSecretsPermission({
authData: req.authData,
switch (req.authData.actor.type) { workspaceId,
case ActorType.USER: { environment,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); secretPath,
ForbiddenError.from(permission).throwUnlessCan( secretAction: ProjectPermissionActions.Read
ProjectPermissionActions.Read, });
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) => true;
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.READ]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: [Permission.READ]
});
break;
}
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -622,37 +530,13 @@ export const getSecretByName = async (req: Request, res: Response) => {
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req); } = await validateRequest(reqValidator.GetSecretByNameV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Read, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Read
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.READ]
});
break;
}
}
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -695,38 +579,14 @@ export const createSecret = async (req: Request, res: Response) => {
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req); } = await validateRequest(reqValidator.CreateSecretV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Create, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Create
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
secretName, secretName,
@@ -796,38 +656,14 @@ export const updateSecretByName = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Missing encrypted key" }); throw BadRequestError({ message: "Missing encrypted key" });
} }
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Edit, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Edit
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -873,37 +709,13 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Delete, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Delete
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -931,38 +743,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
const { const {
body: { secrets, secretPath, environment, workspaceId } body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req); } = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Create, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Create
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const createdSecrets = await SecretService.createSecretBatch({ const createdSecrets = await SecretService.createSecretBatch({
secretPath, secretPath,
@@ -982,37 +770,13 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId } body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Edit, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Edit
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const updatedSecrets = await SecretService.updateSecretBatch({ const updatedSecrets = await SecretService.updateSecretBatch({
secretPath, secretPath,
@@ -1032,37 +796,13 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId } body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
case ActorType.USER: { authData: req.authData,
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); workspaceId,
ForbiddenError.from(permission).throwUnlessCan( environment,
ProjectPermissionActions.Delete, secretPath,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) secretAction: ProjectPermissionActions.Delete
); });
break;
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
break;
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [Permission.WRITE]
});
break;
}
}
const deletedSecrets = await SecretService.deleteSecretBatch({ const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath, secretPath,
@@ -1075,4 +815,4 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secrets: deletedSecrets secrets: deletedSecrets
}); });
}; };
@@ -112,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
user = req.authData.authPayload._id; user = req.authData.authPayload._id;
} }
const isActive = false; const isActive = true;
const serviceTokenData = await new ServiceTokenDataV3({ const serviceTokenData = await new ServiceTokenDataV3({
name, name,
user, user,
@@ -160,7 +160,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData,
serviceToken: `proj_token.${token}` serviceToken: `stv3.${token}`
}); });
} }
+10 -50
View File
@@ -6,60 +6,20 @@ import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter"; import { authLimiter } from "../../../helpers/rateLimiter";
import { AuthMode } from "../../../variables"; 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( router.get(
"/google", "/redirect/saml2/:ssoIdentifier",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
scope: [ 'user:email' ],
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter, authLimiter,
passport.authenticate("github", { (req, res, next) => {
failureRedirect: "/login/provider/error", const options = {
session: false, failureRedirect: "/",
scope: [ 'user:email' ] additionalParams: {
}), RelayState: req.query.callback_port ?? ""
ssoController.redirectSSO },
};
passport.authenticate("saml", options)(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);
});
router.post( router.post(
"/saml2/:ssoIdentifier", "/saml2/:ssoIdentifier",
passport.authenticate("saml", { passport.authenticate("saml", {
+1 -1
View File
@@ -86,7 +86,7 @@ export const validateAuthMode = ({
authMode = AuthMode.SERVICE_TOKEN; authMode = AuthMode.SERVICE_TOKEN;
authTokenValue = tokenValue; authTokenValue = tokenValue;
break; break;
case "proj_token": case "stv3":
authMode = AuthMode.SERVICE_TOKEN_V3; authMode = AuthMode.SERVICE_TOKEN_V3;
authTokenValue = parts.slice(1).join("."); authTokenValue = parts.slice(1).join(".");
break; break;
+2
View File
@@ -41,6 +41,7 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
sso as v1SSORouter,
secretApprovalPolicy as v1SecretApprovalPolicy, secretApprovalPolicy as v1SecretApprovalPolicy,
secretImps as v1SecretImpsRouter, secretImps as v1SecretImpsRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
@@ -181,6 +182,7 @@ const main = async () => {
app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
app.use("/api/v1/sso", v1SSORouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+1
View File
@@ -4,6 +4,7 @@ export enum AuthMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",
+2
View File
@@ -11,6 +11,7 @@ import key from "./key";
import inviteOrg from "./inviteOrg"; import inviteOrg from "./inviteOrg";
import secret from "./secret"; import secret from "./secret";
import serviceToken from "./serviceToken"; import serviceToken from "./serviceToken";
import sso from "./sso";
import password from "./password"; import password from "./password";
import integration from "./integration"; import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
@@ -39,5 +40,6 @@ export {
secretsFolder, secretsFolder,
webhooks, webhooks,
secretImps, secretImps,
sso,
secretApprovalPolicy secretApprovalPolicy
}; };
+72
View File
@@ -0,0 +1,72 @@
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;
+5 -5
View File
@@ -29,11 +29,11 @@ router.patch(
); );
router.put( router.put(
"/me/auth-methods", "/me/auth-methods",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
}), }),
usersController.updateAuthMethods usersController.updateAuthMethods,
); );
router.get( router.get(
+63 -1
View File
@@ -14,16 +14,19 @@ import {
import { createToken } from "../helpers/auth"; import { createToken } from "../helpers/auth";
import { import {
getClientIdGitHubLogin, getClientIdGitHubLogin,
getClientIdGitLabLogin,
getClientIdGoogleLogin, getClientIdGoogleLogin,
getClientSecretGitHubLogin, getClientSecretGitHubLogin,
getClientSecretGitLabLogin,
getClientSecretGoogleLogin, getClientSecretGoogleLogin,
getJwtProviderAuthLifetime, getJwtProviderAuthLifetime,
getJwtProviderAuthSecret, getJwtProviderAuthSecret,
getSiteURL,
getUrlGitLabLogin
} from "../config"; } from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations"; import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors"; import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables"; import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -31,6 +34,8 @@ const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const GitHubStrategy = require("passport-github").Strategy; const GitHubStrategy = require("passport-github").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires // 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"); const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/** /**
@@ -84,6 +89,9 @@ const initializePassport = async () => {
const clientSecretGoogleLogin = await getClientSecretGoogleLogin(); const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
const clientIdGitHubLogin = await getClientIdGitHubLogin(); const clientIdGitHubLogin = await getClientIdGitHubLogin();
const clientSecretGitHubLogin = await getClientSecretGitHubLogin(); const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
const urlGitLab = await getUrlGitLabLogin();
const clientIdGitLabLogin = await getClientIdGitLabLogin();
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
if (clientIdGoogleLogin && clientSecretGoogleLogin) { if (clientIdGoogleLogin && clientSecretGoogleLogin) {
passport.use(new GoogleStrategy({ passport.use(new GoogleStrategy({
@@ -217,6 +225,60 @@ 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( passport.use("saml", new MultiSamlStrategy(
{ {
+2 -1
View File
@@ -84,7 +84,8 @@ export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth
// integration apps endpoints // integration apps endpoints
export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com"; export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com";
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; export const GITLAB_URL = "https://gitlab.com";
export const INTEGRATION_GITLAB_API_URL = `${GITLAB_URL}/api`;
export const INTEGRATION_GITHUB_API_URL = "https://api.github.com"; export const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
@@ -0,0 +1,37 @@
---
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,6 +19,7 @@ your IdP cannot and will not have access to the decryption key needed to decrypt
- [Google SSO](/documentation/platform/sso/google) - [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github) - [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta) - [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure) - [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
+1 -1
View File
@@ -26,7 +26,7 @@ Service Token V3 (ST V3) is a new and improved authentication method that is in
Here's a few pointers to get you acquainted with it: 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 - 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 `proj_token`. The token provides access to the Infisical API and the public-private key `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. 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 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 provisioning granular `read` or `readWrite` access down to each path.
Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

+1 -1
View File
@@ -107,7 +107,7 @@ build-job:
Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application: 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_ID_GITLAB`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB`: The **Client Secret** of your GitLab application. - `CLIENT_SECRET_GITLAB`: The **Secret** of your GitLab application.
Once added, restart your Infisical instance and use the GitLab integration. Once added, restart your Infisical instance and use the GitLab integration.
+1 -1
View File
@@ -36,7 +36,7 @@ Consider the following `JSON`:
{ {
"publicKey": "...", "publicKey": "...",
"privateKey": "...", "privateKey": "...",
"serviceToken": "proj_token..." "serviceToken": "stv3..."
} }
``` ```
+1
View File
@@ -126,6 +126,7 @@
"documentation/platform/sso/overview", "documentation/platform/sso/overview",
"documentation/platform/sso/google", "documentation/platform/sso/google",
"documentation/platform/sso/github", "documentation/platform/sso/github",
"documentation/platform/sso/gitlab",
"documentation/platform/sso/okta", "documentation/platform/sso/okta",
"documentation/platform/sso/azure", "documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud" "documentation/platform/sso/jumpcloud"
@@ -155,6 +155,15 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional> <ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional>
OAuth2 client secret for GitHub login OAuth2 client secret for GitHub login
</ParamField> </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>
<Tab title="Others"> <Tab title="Others">
#### JWT #### JWT
+1
View File
@@ -15,6 +15,7 @@ You can view specific documentation for how to set up each SSO authentication me
- [Google SSO](/documentation/platform/sso/google) - [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github) - [GitHub SSO](/documentation/platform/sso/github)
- [GitLab SSO](/documentation/platform/sso/gitlab)
- [Okta SAML](/documentation/platform/sso/okta) - [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure) - [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
+3 -1
View File
@@ -1,6 +1,7 @@
ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id ARG INTERCOM_ID=intercom-id
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
FROM node:16-alpine AS deps 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. # Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@@ -13,7 +14,6 @@ COPY package.json package-lock.json next.config.js ./
# Install dependencies # Install dependencies
RUN npm ci --only-production --ignore-scripts RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:16-alpine AS builder FROM node:16-alpine AS builder
WORKDIR /app WORKDIR /app
@@ -52,6 +52,8 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_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 --chown=nextjs:nodejs --chmod=555 scripts ./scripts
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons"; import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -9,74 +9,94 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "../v2"; import { Button } from "../v2";
export default function InitialSignupStep({ export default function InitialSignupStep({
setIsSignupWithEmail, setIsSignupWithEmail
}: { }: {
setIsSignupWithEmail: (value: boolean) => void setIsSignupWithEmail: (value: boolean) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
return <div className='flex flex-col mx-auto w-full justify-center items-center'> return (
<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="mx-auto flex w-full flex-col items-center justify-center">
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'> <h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<Button {t("signup.initial-title")}
colorSchema="primary" </h1>
variant="solid" <div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
onClick={() => { <Button
window.open("/api/v1/sso/redirect/google"); colorSchema="primary"
window.close(); variant="solid"
}} onClick={() => {
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} window.open("/api/v1/sso/redirect/google");
className="h-12 w-full mx-0" window.close();
> }}
{t("signup.continue-with-google")} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
</Button> className="mx-0 h-12 w-full"
</div> >
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'> {t("signup.continue-with-google")}
<Button </Button>
colorSchema="primary" </div>
variant="outline_bg" <div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
onClick={() => { <Button
window.open("/api/v1/sso/redirect/github"); colorSchema="primary"
window.close(); variant="outline_bg"
}} onClick={() => {
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} window.open("/api/v1/sso/redirect/github");
className="h-12 w-full mx-0" window.close();
> }}
Continue with GitHub leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
</Button> className="mx-0 h-12 w-full"
</div> >
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> Continue with GitHub
<Button </Button>
colorSchema="primary" </div>
variant="outline_bg" <div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
onClick={() => { <Button
setIsSignupWithEmail(true); colorSchema="primary"
}} variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />} onClick={() => {
className="h-12 w-full mx-0" window.open("/api/v1/sso/redirect/gitlab");
> window.close();
Continue with Email }}
</Button> leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
</div> className="mx-0 h-12 w-full"
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> >
<Button Continue with GitLab
colorSchema="primary" </Button>
variant="outline_bg" </div>
onClick={() => router.push("/saml-sso")} <div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} <Button
className="h-12 w-full mx-0" colorSchema="primary"
> variant="outline_bg"
Continue with SSO onClick={() => {
</Button> setIsSignupWithEmail(true);
</div> }}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'> leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
{t("signup.create-policy")} className="mx-0 h-12 w-full"
</div> >
<div className="mt-2 text-bunker-400 text-xs flex flex-row"> Continue with Email
<Link href="/login"> </Button>
<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> </div>
</Link> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
</div> <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>
</div> </div>
);
} }
+4 -3
View File
@@ -4,9 +4,10 @@ export enum AuthMethod {
EMAIL = "email", EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
GITHUB = "github", GITHUB = "github",
OKTA_SAML = "okta-saml", GITLAB = "gitlab",
AZURE_SAML = "azure-saml", OKTA_SAML = "okta-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
} }
export type User = { export type User = {
@@ -21,6 +21,7 @@ import {
faCheck, faCheck,
faEnvelope, faEnvelope,
faInfinity, faInfinity,
faInfo,
faMobile, faMobile,
faPlus, faPlus,
faQuestion faQuestion
@@ -122,6 +123,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const createWs = useCreateWorkspace(); const createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey(); const uploadWsKey = useUploadWsKey();
const addWsUser = useAddUserToWs(); const addWsUser = useAddUserToWs();
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addNewWs", "addNewWs",
@@ -696,6 +698,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
</div> </div>
</button> </button>
)} )}
{infisicalPlatformVersion && (
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Platform Version: {infisicalPlatformVersion}
</div>
)}
</div> </div>
</nav> </nav>
</aside> </aside>
@@ -2,10 +2,10 @@ import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons"; import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios" import axios from "axios";
import Error from "@app/components/basic/Error"; import Error from "@app/components/basic/Error";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -16,208 +16,234 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
type Props = { type Props = {
setStep: (step: number) => void; setStep: (step: number) => void;
email: string; email: string;
setEmail: (email: string) => void; setEmail: (email: string) => void;
password: string; password: string;
setPassword: (email: string) => void; setPassword: (email: string) => void;
} };
export const InitialStep = ({ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
setStep, const router = useRouter();
email, const { createNotification } = useNotificationContext();
setEmail, const { t } = useTranslation();
password, const [isLoading, setIsLoading] = useState(false);
setPassword const [loginError, setLoginError] = useState(false);
}: Props) => { const { data: serverDetails } = useFetchServerStatus();
const router = useRouter(); const queryParams = new URLSearchParams(window.location.search);
const { createNotification } = useNotificationContext();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { data: serverDetails } = useFetchServerStatus();
const queryParams = new URLSearchParams(window.location.search);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => { const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault();
try { try {
if (!email || !password) { if (!email || !password) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
if (queryParams && queryParams.get("callback_port")) { if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port") const callbackPort = queryParams.get("callback_port");
// attemptCliLogin // attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({ const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(), email: email.toLowerCase(),
password, password
}) });
if (isCliLoginSuccessful && isCliLoginSuccessful.success) { if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`;
if (isCliLoginSuccessful.mfaEnabled) { // send request to server endpoint
// case: login requires MFA step const instance = axios.create();
setStep(1); await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse });
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint // cli page
const instance = axios.create() router.push("/cli-redirect");
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
// cli page // on success, router.push to cli Login Successful page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
} }
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
setIsLoading(false); if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
} }
return ( setIsLoading(false);
<form onSubmit={handleLogin} 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' >Login to Infisical</h1>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> return (
<Button <form
colorSchema="primary" onSubmit={handleLogin}
variant="outline_bg" className="mx-auto flex w-full flex-col items-center justify-center"
onClick={() => { >
const callbackPort = queryParams.get("callback_port"); <h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`); </h1>
window.close(); <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
}} <Button
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} colorSchema="primary"
className="h-11 w-full mx-0" variant="outline_bg"
> onClick={() => {
{t("login.continue-with-google")} const callbackPort = queryParams.get("callback_port");
</Button>
</div> window.open(
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> `/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
<Button );
colorSchema="primary" window.close();
variant="outline_bg" }}
onClick={() => { leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
const callbackPort = queryParams.get("callback_port"); className="mx-0 h-11 w-full"
>
window.open(`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`); {t("login.continue-with-google")}
</Button>
window.close(); </div>
}} <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} <Button
className="h-11 w-full mx-0" colorSchema="primary"
> variant="outline_bg"
Continue with GitHub onClick={() => {
</Button> const callbackPort = queryParams.get("callback_port");
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> window.open(
<Button `/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
colorSchema="primary" );
variant="outline_bg"
onClick={() => { window.close();
setStep(2); }}
}} leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} className="mx-0 h-11 w-full"
className="h-11 w-full mx-0" >
> Continue with GitHub
Continue with SSO </Button>
</Button> </div>
</div> <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'> <Button
<div className='w-full border-t border-mineshaft-400/60' /> colorSchema="primary"
<span className="mx-2 text-mineshaft-200 text-xs">or</span> variant="outline_bg"
<div className='w-full border-t border-mineshaft-400/60' /> onClick={() => {
</div> const callbackPort = queryParams.get("callback_port");
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'>
<Input window.open(
value={email} `/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
onChange={(e) => setEmail(e.target.value)} );
type="email"
placeholder="Enter your email..." window.close();
isRequired }}
autoComplete="username" leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="h-11" className="mx-0 h-11 w-full"
/> >
</div> Continue with GitLab
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> </Button>
<Input </div>
value={password} <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
onChange={(e) => setPassword(e.target.value)} <Button
type="password" colorSchema="primary"
placeholder="Enter your password..." variant="outline_bg"
isRequired onClick={() => {
autoComplete="current-password" setStep(2);
id="current-password" }}
className="h-11 select:-webkit-autofill:focus" leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
/> className="mx-0 h-11 w-full"
</div> >
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-5'> Continue with SSO
<Button </Button>
type="submit" </div>
size="sm" <div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
isFullWidth <div className="w-full border-t border-mineshaft-400/60" />
className='h-11' <span className="mx-2 text-xs text-mineshaft-200">or</span>
colorSchema="primary" <div className="w-full border-t border-mineshaft-400/60" />
variant="solid" </div>
isLoading={isLoading} <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
> Continue with Email </Button> <Input
</div> value={email}
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />} onChange={(e) => setEmail(e.target.value)}
{ type="email"
!serverDetails?.inviteOnlySignup ? placeholder="Enter your email..."
<div className="mt-6 text-bunker-400 text-sm flex flex-row"> isRequired
<span className="mr-1">Don&apos;t have an acount yet?</span> autoComplete="username"
<Link href="/signup"> className="h-11"
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span> />
</Link> </div>
</div> : <div /> <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
} <Input
<div className="text-bunker-400 text-sm flex flex-row"> value={password}
<span className="mr-1">Forgot password?</span> onChange={(e) => setPassword(e.target.value)}
<Link href="/verify-email"> type="password"
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span> placeholder="Enter your password..."
</Link> isRequired
</div> autoComplete="current-password"
</form> id="current-password"
); className="select:-webkit-autofill:focus h-11"
} />
</div>
<div className="mt-5 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-11"
colorSchema="primary"
variant="solid"
isLoading={isLoading}
>
{" "}
Continue with Email{" "}
</Button>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{!serverDetails?.inviteOnlySignup ? (
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{t("login.create-account")}
</span>
</Link>
</div>
) : (
<div />
)}
<div className="flex flex-row text-sm text-bunker-400">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Recover your account
</span>
</Link>
</div>
</form>
);
};
@@ -1,5 +1,6 @@
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
@@ -37,17 +38,6 @@ export const APIKeyTable = () => {
} }
}; };
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
};
return ( return (
<TableContainer> <TableContainer>
<Table> <Table>
@@ -69,9 +59,9 @@ export const APIKeyTable = () => {
return ( return (
<Tr className="h-10" key={`api-key-${_id}`}> <Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td> <Td>{name}</Td>
<Td>{formatDate(lastUsed)}</Td> <Td>{format(new Date(lastUsed), "yyyy-MM-dd")}</Td>
<Td>{formatDate(createdAt)}</Td> <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>{formatDate(expiresAt)}</Td> <Td>{format(new Date(expiresAt), "yyyy-MM-dd")}</Td>
<Td> <Td>
<IconButton <IconButton
onClick={async () => { onClick={async () => {
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { faGithub, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faGitlab, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
@@ -10,129 +10,127 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { Switch } from "@app/components/v2"; import { Switch } from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { import { AuthMethod } from "@app/hooks/api/users/types";
AuthMethod
} from "@app/hooks/api/users/types";
interface AuthMethodOption { interface AuthMethodOption {
label: string, label: string;
value: AuthMethod, value: AuthMethod;
icon: IconDefinition; icon: IconDefinition;
} }
const authMethodOpts: AuthMethodOption[] = [ const authMethodOpts: AuthMethodOption[] = [
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope }, { label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle }, { label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub } { label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub },
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab }
]; ];
const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML]; const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML];
const schema = yup.object({ const schema = yup.object({
authMethods: yup.array().required("Auth method is required") authMethods: yup.array().required("Auth method is required")
}); });
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => { export const AuthMethodSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync } = useUpdateUserAuthMethods();
const {
reset,
setValue,
watch,
} = useForm<FormData>({
defaultValues: {
authMethods: user.authMethods,
},
resolver: yupResolver(schema)
});
const authMethods = watch("authMethods");
useEffect(() => {
if (user) {
reset({
authMethods: user.authMethods,
});
}
}, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod));
if (hasSamlEnabled) { const { reset, setValue, watch } = useForm<FormData>({
createNotification({ defaultValues: {
text: "SAML authentication can only be configured in your organization settings", authMethods: user.authMethods
type: "error" },
}); resolver: yupResolver(schema)
} });
const newAuthMethods = value
? [...authMethods, authMethodOpt.value]
: authMethods.filter(auth => auth !== authMethodOpt.value);
if (value) {
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods); const authMethods = watch("authMethods");
createNotification({
text: "Successfully enabled authentication method", useEffect(() => {
type: "success" if (user) {
}); reset({
return; authMethods: user.authMethods
} });
if (newAuthMethods.length === 0) {
createNotification({
text: "You must keep at least 1 authentication method enabled",
type: "error"
});
return;
}
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully disabled authentication method",
type: "success"
});
} }
}, [user]);
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
Authentication methods samlProviders.includes(authMethod)
</h2>
<p className="text-gray-400 mb-4">
By enabling a SSO provider, you are allowing an account with that provider which uses the same email address as your existing Infisical account to be able to log in to Infisical.
</p>
<div className="mb-4">
{user && authMethodOpts.map((authMethodOpt) => {
return (
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
</div>
<Switch
id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
>
<p className="w-12 mr-4">{authMethodOpt.label}</p>
</Switch>
</div>
);
})}
</div>
</div>
); );
}
if (hasSamlEnabled) {
createNotification({
text: "SAML authentication can only be configured in your organization settings",
type: "error"
});
}
const newAuthMethods = value
? [...authMethods, authMethodOpt.value]
: authMethods.filter((auth) => auth !== authMethodOpt.value);
if (value) {
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully enabled authentication method",
type: "success"
});
return;
}
if (newAuthMethods.length === 0) {
createNotification({
text: "You must keep at least 1 authentication method enabled",
type: "error"
});
return;
}
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully disabled authentication method",
type: "success"
});
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">
Authentication methods
</h2>
<p className="mb-4 text-gray-400">
By enabling a SSO provider, you are allowing an account with that provider which uses the
same email address as your existing Infisical account to be able to log in to Infisical.
</p>
<div className="mb-4">
{user &&
authMethodOpts.map((authMethodOpt) => {
return (
<div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
</div>
<Switch
id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
>
<p className="mr-4 w-12">{authMethodOpt.label}</p>
</Switch>
</div>
);
})}
</div>
</div>
);
};
@@ -140,15 +140,19 @@ export const AddServiceTokenV3Modal = ({
if (serviceTokenData) { if (serviceTokenData) {
reset({ reset({
name: serviceTokenData.name, name: serviceTokenData.name,
scopes: serviceTokenData.scopes.map((scope: ServiceTokenV3Scope) => { scopes: serviceTokenData.scopes.map(({
environment,
secretPath,
permissions
}: ServiceTokenV3Scope) => {
let permission = "read"; let permission = "read";
if (scope.permissions.includes(Permission.WRITE)) { if (permissions.includes(Permission.WRITE)) {
permission = "readWrite"; permission = "readWrite";
} }
return ({ return ({
environment: "dev", environment,
secretPath: "/", secretPath,
permission permission
}) })
}), }),
@@ -1,5 +1,6 @@
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
@@ -70,17 +71,6 @@ export const ServiceTokenV3Table = ({
}); });
} }
} }
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
};
return ( return (
<TableContainer> <TableContainer>
@@ -171,9 +161,9 @@ export const ServiceTokenV3Table = ({
})} })}
</Td> </Td>
{/* <Td>{usageCount}</Td> */} {/* <Td>{usageCount}</Td> */}
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td> <Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
<Td>{formatDate(createdAt)}</Td> <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td> <Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
Binary file not shown.

After

Width:  |  Height:  |  Size: 913 KiB