diff --git a/.github/workflows/build-docker-image-to-prod.yml b/.github/workflows/build-docker-image-to-prod.yml index 116ca0cf9..bea66e3fc 100644 --- a/.github/workflows/build-docker-image-to-prod.yml +++ b/.github/workflows/build-docker-image-to-prod.yml @@ -93,6 +93,7 @@ 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 @@ -116,3 +117,4 @@ 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 }} diff --git a/.github/workflows/build-staging-img.yml b/.github/workflows/build-staging-img.yml index 806e89b87..f4233c713 100644 --- a/.github/workflows/build-staging-img.yml +++ b/.github/workflows/build-staging-img.yml @@ -83,6 +83,7 @@ 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 @@ -105,6 +106,7 @@ 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 diff --git a/README.md b/README.md index 499f534a1..900e70625 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@

- infisical infisical

-

Open-source, end-to-end encrypted secret management platform: distribute secrets/configs across your team/infrastructure and prevent secret leaks.

+

The open-source secret management platform: Sync secrets/configs across your team/infrastructure and prevent secret leaks.

@@ -44,11 +43,11 @@

-Dashboard +Dashboard ## 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. diff --git a/backend/package-lock.json b/backend/package-lock.json index 66aeceee2..3429181a2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -50,6 +50,7 @@ "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", @@ -63,7 +64,7 @@ "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6", - "zod": "^3.21.4" + "zod": "^3.22.3" }, "devDependencies": { "@jest/globals": "^29.3.1", @@ -13727,6 +13728,17 @@ "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", @@ -16684,9 +16696,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "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" } @@ -27163,6 +27175,14 @@ "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", @@ -29384,9 +29404,9 @@ "dev": true }, "zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==" } } } diff --git a/backend/package.json b/backend/package.json index e8ecb384f..cd38aa20f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,6 +41,7 @@ "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", @@ -54,7 +55,7 @@ "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6", - "zod": "^3.21.4" + "zod": "^3.22.3" }, "name": "infisical-api", "version": "1.0.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index d84d2ff67..5c3e2f819 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,3 +1,5 @@ +import { GITLAB_URL } from "../variables"; + import InfisicalClient from "infisical-node"; 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 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"; diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index b111d4566..9441734f9 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -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,6 +394,11 @@ 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 }, @@ -447,10 +452,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": [] }] @@ -483,7 +488,7 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => #swagger.responses[200] = { content: { "application/json": { - "schema": { + "schema": { "type": "object", "properties": { "message": { @@ -501,9 +506,9 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => }, "description": "Response after deleting an environment from a workspace" } - } + } } - } + } */ const { params: { workspaceId }, @@ -590,10 +595,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": [] }] @@ -640,7 +645,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: } } } - } + } */ const { params: { workspaceId } diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index f8e5e8702..482e5baa6 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -34,6 +34,93 @@ import { } from "../../validation"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { ActorType } from "../../ee/models"; +import { UnauthorizedRequestError } from "../../utils/errors"; +import { AuthData } from "../../interfaces/middleware"; + +const checkSecretsPermission = async ({ + authData, + workspaceId, + environment, + secretPath, + secretAction +}: { + authData: AuthData; + workspaceId: string; + environment: string; + secretPath: string; + secretAction: ProjectPermissionActions; // CRUD +}): Promise<(env: string, secPath: string) => boolean> => { + + let STV2RequiredPermissions = []; + let STV3RequiredPermissions: Permission[] = []; + + switch (secretAction) { + case ProjectPermissionActions.Create: + STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; + STV3RequiredPermissions = [Permission.WRITE]; + break; + case ProjectPermissionActions.Read: + STV2RequiredPermissions = [PERMISSION_READ_SECRETS]; + STV3RequiredPermissions = [Permission.READ]; + break; + case ProjectPermissionActions.Edit: + STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; + STV3RequiredPermissions = [Permission.WRITE]; + break; + case ProjectPermissionActions.Delete: + STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS]; + STV3RequiredPermissions = [Permission.WRITE]; + break; + } + + switch (authData.actor.type) { + case ActorType.USER: { + const { permission } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + secretAction, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + return (env: string, secPath: string) => + permission.can( + secretAction, + subject(ProjectPermissionSub.Secrets, { + environment: env, + secretPath: secPath + }) + ); + } + case ActorType.SERVICE: { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: STV2RequiredPermissions + }); + return () => true; + } + case ActorType.SERVICE_V3: { + await validateServiceTokenDataV3ClientForWorkspace({ + authData, + serviceTokenData: authData.authPayload as IServiceTokenDataV3, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: STV3RequiredPermissions + }); + return (env: string, secPath: string) => + isValidScopeV3({ + authPayload: authData.authPayload as IServiceTokenDataV3, + environment: env, + secretPath: secPath, + requiredPermissions: STV3RequiredPermissions + }); + } + default: { + throw UnauthorizedRequestError(); + } + } +} /** * Return secrets for workspace with id [workspaceId] and environment @@ -73,54 +160,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => { if (!environment || !workspaceId) throw BadRequestError({ message: "Missing environment or workspace id" }); - let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - 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 permissionCheckFn = await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Read + }); const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -186,38 +232,14 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId, type, include_imports }, params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameRawV3, req); - - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.READ] - }); - break; - } - } + + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Read + }); const secret = await SecretService.getSecret({ secretName, @@ -260,37 +282,13 @@ export const createSecretRaw = async (req: Request, res: Response) => { } } = await validateRequest(reqValidator.CreateSecretRawV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Create + }); const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -360,37 +358,13 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding } } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Edit + }); const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -441,37 +415,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { body: { environment, secretPath, type, workspaceId } } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Delete + }); const { secret } = await SecretService.deleteSecret({ secretName, @@ -525,55 +475,13 @@ export const getSecrets = async (req: Request, res: Response) => { secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; } - let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret - - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - 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 permissionCheckFn = await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Read + }); const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -622,37 +530,13 @@ export const getSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.GetSecretByNameV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_READ_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.READ] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Read + }); const secret = await SecretService.getSecret({ secretName, @@ -695,38 +579,14 @@ export const createSecret = async (req: Request, res: Response) => { }, params: { secretName } } = await validateRequest(reqValidator.CreateSecretV3, req); - - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Create + }); const secret = await SecretService.createSecret({ secretName, @@ -796,38 +656,14 @@ export const updateSecretByName = async (req: Request, res: Response) => { throw BadRequestError({ message: "Missing encrypted key" }); } - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } - + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Edit + }); + const secret = await SecretService.updateSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -873,37 +709,13 @@ export const deleteSecretByName = async (req: Request, res: Response) => { params: { secretName } } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Delete + }); const { secret } = await SecretService.deleteSecret({ secretName, @@ -931,38 +743,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { const { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req); - - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Create + }); const createdSecrets = await SecretService.createSecretBatch({ secretPath, @@ -982,37 +770,13 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Edit + }); const updatedSecrets = await SecretService.updateSecretBatch({ secretPath, @@ -1032,37 +796,13 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { body: { secrets, secretPath, environment, workspaceId } } = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req); - switch (req.authData.actor.type) { - case ActorType.USER: { - const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); - break; - } - case ActorType.SERVICE: { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - break; - } - case ActorType.SERVICE_V3: { - await validateServiceTokenDataV3ClientForWorkspace({ - authData: req.authData, - serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [Permission.WRITE] - }); - break; - } - } + await checkSecretsPermission({ + authData: req.authData, + workspaceId, + environment, + secretPath, + secretAction: ProjectPermissionActions.Delete + }); const deletedSecrets = await SecretService.deleteSecretBatch({ secretPath, @@ -1075,4 +815,4 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { return res.status(200).send({ secrets: deletedSecrets }); -}; +}; \ No newline at end of file diff --git a/backend/src/ee/controllers/v3/serviceTokenDataController.ts b/backend/src/ee/controllers/v3/serviceTokenDataController.ts index 2c5d6d3d4..3f57602f9 100644 --- a/backend/src/ee/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/ee/controllers/v3/serviceTokenDataController.ts @@ -112,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { user = req.authData.authPayload._id; } - const isActive = false; + const isActive = true; const serviceTokenData = await new ServiceTokenDataV3({ name, user, @@ -160,7 +160,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { return res.status(200).send({ serviceTokenData, - serviceToken: `proj_token.${token}` + serviceToken: `stv3.${token}` }); } diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 90d25cd45..baa75d505 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -6,60 +6,20 @@ 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( - "/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, - scope: [ 'user:email' ], - ...(req.query.callback_port - ? { - state: req.query.callback_port as string - } - : {}) - })(req, res, next); -}); - -router.get( - "/github", + "/redirect/saml2/:ssoIdentifier", authLimiter, - passport.authenticate("github", { - failureRedirect: "/login/provider/error", - session: false, - scope: [ 'user:email' ] - }), - ssoController.redirectSSO + (req, res, next) => { + const options = { + failureRedirect: "/", + additionalParams: { + RelayState: req.query.callback_port ?? "" + }, + }; + 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( "/saml2/:ssoIdentifier", passport.authenticate("saml", { diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 861301fb6..31e584fe5 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -86,7 +86,7 @@ export const validateAuthMode = ({ authMode = AuthMode.SERVICE_TOKEN; authTokenValue = tokenValue; break; - case "proj_token": + case "stv3": authMode = AuthMode.SERVICE_TOKEN_V3; authTokenValue = parts.slice(1).join("."); break; diff --git a/backend/src/index.ts b/backend/src/index.ts index fbe85c3c1..602da13ec 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -41,6 +41,7 @@ import { membership as v1MembershipRouter, organization as v1OrganizationRouter, password as v1PasswordRouter, + sso as v1SSORouter, secretApprovalPolicy as v1SecretApprovalPolicy, secretImps as v1SecretImpsRouter, secret as v1SecretRouter, @@ -181,6 +182,7 @@ 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); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index c85c0936b..911776a63 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -4,6 +4,7 @@ export enum AuthMethod { EMAIL = "email", GOOGLE = "google", GITHUB = "github", + GITLAB = "gitlab", OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index cfdccdc92..50275d00f 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -11,6 +11,7 @@ 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"; @@ -39,5 +40,6 @@ export { secretsFolder, webhooks, secretImps, + sso, secretApprovalPolicy }; diff --git a/backend/src/routes/v1/sso.ts b/backend/src/routes/v1/sso.ts new file mode 100644 index 000000000..b06ba9986 --- /dev/null +++ b/backend/src/routes/v1/sso.ts @@ -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; \ No newline at end of file diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index c093be21c..41d4c8e6b 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -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( diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 45e8e47f5..6f46824d4 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -14,16 +14,19 @@ import { 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 @@ -31,6 +34,8 @@ 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"); /** @@ -84,6 +89,9 @@ 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({ @@ -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( { diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 3adfad4a8..e6e20e73c 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -84,7 +84,8 @@ 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 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_VERCEL_API_URL = "https://api.vercel.com"; export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; diff --git a/docs/documentation/platform/sso/gitlab.mdx b/docs/documentation/platform/sso/gitlab.mdx new file mode 100644 index 000000000..cac946280 --- /dev/null +++ b/docs/documentation/platform/sso/gitlab.mdx @@ -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) + + + If you have a GitLab group, you can create an OAuth application under it + in your group Settings > Applications. + + +## 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. \ No newline at end of file diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 359f09fb3..917bb7e19 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -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) - [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) \ No newline at end of file diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index 1c3438ce2..a04c9b853 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -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: - 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. - 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. diff --git a/docs/images/sso/gitlab/credentials.png b/docs/images/sso/gitlab/credentials.png new file mode 100644 index 000000000..f44223a1e Binary files /dev/null and b/docs/images/sso/gitlab/credentials.png differ diff --git a/docs/images/sso/gitlab/edit-profile.png b/docs/images/sso/gitlab/edit-profile.png new file mode 100644 index 000000000..c6eb4d95a Binary files /dev/null and b/docs/images/sso/gitlab/edit-profile.png differ diff --git a/docs/images/sso/gitlab/new-app-form.png b/docs/images/sso/gitlab/new-app-form.png new file mode 100644 index 000000000..988778b90 Binary files /dev/null and b/docs/images/sso/gitlab/new-app-form.png differ diff --git a/docs/images/sso/gitlab/new-app.png b/docs/images/sso/gitlab/new-app.png new file mode 100644 index 000000000..fac7490a6 Binary files /dev/null and b/docs/images/sso/gitlab/new-app.png differ diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index 9c3df9720..8635a5527 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -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 **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. diff --git a/docs/internals/service-tokens.mdx b/docs/internals/service-tokens.mdx index 08303dd06..4fc8a29cf 100644 --- a/docs/internals/service-tokens.mdx +++ b/docs/internals/service-tokens.mdx @@ -36,7 +36,7 @@ Consider the following `JSON`: { "publicKey": "...", "privateKey": "...", - "serviceToken": "proj_token..." + "serviceToken": "stv3..." } ``` diff --git a/docs/mint.json b/docs/mint.json index 41c406bc6..9ed8b8ab4 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -126,6 +126,7 @@ "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" diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index d03f57128..e348f5b3e 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -155,6 +155,15 @@ Other environment variables are listed below to increase the functionality of yo OAuth2 client secret for GitHub login + + OAuth2 client ID for GitLab login + + + OAuth2 client secret for GitLab login + + + URL of your self-hosted instance of GitLab where the OAuth application is registered + #### JWT diff --git a/docs/self-hosting/configuration/sso.mdx b/docs/self-hosting/configuration/sso.mdx index 2497e368b..2d663790d 100644 --- a/docs/self-hosting/configuration/sso.mdx +++ b/docs/self-hosting/configuration/sso.mdx @@ -15,6 +15,7 @@ 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) \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 49cbd3024..2060c214a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,6 +1,7 @@ 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. @@ -13,7 +14,6 @@ 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,6 +52,8 @@ 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 diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index 0a1248b2d..7953ff0df 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; 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 { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -9,74 +9,94 @@ 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
-

{t("signup.initial-title")}

-
- -
-
- -
-
- -
-
- -
-
- {t("signup.create-policy")} -
-
- - {t("signup.already-have-account")} - -
+ return ( +
+

+ {t("signup.initial-title")} +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {t("signup.create-policy")} +
+
+ + + {t("signup.already-have-account")} + + +
+ ); } diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 07f497c07..6917bcdc7 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -4,9 +4,10 @@ export enum AuthMethod { EMAIL = "email", GOOGLE = "google", GITHUB = "github", - OKTA_SAML = "okta-saml", - AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + GITLAB = "gitlab", + OKTA_SAML = "okta-saml", + AZURE_SAML = "azure-saml", + JUMPCLOUD_SAML = "jumpcloud-saml" } export type User = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 50dae373b..e9a66ead2 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -21,6 +21,7 @@ import { faCheck, faEnvelope, faInfinity, + faInfo, faMobile, faPlus, faQuestion @@ -122,6 +123,7 @@ export const AppLayout = ({ children }: LayoutProps) => { const createWs = useCreateWorkspace(); const uploadWsKey = useUploadWsKey(); const addWsUser = useAddUserToWs(); + const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addNewWs", @@ -696,6 +698,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
)} + {infisicalPlatformVersion && ( +
+ + Platform Version: {infisicalPlatformVersion} +
+ )} diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 7bab7414e..50eaf5cb7 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -2,10 +2,10 @@ import { FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import axios from "axios" +import axios from "axios"; import Error from "@app/components/basic/Error"; 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"; type Props = { - setStep: (step: number) => void; - email: string; - setEmail: (email: string) => void; - password: string; - setPassword: (email: string) => void; -} + setStep: (step: number) => void; + email: string; + setEmail: (email: string) => void; + password: string; + setPassword: (email: string) => void; +}; -export const InitialStep = ({ - setStep, - email, - setEmail, - password, - setPassword -}: Props) => { - const router = useRouter(); - 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); +export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => { + const router = useRouter(); + 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) => { - e.preventDefault() - try { - if (!email || !password) { - return; - } + const handleLogin = async (e: FormEvent) => { + e.preventDefault(); + try { + if (!email || !password) { + return; + } - setIsLoading(true); - if (queryParams && queryParams.get("callback_port")) { - const callbackPort = queryParams.get("callback_port") + setIsLoading(true); + if (queryParams && queryParams.get("callback_port")) { + const callbackPort = queryParams.get("callback_port"); - // attemptCliLogin - const isCliLoginSuccessful = await attemptCliLogin({ - email: email.toLowerCase(), - password, - }) + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email: email.toLowerCase(), + 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) { - // case: login requires MFA step - setStep(1); - setIsLoading(false); - return; - } - // case: login was successful - const cliUrl = `http://localhost:${callbackPort}` + // send request to server endpoint + const instance = axios.create(); + await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }); - // send request to server endpoint - const instance = axios.create() - await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) + // cli page + router.push("/cli-redirect"); - // cli 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" - }); + // 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 - 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 ( -
-

Login to Infisical

-
- -
-
- -
-
- -
-
-
- or -
-
-
- setEmail(e.target.value)} - type="email" - placeholder="Enter your email..." - isRequired - autoComplete="username" - className="h-11" - /> -
-
- setPassword(e.target.value)} - type="password" - placeholder="Enter your password..." - isRequired - autoComplete="current-password" - id="current-password" - className="h-11 select:-webkit-autofill:focus" - /> -
-
- -
- {!isLoading && loginError && } - { - !serverDetails?.inviteOnlySignup ? -
- Don't have an acount yet? - - {t("login.create-account")} - -
:
- } -
- Forgot password? - - Recover your account - -
- - ); -} \ No newline at end of file + setIsLoading(false); + }; + + return ( +
+

+ Login to Infisical +

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ or +
+
+
+ setEmail(e.target.value)} + type="email" + placeholder="Enter your email..." + isRequired + autoComplete="username" + className="h-11" + /> +
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="select:-webkit-autofill:focus h-11" + /> +
+
+ +
+ {!isLoading && loginError && } + {!serverDetails?.inviteOnlySignup ? ( +
+ Don't have an acount yet? + + + {t("login.create-account")} + + +
+ ) : ( +
+ )} +
+ Forgot password? + + + Recover your account + + +
+ + ); +}; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx index 788b8d23c..3c08c04cd 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/APIKeySection/APIKeyTable.tsx @@ -1,5 +1,6 @@ import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { @@ -37,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 ( @@ -69,9 +59,9 @@ export const APIKeyTable = () => { return ( - - - + + + {/* */} - - - + + +
{name}{formatDate(lastUsed)}{formatDate(createdAt)}{formatDate(expiresAt)}{format(new Date(lastUsed), "yyyy-MM-dd")}{format(new Date(createdAt), "yyyy-MM-dd")}{format(new Date(expiresAt), "yyyy-MM-dd")} { diff --git a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx index 42917aae6..73d266e2e 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/AuthMethodSection/AuthMethodSection.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { useUser } from "@app/context"; import { useUpdateUserAuthMethods } from "@app/hooks/api"; -import { - AuthMethod -} from "@app/hooks/api/users/types"; +import { AuthMethod } from "@app/hooks/api/users/types"; interface AuthMethodOption { - label: string, - value: AuthMethod, - icon: IconDefinition; + label: string; + value: AuthMethod; + icon: IconDefinition; } const authMethodOpts: AuthMethodOption[] = [ - { label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope }, - { label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle }, - { label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub } + { label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope }, + { label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle }, + { 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 schema = yup.object({ - authMethods: yup.array().required("Auth method is required") + authMethods: yup.array().required("Auth method is required") }); export type FormData = yup.InferType; export const AuthMethodSection = () => { - const { createNotification } = useNotificationContext(); - const { user } = useUser(); - const { mutateAsync } = useUpdateUserAuthMethods(); - - const { - reset, - setValue, - watch, - } = useForm({ - 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)); + const { createNotification } = useNotificationContext(); + const { user } = useUser(); + const { mutateAsync } = useUpdateUserAuthMethods(); - 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 - }); + const { reset, setValue, watch } = useForm({ + defaultValues: { + authMethods: user.authMethods + }, + resolver: yupResolver(schema) + }); - 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" - }); + const authMethods = watch("authMethods"); + + useEffect(() => { + if (user) { + reset({ + authMethods: user.authMethods + }); } - - return ( -
-

- Authentication methods -

-

- 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. -

-
- {user && authMethodOpts.map((authMethodOpt) => { - return ( -
-
- -
- onAuthMethodToggle(value, authMethodOpt)} - isChecked={authMethods?.includes(authMethodOpt.value) ?? false} - > -

{authMethodOpt.label}

-
-
- ); - })} -
-
+ }, [user]); + + const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => { + const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => + samlProviders.includes(authMethod) ); -} + + 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 ( +
+

+ Authentication methods +

+

+ 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. +

+
+ {user && + authMethodOpts.map((authMethodOpt) => { + return ( +
+
+ +
+ onAuthMethodToggle(value, authMethodOpt)} + isChecked={authMethods?.includes(authMethodOpt.value) ?? false} + > +

{authMethodOpt.label}

+
+
+ ); + })} +
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index ea8ab6f4c..cc3c81bab 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -140,15 +140,19 @@ export const AddServiceTokenV3Modal = ({ if (serviceTokenData) { reset({ name: serviceTokenData.name, - scopes: serviceTokenData.scopes.map((scope: ServiceTokenV3Scope) => { + scopes: serviceTokenData.scopes.map(({ + environment, + secretPath, + permissions + }: ServiceTokenV3Scope) => { let permission = "read"; - if (scope.permissions.includes(Permission.WRITE)) { + if (permissions.includes(Permission.WRITE)) { permission = "readWrite"; } - + return ({ - environment: "dev", - secretPath: "/", + environment, + secretPath, permission }) }), diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx index e30d83eca..dd9268bf6 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/ServiceTokenV3Table.tsx @@ -1,5 +1,6 @@ import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { ProjectPermissionCan } from "@app/components/permissions"; @@ -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 ( @@ -171,9 +161,9 @@ export const ServiceTokenV3Table = ({ })}
{usageCount}{lastUsed ? formatDate(lastUsed) : "-"}{formatDate(createdAt)}{expiresAt ? formatDate(expiresAt) : "-"}{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}{format(new Date(createdAt), "yyyy-MM-dd")}{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}