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";
@@ -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 },
+126 -386
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) {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.READ] secretAction: ProjectPermissionActions.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),
@@ -187,37 +233,13 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req); } = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
switch (req.authData.actor.type) { await checkSecretsPermission({
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.READ] secretAction: ProjectPermissionActions.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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Create
}); });
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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Edit
}); });
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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Delete
}); });
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({
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.READ] secretAction: ProjectPermissionActions.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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.READ] secretAction: ProjectPermissionActions.Read
}); });
break;
}
}
const secret = await SecretService.getSecret({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -696,37 +580,13 @@ 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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Create
}); });
break;
}
}
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
secretName, secretName,
@@ -796,37 +656,13 @@ 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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Edit
}); });
break;
}
}
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
secretName, secretName,
@@ -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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Delete
}); });
break;
}
}
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -932,37 +744,13 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Create
}); });
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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Edit
}); });
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: {
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, authData: req.authData,
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
requiredPermissions: [Permission.WRITE] secretAction: ProjectPermissionActions.Delete
}); });
break;
}
}
const deletedSecrets = await SecretService.deleteSecretBatch({ const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath, secretPath,
@@ -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}`
}); });
} }
+5 -45
View File
@@ -6,59 +6,19 @@ 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",
session: false,
scope: [ 'user:email' ]
}),
ssoController.redirectSSO
);
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
const options = { const options = {
failureRedirect: "/", failureRedirect: "/",
additionalParams: { additionalParams: {
RelayState: req.query.callback_port ?? "" RelayState: req.query.callback_port ?? ""
} },
}; };
passport.authenticate("saml", options)(req, res, next); passport.authenticate("saml", options)(req, res, next);
}); }
);
router.post( router.post(
"/saml2/:ssoIdentifier", "/saml2/:ssoIdentifier",
+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;
+2 -2
View File
@@ -31,9 +31,9 @@ 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({
@@ -218,6 +226,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(
{ {
passReqToCallback: true, passReqToCallback: true,
+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,16 +9,19 @@ 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">
{t("signup.initial-title")}
</h1>
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
@@ -27,12 +30,12 @@ export default function InitialSignupStep({
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
{t("signup.continue-with-google")} {t("signup.continue-with-google")}
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -41,12 +44,26 @@ export default function InitialSignupStep({
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with GitHub Continue with GitHub
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/gitlab");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitLab
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -54,29 +71,32 @@ export default function InitialSignupStep({
setIsSignupWithEmail(true); setIsSignupWithEmail(true);
}} }}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with Email Continue with Email
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => router.push("/saml-sso")} onClick={() => router.push("/saml-sso")}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-12 w-full mx-0" className="mx-0 h-12 w-full"
> >
Continue with SSO Continue with SSO
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'> <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")} {t("signup.create-policy")}
</div> </div>
<div className="mt-2 text-bunker-400 text-xs flex flex-row"> <div className="mt-2 flex flex-row text-xs text-bunker-400">
<Link href="/login"> <Link href="/login">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("signup.already-have-account")}</span> <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> </Link>
</div> </div>
</div> </div>
);
} }
+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"
@@ -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";
@@ -21,15 +21,9 @@ type Props = {
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,
email,
setEmail,
password,
setPassword
}: Props) => {
const router = useRouter(); const router = useRouter();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -39,7 +33,7 @@ export const InitialStep = ({
const queryParams = new URLSearchParams(window.location.search); 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;
@@ -47,16 +41,15 @@ export const InitialStep = ({
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) { if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step // case: login requires MFA step
setStep(1); setStep(1);
@@ -64,22 +57,21 @@ export const InitialStep = ({
return; return;
} }
// case: login was successful // case: login was successful
const cliUrl = `http://localhost:${callbackPort}` const cliUrl = `http://localhost:${callbackPort}`;
// send request to server endpoint // send request to server endpoint
const instance = axios.create() const instance = axios.create();
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse });
// cli page // cli page
router.push("/cli-redirect"); router.push("/cli-redirect");
// on success, router.push to cli Login Successful page // on success, router.push to cli Login Successful page
} }
} else { } else {
const isLoginSuccessful = await attemptLogin({ const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(), email: email.toLowerCase(),
password, password
}); });
if (isLoginSuccessful && isLoginSuccessful.success) { if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful // case: login was successful
@@ -101,8 +93,6 @@ export const InitialStep = ({
router.push(`/org/${userOrg}/overview`); router.push(`/org/${userOrg}/overview`);
} }
} }
} catch (err) { } catch (err) {
setLoginError(true); setLoginError(true);
createNotification({ createNotification({
@@ -112,45 +102,73 @@ export const InitialStep = ({
} }
setIsLoading(false); setIsLoading(false);
} };
return ( return (
<form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'> <form
<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> onSubmit={handleLogin}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> className="mx-auto flex w-full flex-col items-center justify-center"
>
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
</h1>
<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 <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.open(
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
{t("login.continue-with-google")} {t("login.continue-with-google")}
</Button> </Button>
</div> </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'> <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 <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`); window.open(
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close(); window.close();
}} }}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
Continue with GitHub Continue with GitHub
</Button> </Button>
</div> </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'> <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
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-11 w-full"
>
Continue with GitLab
</Button>
</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">
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
@@ -158,17 +176,17 @@ export const InitialStep = ({
setStep(2); setStep(2);
}} }}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="h-11 w-full mx-0" className="mx-0 h-11 w-full"
> >
Continue with SSO Continue with SSO
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'> <div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
<div className='w-full border-t border-mineshaft-400/60' /> <div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-mineshaft-200 text-xs">or</span> <span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className='w-full border-t border-mineshaft-400/60' /> <div className="w-full border-t border-mineshaft-400/60" />
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'> <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input <Input
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
@@ -179,7 +197,7 @@ export const InitialStep = ({
className="h-11" className="h-11"
/> />
</div> </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'> <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 <Input
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
@@ -188,36 +206,44 @@ export const InitialStep = ({
isRequired isRequired
autoComplete="current-password" autoComplete="current-password"
id="current-password" id="current-password"
className="h-11 select:-webkit-autofill:focus" className="select:-webkit-autofill:focus h-11"
/> />
</div> </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'> <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 <Button
type="submit" type="submit"
size="sm" size="sm"
isFullWidth isFullWidth
className='h-11' className="h-11"
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
isLoading={isLoading} isLoading={isLoading}
> Continue with Email </Button> >
{" "}
Continue with Email{" "}
</Button>
</div> </div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />} {!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{ {!serverDetails?.inviteOnlySignup ? (
!serverDetails?.inviteOnlySignup ? <div className="mt-6 flex flex-row text-sm text-bunker-400">
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span> <span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup"> <Link href="/signup">
<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> <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> </Link>
</div> : <div /> </div>
} ) : (
<div className="text-bunker-400 text-sm flex flex-row"> <div />
)}
<div className="flex flex-row text-sm text-bunker-400">
<span className="mr-1">Forgot password?</span> <span className="mr-1">Forgot password?</span>
<Link href="/verify-email"> <Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span> <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> </Link>
</div> </div>
</form> </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,20 +10,19 @@ 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];
@@ -39,13 +38,9 @@ export const AuthMethodSection = () => {
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync } = useUpdateUserAuthMethods();
const { const { reset, setValue, watch } = useForm<FormData>({
reset,
setValue,
watch,
} = useForm<FormData>({
defaultValues: { defaultValues: {
authMethods: user.authMethods, authMethods: user.authMethods
}, },
resolver: yupResolver(schema) resolver: yupResolver(schema)
}); });
@@ -55,14 +50,15 @@ export const AuthMethodSection = () => {
useEffect(() => { useEffect(() => {
if (user) { if (user) {
reset({ reset({
authMethods: user.authMethods, authMethods: user.authMethods
}); });
} }
}, [user]); }, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => { const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod)); samlProviders.includes(authMethod)
);
if (hasSamlEnabled) { if (hasSamlEnabled) {
createNotification({ createNotification({
@@ -73,7 +69,7 @@ export const AuthMethodSection = () => {
const newAuthMethods = value const newAuthMethods = value
? [...authMethods, authMethodOpt.value] ? [...authMethods, authMethodOpt.value]
: authMethods.filter(auth => auth !== authMethodOpt.value); : authMethods.filter((auth) => auth !== authMethodOpt.value);
if (value) { if (value) {
const newUser = await mutateAsync({ const newUser = await mutateAsync({
@@ -105,20 +101,22 @@ export const AuthMethodSection = () => {
text: "Successfully disabled authentication method", text: "Successfully disabled authentication method",
type: "success" type: "success"
}); });
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> <h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">
Authentication methods Authentication methods
</h2> </h2>
<p className="text-gray-400 mb-4"> <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. 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> </p>
<div className="mb-4"> <div className="mb-4">
{user && authMethodOpts.map((authMethodOpt) => { {user &&
authMethodOpts.map((authMethodOpt) => {
return ( return (
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}> <div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center"> <div className="flex items-center">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" /> <FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
</div> </div>
@@ -127,7 +125,7 @@ export const AuthMethodSection = () => {
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)} onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false} isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
> >
<p className="w-12 mr-4">{authMethodOpt.label}</p> <p className="mr-4 w-12">{authMethodOpt.label}</p>
</Switch> </Switch>
</div> </div>
); );
@@ -135,4 +133,4 @@ export const AuthMethodSection = () => {
</div> </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";
@@ -71,17 +72,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>
<Table> <Table>
@@ -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