Compare commits

...

58 Commits

Author SHA1 Message Date
Maidul Islam
ea426e8b2d Merge pull request #2685 from akhilmhdh/fix/tag-no-update-in-approval
fix: resolved tag update not happening via approval
2024-11-05 09:54:13 -05:00
=
4d567f0b08 fix: resolved tag update not happening via approval 2024-11-05 20:18:16 +05:30
Sheen
6548372e3b Merge pull request #2690 from Infisical/feat/add-mssql-secret-rotation-support
feat: add mssql secret rotation template
2024-11-05 22:33:56 +08:00
Sheen Capadngan
77af640c4c misc: addressed lint issues 2024-11-05 22:22:41 +08:00
Sheen Capadngan
90f85152bc misc: added configurable env for enabling/disabling encrypt 2024-11-05 22:08:16 +08:00
Sheen Capadngan
cfa8770bdc misc: addressed issue 2024-11-05 21:57:40 +08:00
Sheen Capadngan
be8562824d feat: add mssql secret rotation template 2024-11-05 18:38:09 +08:00
Vlad Matsiiako
7503876ca0 Merge pull request #2683 from Infisical/blueprint-org-structure
added blueprint for org structure
2024-11-03 09:48:27 -08:00
Vladyslav Matsiiako
36b5a3dc90 fix typo 2024-11-03 09:40:33 -08:00
Vlad Matsiiako
dfe36f346f Merge pull request #2682 from cyberbohu/patch-1
Update overview.mdx
2024-11-03 09:29:56 -08:00
Vladyslav Matsiiako
b1b61842c6 added blueprint for org structure 2024-11-03 09:29:05 -08:00
cyberbohu
f9ca9b51b2 Update overview.mdx
spell check
2024-11-03 12:37:30 +01:00
Maidul Islam
7e7e6ade5c Update deployment-pipeline.yml 2024-11-02 13:19:50 -04:00
Maidul Islam
4010817916 Increase batch size and remove transation 2024-11-02 12:48:34 -04:00
Maidul Islam
eea367c3bc Merge pull request #2606 from Infisical/daniel/multiple-auth-methods
feat: multiple auth methods for identities
2024-11-02 12:17:37 -04:00
Daniel Hougaard
860ebb73a9 Update 20241014084900_identity-multiple-auth-methods.ts 2024-11-02 19:44:09 +04:00
Maidul Islam
56567ee7c9 Update deployment-pipeline.yml 2024-11-02 11:31:37 -04:00
Daniel Hougaard
1cd17a451c fix: add batching 2024-11-02 19:27:07 +04:00
BlackMagiq
6b7bc2a3c4 Merge pull request #2677 from un/main
fix: minor typo
2024-11-01 13:12:07 -07:00
McPizza0
cb52568ebd fix: minor typo 2024-11-01 19:59:08 +01:00
Vlad Matsiiako
9d30fb3870 Merge pull request #2676 from scott-ray-wilson/oidc-default-org-docs
Docs: OIDC Default Org Support and OIDC/SAML Tip/Info Improvements
2024-11-01 10:46:15 -07:00
Scott Wilson
161ac5e097 docs: oidc added to default org description and improve oidc/smal info/tips 2024-11-01 10:38:57 -07:00
Maidul Islam
bb5b585cf6 Merge pull request #2674 from scott-ray-wilson/docs-update-api-base-url
Docs: Update OpenAPI Spec Servers
2024-11-01 00:54:51 -04:00
BlackMagiq
fa94191c40 Merge pull request #2673 from areifert/misc/make-azure-devops-variables-secret
Make synced Azure DevOps variables secret
2024-10-31 20:39:59 -07:00
Scott Wilson
6a5eabc411 docs: update urls for openapi docs 2024-10-31 19:51:26 -07:00
Maidul Islam
c956a0f91f Merge pull request #2667 from scott-ray-wilson/oidc-default-org-slug
Feature: OIDC Default Org
2024-10-31 21:56:53 -04:00
Scott Wilson
df7b55606e feature: oidc support for oidc and only display saml/oidc login if enforced 2024-10-31 15:13:13 -07:00
areifert
5f14b27f41 Make Azure DevOps variables secret 2024-10-31 13:29:43 -06:00
Scott Wilson
02b2395276 Merge pull request #2666 from scott-ray-wilson/snowflake-dynamic-secrets
Feature: Snowflake Dynamic Secrets
2024-10-31 11:58:00 -07:00
Scott Wilson
402fa2b0e0 fix: correct typo 2024-10-31 11:53:02 -07:00
Scott Wilson
3725241f52 improvement: improve error for leases 2024-10-31 10:39:43 -07:00
Scott Wilson
10b457a695 fix: correct early return for renew 2024-10-31 10:31:50 -07:00
Scott Wilson
3912e2082d fix: check that renew statement actually exists 2024-10-31 10:27:55 -07:00
Scott Wilson
7dd6eac20a improvement: address feedback 2024-10-31 10:24:30 -07:00
Sheen
5664e1ff26 Merge pull request #2670 from Infisical/feat/added-key-id-column
feat: added key id column
2024-11-01 01:14:46 +08:00
Sheen Capadngan
a27a428329 misc: added mint json changes 2024-11-01 00:47:06 +08:00
Sheen Capadngan
b196251c19 doc: add kubernetes encryption 2024-11-01 00:42:23 +08:00
Sheen Capadngan
b18d8d542f misc: add copy to clipboard 2024-11-01 00:22:21 +08:00
Scott Wilson
3c287600ab Merge pull request #2645 from scott-ray-wilson/secrets-quick-search
Feature: Secrets Dashboard Quick/Deep Search
2024-10-31 08:41:54 -07:00
Sheen Capadngan
759d11ff21 feat: added key id column 2024-10-31 19:05:53 +08:00
Scott Wilson
b693c035ce chore: remove dev value 2024-10-30 15:02:25 -07:00
Scott Wilson
c65a991943 fix: add missing type properties on client side 2024-10-30 15:01:21 -07:00
Scott Wilson
3a3811cb3c feature: snowflake dynamic secrets 2024-10-30 14:57:15 -07:00
Scott Wilson
e3ba1c59bf improvement: add search filter tooltip to quick search 2024-10-30 10:38:25 -07:00
Scott Wilson
21ea7dd317 feature: deep search for secrets dashboard 2024-10-29 15:08:19 -07:00
Daniel Hougaard
7245aaa9ec bug fixes 2024-10-26 23:27:38 +04:00
=
d32f69e052 feat: removed redundant check made and error message fix 2024-10-26 23:27:38 +04:00
=
726477e3d7 fix: resolved universal auth update failing 2024-10-26 23:27:38 +04:00
Daniel Hougaard
a4ca996a1b requested changes 2024-10-26 23:27:38 +04:00
Daniel Hougaard
303312fe91 Update identities.ts 2024-10-26 23:27:38 +04:00
Daniel Hougaard
f3f2879d6d chore: minor UI improvement 2024-10-26 23:27:38 +04:00
Daniel Hougaard
d0f3d96b3e fix:find-my-way security vulnerability 2024-10-26 23:27:38 +04:00
Daniel Hougaard
70d2a21fbc fix: make api always return an authMethods array 2024-10-26 23:26:22 +04:00
Daniel Hougaard
418ae42d94 fix: query issues 2024-10-26 23:26:22 +04:00
Daniel Hougaard
273c6b3842 tests: fixed identity creation tests 2024-10-26 23:26:22 +04:00
Daniel Hougaard
6be8d5d2a7 chore: requested changes 2024-10-26 23:26:22 +04:00
Daniel Hougaard
9eb7640755 chore: cleanup 2024-10-26 23:26:22 +04:00
Daniel Hougaard
741138c4bd feat: multiple auth methods for identities 2024-10-26 23:26:22 +04:00
123 changed files with 7974 additions and 2907 deletions

View File

@@ -78,3 +78,5 @@ PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS= PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY= SSL_CLIENT_CERTIFICATE_HEADER_KEY=
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=

View File

@@ -170,6 +170,12 @@ jobs:
- uses: twingate/github-action@v1 - uses: twingate/github-action@v1
with: with:
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: eu-central-1
role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Node.js environment - name: Setup Node.js environment
@@ -183,12 +189,6 @@ jobs:
cd backend cd backend
npm install npm install
npm run migration:latest npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.eu-central-1.amazonaws.com
aws-region: eu-central-1
role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2

View File

@@ -34,7 +34,7 @@ describe("Identity v1", async () => {
test("Create identity", async () => { test("Create identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1"); expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull(); expect(newIdentity.authMethods).toEqual([]);
await deleteIdentity(newIdentity.id); await deleteIdentity(newIdentity.id);
}); });
@@ -42,7 +42,7 @@ describe("Identity v1", async () => {
test("Update identity", async () => { test("Update identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1"); expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull(); expect(newIdentity.authMethods).toEqual([]);
const updatedIdentity = await testServer.inject({ const updatedIdentity = await testServer.inject({
method: "PATCH", method: "PATCH",

2502
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest", "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up", "auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down", "auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
@@ -156,7 +156,7 @@
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"cron": "^3.1.7", "cron": "^3.1.7",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"fastify": "^4.26.0", "fastify": "^4.28.1",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0", "google-auth-library": "^9.9.0",
"googleapis": "^137.1.0", "googleapis": "^137.1.0",
@@ -196,6 +196,7 @@
"scim2-parse-filter": "^0.2.10", "scim2-parse-filter": "^0.2.10",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"snowflake-sdk": "^1.14.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",

View File

@@ -0,0 +1,76 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 30_000;
export async function up(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
if (!hasAuthMethodColumnAccessToken) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("authMethod").nullable();
});
let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
let totalUpdated = 0;
do {
const batchIds = nullableAccessTokens.map((token) => token.id);
// ! Update the auth method column in batches for the current batch
// eslint-disable-next-line no-await-in-loop
await knex(TableName.IdentityAccessToken)
.whereIn("id", batchIds)
.update({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore because generate schema happens after this
authMethod: knex(TableName.Identity)
.select("authMethod")
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
.whereNotNull("authMethod")
.first()
});
// eslint-disable-next-line no-await-in-loop
nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
totalUpdated += batchIds.length;
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
} while (nullableAccessTokens.length > 0);
// ! We delete all access tokens where the identity has no auth method set!
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
await knex(TableName.IdentityAccessToken)
.whereNotExists((queryBuilder) => {
void queryBuilder
.select("id")
.from(TableName.Identity)
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
.whereNotNull("authMethod");
})
.delete();
// Finally we set the authMethod to notNullable after populating the column.
// This will fail if the data is not populated correctly, so it's safe.
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("authMethod").notNullable().alter();
});
}
// ! We aren't dropping the authMethod column from the Identity itself, because we wan't to be able to easily rollback for the time being.
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function down(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
if (hasAuthMethodColumnAccessToken) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("authMethod");
});
}
}
const config = { transaction: false };
export { config };

View File

@@ -20,7 +20,8 @@ export const IdentityAccessTokensSchema = z.object({
identityId: z.string().uuid(), identityId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
name: z.string().nullable().optional() name: z.string().nullable().optional(),
authMethod: z.string()
}); });
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>; export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;

View File

@@ -189,7 +189,7 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod { export enum IdentityAuthMethod {
TOKEN_AUTH = "token-auth", TOKEN_AUTH = "token-auth",
Univeral = "universal-auth", UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth", KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",

View File

@@ -16,7 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
// @ts-ignore // @ts-ignore
id: seedData1.machineIdentity.id, id: seedData1.machineIdentity.id,
name: seedData1.machineIdentity.name, name: seedData1.machineIdentity.name,
authMethod: IdentityAuthMethod.Univeral authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
} }
]); ]);
const identityUa = await knex(TableName.IdentityUniversalAuth) const identityUa = await knex(TableName.IdentityUniversalAuth)

View File

@@ -9,7 +9,7 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -22,6 +22,7 @@ import {
TDeleteDynamicSecretDTO, TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO, TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO, TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO, TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO, TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO TUpdateDynamicSecretDTO
@@ -454,8 +455,44 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: ProjectServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
const userAccessibleFolderMappings = folderMappings.filter(({ path, environment }) =>
permission.can(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath: path })
)
);
const groupedFolderMappings = new Map(userAccessibleFolderMappings.map((path) => [path.folderId, path]));
const dynamicSecrets = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: userAccessibleFolderMappings.map(({ folderId }) => folderId),
...filters
});
return dynamicSecrets.map((dynamicSecret) => {
const { environment, path } = groupedFolderMappings.get(dynamicSecret.folderId)!;
return {
...dynamicSecret,
environment,
path
};
});
};
// get dynamic secrets for multiple envs // get dynamic secrets for multiple envs
const listDynamicSecretsByFolderIds = async ({ const listDynamicSecretsByEnvs = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
@@ -521,9 +558,10 @@ export const dynamicSecretServiceFactory = ({
deleteByName, deleteByName,
getDetails, getDetails,
listDynamicSecretsByEnv, listDynamicSecretsByEnv,
listDynamicSecretsByFolderIds, listDynamicSecretsByEnvs,
getDynamicSecretCount, getDynamicSecretCount,
getCountMultiEnv, getCountMultiEnv,
fetchAzureEntraIdUsers fetchAzureEntraIdUsers,
listDynamicSecretsByFolderIds
}; };
}; };

View File

@@ -48,17 +48,27 @@ export type TDetailsDynamicSecretDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsDTO = { export type ListDynamicSecretsFilters = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
offset?: number; offset?: number;
limit?: number; limit?: number;
orderBy?: SecretsOrderBy; orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection; orderDirection?: OrderByDirection;
search?: string; search?: string;
} & Omit<TProjectPermission, "projectId">; };
export type TListDynamicSecretsDTO = {
path: string;
environmentSlug: string;
projectSlug?: string;
projectId?: string;
} & ListDynamicSecretsFilters &
Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsByFolderMappingsDTO = {
projectId: string;
folderMappings: { folderId: string; path: string; environment: string }[];
filters: ListDynamicSecretsFilters;
};
export type TListDynamicSecretsMultiEnvDTO = Omit< export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO, TListDynamicSecretsDTO,

View File

@@ -1,3 +1,5 @@
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam"; import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id"; import { AzureEntraIDProvider } from "./azure-entra-id";
@@ -24,5 +26,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider() [DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
}); });

View File

@@ -177,6 +177,16 @@ export const DynamicSecretSapHanaSchema = z.object({
ca: z.string().optional() ca: z.string().optional()
}); });
export const DynamicSecretSnowflakeSchema = z.object({
accountId: z.string().trim().min(1),
orgId: z.string().trim().min(1),
username: z.string().trim().min(1),
password: z.string().trim().min(1),
creationStatement: z.string().trim().min(1),
revocationStatement: z.string().trim().min(1),
renewStatement: z.string().trim().optional()
});
export const AzureEntraIDSchema = z.object({ export const AzureEntraIDSchema = z.object({
tenantId: z.string().trim().min(1), tenantId: z.string().trim().min(1),
userId: z.string().trim().min(1), userId: z.string().trim().min(1),
@@ -208,7 +218,8 @@ export enum DynamicSecretProviders {
RabbitMq = "rabbit-mq", RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id", AzureEntraID = "azure-entra-id",
Ldap = "ldap", Ldap = "ldap",
SapHana = "sap-hana" SapHana = "sap-hana",
Snowflake = "snowflake"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -223,7 +234,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }) z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {

View File

@@ -0,0 +1,174 @@
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import snowflake from "snowflake-sdk";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
// destroy client requires callback...
const noop = () => {};
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return `infisical_${alphaNumericNanoId(32)}`; // username must start with alpha character, hence prefix
};
const getDaysToExpiry = (expiryDate: Date) => {
const start = new Date().getTime();
const end = new Date(expiryDate).getTime();
const diffTime = Math.abs(end - start);
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
export const SnowflakeProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
const client = snowflake.createConnection({
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
username: providerInputs.username,
password: providerInputs.password,
application: "Infisical"
});
await client.connectAsync(noop);
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
let isValidConnection: boolean;
try {
isValidConnection = await Promise.race([
client.isValidAsync(),
new Promise((resolve) => {
setTimeout(resolve, 10000);
}).then(() => {
throw new BadRequestError({ message: "Unable to establish connection - verify credentials" });
})
]);
} finally {
client.destroy(noop);
}
return isValidConnection;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
await new Promise((resolve, reject) => {
client.execute({
sqlText: creationStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "CreateLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
await new Promise((resolve, reject) => {
client.execute({
sqlText: revokeStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RevokeLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId: username };
const client = await getClient(providerInputs);
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username,
expiration
});
await new Promise((resolve, reject) => {
client.execute({
sqlText: renewStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RenewLease", message: err.message }));
}
return resolve(true);
}
});
});
} finally {
client.destroy(noop);
}
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -267,7 +267,8 @@ export const secretApprovalRequestServiceFactory = ({
: "", : "",
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "" : "",
tags: el.secretVersion.tags
} }
: undefined : undefined
})); }));
@@ -571,7 +572,7 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote: el.reminderNote, reminderNote: el.reminderNote,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key, key: el.key,
tagIds: el?.tags.map(({ id }) => id), tags: el?.tags.map(({ id }) => id),
...encryptedValue ...encryptedValue
} }
}; };

View File

@@ -85,7 +85,8 @@ export const secretRotationDbFn = async ({
password, password,
username, username,
client, client,
variables variables,
options
}: TSecretRotationDbFn) => { }: TSecretRotationDbFn) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -117,7 +118,8 @@ export const secretRotationDbFn = async ({
password, password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl, ssl,
pool: { min: 0, max: 1 } pool: { min: 0, max: 1 },
options
} }
}); });
const data = await db.raw(query, variables); const data = await db.raw(query, variables);
@@ -153,6 +155,14 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str
variables: [variables.username] variables: [variables.username]
}; };
} }
if (db === TDbProviderClients.MsSqlServer) {
return {
query: `ALTER LOGIN ?? WITH PASSWORD = '${variables.password}'`,
variables: [variables.username]
};
}
// add more based on client // add more based on client
return { return {
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`, query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,

View File

@@ -24,4 +24,5 @@ export type TSecretRotationDbFn = {
query: string; query: string;
variables: unknown[]; variables: unknown[];
ca?: string; ca?: string;
options?: Record<string, unknown>;
}; };

View File

@@ -94,7 +94,9 @@ export const secretRotationQueueFactory = ({
// on prod it this will be in days, in development this will be second // on prod it this will be in days, in development this will be second
every: appCfg.NODE_ENV === "development" ? secondsToMillis(interval) : daysToMillisecond(interval), every: appCfg.NODE_ENV === "development" ? secondsToMillis(interval) : daysToMillisecond(interval),
immediately: true immediately: true
} },
removeOnComplete: true,
removeOnFail: true
} }
); );
}; };
@@ -114,6 +116,7 @@ export const secretRotationQueueFactory = ({
queue.start(QueueName.SecretRotation, async (job) => { queue.start(QueueName.SecretRotation, async (job) => {
const { rotationId } = job.data; const { rotationId } = job.data;
const appCfg = getConfig();
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`); logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
const secretRotation = await secretRotationDAL.findById(rotationId); const secretRotation = await secretRotationDAL.findById(rotationId);
const rotationProvider = rotationTemplates.find(({ name }) => name === secretRotation?.provider); const rotationProvider = rotationTemplates.find(({ name }) => name === secretRotation?.provider);
@@ -172,6 +175,15 @@ export const secretRotationQueueFactory = ({
// set a random value for new password // set a random value for new password
newCredential.internal.rotated_password = alphaNumericNanoId(32); newCredential.internal.rotated_password = alphaNumericNanoId(32);
const { admin_username: username, admin_password: password, host, database, port, ca } = newCredential.inputs; const { admin_username: username, admin_password: password, host, database, port, ca } = newCredential.inputs;
const options =
provider.template.client === TDbProviderClients.MsSqlServer
? ({
encrypt: appCfg.ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT,
cryptoCredentialsDetails: ca ? { ca } : {}
} as Record<string, unknown>)
: undefined;
const dbFunctionArg = { const dbFunctionArg = {
username, username,
password, password,
@@ -179,8 +191,10 @@ export const secretRotationQueueFactory = ({
database, database,
port, port,
ca: ca as string, ca: ca as string,
client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client,
options
} as TSecretRotationDbFn; } as TSecretRotationDbFn;
// set function // set function
await secretRotationDbFn({ await secretRotationDbFn({
...dbFunctionArg, ...dbFunctionArg,
@@ -189,12 +203,17 @@ export const secretRotationQueueFactory = ({
username: newCredential.internal.username as string username: newCredential.internal.username as string
}) })
}); });
// test function // test function
const testQuery =
provider.template.client === TDbProviderClients.MsSqlServer ? "SELECT GETDATE()" : "SELECT NOW()";
await secretRotationDbFn({ await secretRotationDbFn({
...dbFunctionArg, ...dbFunctionArg,
query: "SELECT NOW()", query: testQuery,
variables: [] variables: []
}); });
newCredential.outputs.db_username = newCredential.internal.username; newCredential.outputs.db_username = newCredential.internal.username;
newCredential.outputs.db_password = newCredential.internal.rotated_password; newCredential.outputs.db_password = newCredential.internal.rotated_password;
// clean up // clean up

View File

@@ -1,4 +1,5 @@
import { AWS_IAM_TEMPLATE } from "./aws-iam"; import { AWS_IAM_TEMPLATE } from "./aws-iam";
import { MSSQL_TEMPLATE } from "./mssql";
import { MYSQL_TEMPLATE } from "./mysql"; import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres"; import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid"; import { SENDGRID_TEMPLATE } from "./sendgrid";
@@ -26,6 +27,13 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
description: "Rotate MySQL@7/MariaDB user credentials", description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE template: MYSQL_TEMPLATE
}, },
{
name: "mssql",
title: "Microsoft SQL Server",
image: "mssqlserver.png",
description: "Rotate Microsoft SQL server user credentials",
template: MSSQL_TEMPLATE
},
{ {
name: "aws-iam", name: "aws-iam",
title: "AWS IAM", title: "AWS IAM",

View File

@@ -0,0 +1,33 @@
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
export const MSSQL_TEMPLATE = {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.MsSqlServer,
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const, default: "master" },
port: { type: "integer" as const, default: "1433" },
username1: {
type: "string",
default: "infisical-sql-user1",
desc: "SQL Server login name that must be created at server level with a matching database user"
},
username2: {
type: "string",
default: "infisical-sql-user2",
desc: "SQL Server login name that must be created at server level with a matching database user"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: ["admin_username", "admin_password", "host", "database", "username1", "username2", "port"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
}
};

View File

@@ -8,7 +8,9 @@ export enum TDbProviderClients {
// postgres, cockroack db, amazon red shift // postgres, cockroack db, amazon red shift
Pg = "pg", Pg = "pg",
// mysql and maria db // mysql and maria db
MySql = "mysql" MySql = "mysql",
MsSqlServer = "mssql"
} }
export enum TAwsProviderSystems { export enum TAwsProviderSystems {

View File

@@ -162,7 +162,8 @@ const envSchema = z
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()) WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,

View File

@@ -57,3 +57,10 @@ export enum OrderByDirection {
ASC = "asc", ASC = "asc",
DESC = "desc" DESC = "desc"
} }
export type ProjectServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;
orgId: string;
};

View File

@@ -15,8 +15,12 @@ export const fastifySwagger = fp(async (fastify) => {
}, },
servers: [ servers: [
{ {
url: "https://app.infisical.com", url: "https://us.infisical.com",
description: "Production server" description: "Production server (US)"
},
{
url: "https://eu.infisical.com",
description: "Production server (EU)"
}, },
{ {
url: "http://localhost:8080", url: "http://localhost:8080",

View File

@@ -1087,7 +1087,6 @@ export const registerRoutes = async (
const identityTokenAuthService = identityTokenAuthServiceFactory({ const identityTokenAuthService = identityTokenAuthServiceFactory({
identityTokenAuthDAL, identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -1096,7 +1095,6 @@ export const registerRoutes = async (
const identityUaService = identityUaServiceFactory({ const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL, identityOrgMembershipDAL,
permissionService, permissionService,
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityUaClientSecretDAL, identityUaClientSecretDAL,
identityUaDAL, identityUaDAL,
@@ -1106,7 +1104,6 @@ export const registerRoutes = async (
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
orgBotDAL, orgBotDAL,
permissionService, permissionService,
licenseService licenseService
@@ -1115,7 +1112,6 @@ export const registerRoutes = async (
identityGcpAuthDAL, identityGcpAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -1124,7 +1120,6 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
licenseService, licenseService,
permissionService permissionService
}); });
@@ -1133,7 +1128,6 @@ export const registerRoutes = async (
identityAzureAuthDAL, identityAzureAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -1142,7 +1136,6 @@ export const registerRoutes = async (
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService, licenseService,
orgBotDAL orgBotDAL

View File

@@ -29,6 +29,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
defaultAuthOrgAuthEnforced: z.boolean().nullish(),
defaultAuthOrgAuthMethod: z.string().nullish(),
isSecretScanningDisabled: z.boolean() isSecretScanningDisabled: z.boolean()
}) })
}) })

View File

@@ -20,6 +20,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results
// handle querystring boolean values // handle querystring boolean values
const booleanSchema = z const booleanSchema = z
.union([z.boolean(), z.string().trim()]) .union([z.boolean(), z.string().trim()])
@@ -34,6 +36,35 @@ const booleanSchema = z
.optional() .optional()
.default(true); .default(true);
const parseSecretPathSearch = (search?: string) => {
if (!search)
return {
searchName: "",
searchPath: ""
};
if (!search.includes("/"))
return {
searchName: search,
searchPath: ""
};
if (search === "/")
return {
searchName: "",
searchPath: "/"
};
const [searchName, ...searchPathSegments] = search.split("/").reverse();
let searchPath = removeTrailingSlash(searchPathSegments.reverse().join("/").toLowerCase());
if (!searchPath.startsWith("/")) searchPath = `/${searchPath}`;
return {
searchName,
searchPath
};
};
export const registerDashboardRouter = async (server: FastifyZodProvider) => { export const registerDashboardRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@@ -134,7 +165,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined; let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
let dynamicSecrets: let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>> | Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined; | undefined;
let totalFolderCount: number | undefined; let totalFolderCount: number | undefined;
@@ -218,7 +249,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({ dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnvs({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -633,4 +664,180 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}; };
} }
}); });
server.route({
method: "GET",
url: "/secrets-deep-search",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environments: z.string().trim().transform(decodeURIComponent),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
search: z.string().trim().optional(),
tags: z.string().trim().transform(decodeURIComponent).optional()
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({ path: z.string() }).array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ path: z.string(), environment: z.string() })
.array()
.optional(),
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, search } = req.query;
const environments = req.query.environments.split(",").filter((env) => Boolean(env.trim()));
if (!environments.length) throw new BadRequestError({ message: "One or more environments required" });
const tags = req.query.tags?.split(",").filter((tag) => Boolean(tag.trim())) ?? [];
if (!search && !tags.length) throw new BadRequestError({ message: "Search or tags required" });
const searchHasTags = Boolean(tags.length);
const allFolders = await server.services.folder.getFoldersDeepByEnvs(
{
projectId,
environments,
secretPath
},
req.permission
);
const { searchName, searchPath } = parseSecretPathSearch(search);
const folderMappings = allFolders.map((folder) => ({
folderId: folder.id,
path: folder.path,
environment: folder.environment
}));
const sharedFilters = {
search: searchName,
limit: MAX_DEEP_SEARCH_LIMIT,
orderBy: SecretsOrderBy.Name
};
const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{
projectId,
folderMappings,
filters: {
...sharedFilters,
tagSlugs: tags,
includeTagsInSearch: true
}
},
req.permission
);
const dynamicSecrets = searchHasTags
? []
: await server.services.dynamicSecret.listDynamicSecretsByFolderIds(
{
projectId,
folderMappings,
filters: sharedFilters
},
req.permission
);
for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountForEnv
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountForEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
const sliceQuickSearch = <T>(array: T[]) => array.slice(0, 25);
return {
secrets: sliceQuickSearch(
searchPath ? secrets.filter((secret) => secret.secretPath.endsWith(searchPath)) : secrets
),
dynamicSecrets: sliceQuickSearch(
searchPath
? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath))
: dynamicSecrets
),
folders: searchHasTags
? []
: sliceQuickSearch(
allFolders.filter((folder) => {
const [folderName, ...folderPathSegments] = folder.path.split("/").reverse();
const folderPath = folderPathSegments.reverse().join("/").toLowerCase() || "/";
if (searchPath) {
if (searchPath === "/") {
// only show root folders if no folder name search
if (!searchName) return folderPath === searchPath;
// start partial match on root folders
return folderName.toLowerCase().startsWith(searchName.toLowerCase());
}
// support ending partial path match
return (
folderPath.endsWith(searchPath) && folderName.toLowerCase().startsWith(searchName.toLowerCase())
);
}
// no search path, "fuzzy" match all folders
return folderName.toLowerCase().includes(searchName.toLowerCase());
})
)
};
}
});
}; };

View File

@@ -37,7 +37,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema identity: IdentitiesSchema.extend({
authMethods: z.array(z.string())
})
}) })
} }
}, },
@@ -216,7 +218,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}) })
}) })
} }
@@ -261,7 +265,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}).array(), }).array(),
totalCount: z.number() totalCount: z.number()
}) })
@@ -319,7 +325,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
) )

View File

@@ -58,7 +58,9 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}) })
).array(), ).array(),
totalCount: z.number() totalCount: z.number()

View File

@@ -264,7 +264,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
.array(), .array(),
@@ -285,6 +287,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
orderDirection: req.query.orderDirection, orderDirection: req.query.orderDirection,
search: req.query.search search: req.query.search
}); });
return { identityMemberships, totalCount }; return { identityMemberships, totalCount };
} }
}); });
@@ -328,7 +331,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
}) })

View File

@@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ProjectServiceActor } from "@app/lib/types";
import { import {
TCmekDecryptDTO, TCmekDecryptDTO,
TCmekEncryptDTO, TCmekEncryptDTO,
@@ -23,7 +23,7 @@ type TCmekServiceFactoryDep = {
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>; export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => { export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => { const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: ProjectServiceActor) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -43,7 +43,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek; return cmek;
}; };
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => { const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId); const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -65,7 +65,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek; return cmek;
}; };
const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => { const deleteCmekById = async (keyId: string, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId); const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -89,7 +89,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
const listCmeksByProjectId = async ( const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO, { projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: FastifyRequest["permission"] actor: ProjectServiceActor
) => { ) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor.type, actor.type,
@@ -106,7 +106,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return { cmeks, totalCount }; return { cmeks, totalCount };
}; };
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => { const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId); const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -132,7 +132,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cipherTextBlob.toString("base64"); return cipherTextBlob.toString("base64");
}; };
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => { const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: ProjectServiceActor) => {
const key = await kmsDAL.findById(keyId); const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` }); if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });

View File

@@ -1,9 +1,9 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectServiceActor } from "@app/lib/types";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
@@ -25,7 +25,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
permissionService, permissionService,
orgRoleDAL orgRoleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => { }: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => { const listExternalGroupOrgRoleMappings = async (actor: ProjectServiceActor) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -46,7 +46,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
const updateExternalGroupOrgRoleMappings = async ( const updateExternalGroupOrgRoleMappings = async (
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO, dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
actor: FastifyRequest["permission"] actor: ProjectServiceActor
) => { ) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,

View File

@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -17,54 +17,27 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken) const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken)
.where(filter) .where(filter)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.leftJoin(TableName.IdentityUaClientSecret, (qb) => { .leftJoin(
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( TableName.IdentityUaClientSecret,
`${TableName.IdentityAccessToken}.identityUAClientSecretId`, `${TableName.IdentityAccessToken}.identityUAClientSecretId`,
`${TableName.IdentityUaClientSecret}.id` `${TableName.IdentityUaClientSecret}.id`
); )
}) .leftJoin(
.leftJoin(TableName.IdentityUniversalAuth, (qb) => { TableName.IdentityUniversalAuth,
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( `${TableName.IdentityUaClientSecret}.identityUAId`,
`${TableName.IdentityUaClientSecret}.identityUAId`, `${TableName.IdentityUniversalAuth}.id`
`${TableName.IdentityUniversalAuth}.id` )
); .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
}) .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
.leftJoin(TableName.IdentityGcpAuth, (qb) => { .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.GCP_AUTH])).andOn( .leftJoin(
`${TableName.Identity}.id`, TableName.IdentityKubernetesAuth,
`${TableName.IdentityGcpAuth}.identityId` `${TableName.Identity}.id`,
); `${TableName.IdentityKubernetesAuth}.identityId`
}) )
.leftJoin(TableName.IdentityAwsAuth, (qb) => { .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AWS_AUTH])).andOn( .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
`${TableName.Identity}.id`,
`${TableName.IdentityAwsAuth}.identityId`
);
})
.leftJoin(TableName.IdentityAzureAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AZURE_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityAzureAuth}.identityId`
);
})
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
);
})
.leftJoin(TableName.IdentityOidcAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.OIDC_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityOidcAuth}.identityId`
);
})
.leftJoin(TableName.IdentityTokenAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.TOKEN_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityTokenAuth}.identityId`
);
})
.select(selectAllTableCols(TableName.IdentityAccessToken)) .select(selectAllTableCols(TableName.IdentityAccessToken))
.select( .select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
@@ -82,14 +55,13 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
return { return {
...doc, ...doc,
accessTokenTrustedIps: trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
doc.accessTokenTrustedIpsUa || trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
doc.accessTokenTrustedIpsGcp || trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
doc.accessTokenTrustedIpsAws || trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
doc.accessTokenTrustedIpsAzure || trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
doc.accessTokenTrustedIpsK8s || trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
doc.accessTokenTrustedIpsOidc || trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
doc.accessTokenTrustedIpsToken
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });

View File

@@ -1,6 +1,6 @@
import jwt, { JwtPayload } from "jsonwebtoken"; import jwt, { JwtPayload } from "jsonwebtoken";
import { TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
@@ -164,10 +164,22 @@ export const identityAccessTokenServiceFactory = ({
message: "Failed to authorize revoked access token, access token is revoked" message: "Failed to authorize revoked access token, access token is revoked"
}); });
if (ipAddress && identityAccessToken) { const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
if (ipAddress) {
checkIPAgainstBlocklist({ checkIPAgainstBlocklist({
ipAddress, ipAddress,
trustedIps: identityAccessToken?.accessTokenTrustedIps as TIp[] trustedIps: trustedIps as TIp[]
}); });
} }

View File

@@ -13,7 +13,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -33,7 +32,6 @@ type TIdentityAwsAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">; identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
}; };
@@ -44,7 +42,6 @@ export const identityAwsAuthServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
licenseService, licenseService,
permissionService permissionService
}: TIdentityAwsAuthServiceFactoryDep) => { }: TIdentityAwsAuthServiceFactoryDep) => {
@@ -113,7 +110,8 @@ export const identityAwsAuthServiceFactory = ({
accessTokenTTL: identityAwsAuth.accessTokenTTL, accessTokenTTL: identityAwsAuth.accessTokenTTL,
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AWS_AUTH
}, },
tx tx
); );
@@ -155,10 +153,12 @@ export const identityAwsAuthServiceFactory = ({
}: TAttachAwsAuthDTO) => { }: TAttachAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add AWS Auth to already configured identity" message: "Failed to add AWS Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -206,13 +206,6 @@ export const identityAwsAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.AWS_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId }; return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId };
@@ -234,10 +227,12 @@ export const identityAwsAuthServiceFactory = ({
}: TUpdateAwsAuthDTO) => { }: TUpdateAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
throw new BadRequestError({ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
message: "Failed to update AWS Auth" throw new NotFoundError({
message: "The identity does not have AWS Auth attached"
}); });
}
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
@@ -293,10 +288,12 @@ export const identityAwsAuthServiceFactory = ({
const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => { const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have AWS Auth attached" message: "The identity does not have AWS Auth attached"
}); });
}
const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId });
@@ -320,10 +317,11 @@ export const identityAwsAuthServiceFactory = ({
}: TRevokeAwsAuthDTO) => { }: TRevokeAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have aws auth" message: "The identity does not have aws auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -348,7 +346,6 @@ export const identityAwsAuthServiceFactory = ({
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityAwsAuth; return revokedIdentityAwsAuth;

View File

@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -32,7 +31,6 @@ type TIdentityAzureAuthServiceFactoryDep = {
>; >;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -43,7 +41,6 @@ export const identityAzureAuthServiceFactory = ({
identityAzureAuthDAL, identityAzureAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityAzureAuthServiceFactoryDep) => { }: TIdentityAzureAuthServiceFactoryDep) => {
@@ -84,7 +81,8 @@ export const identityAzureAuthServiceFactory = ({
accessTokenTTL: identityAzureAuth.accessTokenTTL, accessTokenTTL: identityAzureAuth.accessTokenTTL,
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AZURE_AUTH
}, },
tx tx
); );
@@ -126,11 +124,12 @@ export const identityAzureAuthServiceFactory = ({
}: TAttachAzureAuthDTO) => { }: TAttachAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Azure Auth to already configured identity" message: "Failed to add Azure Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
} }
@@ -176,13 +175,7 @@ export const identityAzureAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.AZURE_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
@@ -204,10 +197,11 @@ export const identityAzureAuthServiceFactory = ({
}: TUpdateAzureAuthDTO) => { }: TUpdateAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Azure Auth" message: "Failed to update Azure Auth"
}); });
}
const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId });
@@ -266,10 +260,11 @@ export const identityAzureAuthServiceFactory = ({
const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => { const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Azure Auth attached" message: "The identity does not have Azure Auth attached"
}); });
}
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
@@ -294,10 +289,11 @@ export const identityAzureAuthServiceFactory = ({
}: TRevokeAzureAuthDTO) => { }: TRevokeAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have azure auth" message: "The identity does not have azure auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -321,7 +317,6 @@ export const identityAzureAuthServiceFactory = ({
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityAzureAuth; return revokedIdentityAzureAuth;

View File

@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -30,7 +29,6 @@ type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">; identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -41,7 +39,6 @@ export const identityGcpAuthServiceFactory = ({
identityGcpAuthDAL, identityGcpAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityGcpAuthServiceFactoryDep) => { }: TIdentityGcpAuthServiceFactoryDep) => {
@@ -125,7 +122,8 @@ export const identityGcpAuthServiceFactory = ({
accessTokenTTL: identityGcpAuth.accessTokenTTL, accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.GCP_AUTH
}, },
tx tx
); );
@@ -168,10 +166,12 @@ export const identityGcpAuthServiceFactory = ({
}: TAttachGcpAuthDTO) => { }: TAttachGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add GCP Auth to already configured identity" message: "Failed to add GCP Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -219,13 +219,6 @@ export const identityGcpAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
@@ -248,10 +241,12 @@ export const identityGcpAuthServiceFactory = ({
}: TUpdateGcpAuthDTO) => { }: TUpdateGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update GCP Auth" message: "Failed to update GCP Auth"
}); });
}
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
@@ -311,10 +306,12 @@ export const identityGcpAuthServiceFactory = ({
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => { const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have GCP Auth attached" message: "The identity does not have GCP Auth attached"
}); });
}
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
@@ -339,10 +336,12 @@ export const identityGcpAuthServiceFactory = ({
}: TRevokeGcpAuthDTO) => { }: TRevokeGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have gcp auth" message: "The identity does not have gcp auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -366,7 +365,6 @@ export const identityGcpAuthServiceFactory = ({
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityGcpAuth; return revokedIdentityGcpAuth;

View File

@@ -22,7 +22,6 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -44,7 +43,6 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
>; >;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -56,7 +54,6 @@ export const identityKubernetesAuthServiceFactory = ({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
orgBotDAL, orgBotDAL,
permissionService, permissionService,
licenseService licenseService
@@ -215,7 +212,8 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenTTL: identityKubernetesAuth.accessTokenTTL, accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
}, },
tx tx
); );
@@ -260,10 +258,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TAttachKubernetesAuthDTO) => { }: TAttachKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Kubernetes Auth to already configured identity" message: "Failed to add Kubernetes Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -372,13 +372,6 @@ export const identityKubernetesAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
);
return doc; return doc;
}); });
@@ -404,10 +397,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TUpdateKubernetesAuthDTO) => { }: TUpdateKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Kubernetes Auth" message: "Failed to update Kubernetes Auth"
}); });
}
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
@@ -532,11 +527,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TGetKubernetesAuthDTO) => { }: TGetKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Kubernetes Auth attached" message: "The identity does not have Kubernetes Auth attached"
}); });
}
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -597,10 +593,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TRevokeKubernetesAuthDTO) => { }: TRevokeKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have kubernetes auth" message: "The identity does not have kubernetes auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -624,7 +622,6 @@ export const identityKubernetesAuthServiceFactory = ({
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityKubernetesAuth; return revokedIdentityKubernetesAuth;

View File

@@ -22,7 +22,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -41,7 +40,6 @@ type TIdentityOidcAuthServiceFactoryDep = {
identityOidcAuthDAL: TIdentityOidcAuthDALFactory; identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
@@ -52,7 +50,6 @@ export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuth
export const identityOidcAuthServiceFactory = ({ export const identityOidcAuthServiceFactory = ({
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
permissionService, permissionService,
licenseService, licenseService,
identityAccessTokenDAL, identityAccessTokenDAL,
@@ -61,7 +58,7 @@ export const identityOidcAuthServiceFactory = ({
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) { if (!identityOidcAuth) {
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
} }
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
@@ -181,7 +178,8 @@ export const identityOidcAuthServiceFactory = ({
accessTokenTTL: identityOidcAuth.accessTokenTTL, accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OIDC_AUTH
}, },
tx tx
); );
@@ -228,10 +226,11 @@ export const identityOidcAuthServiceFactory = ({
if (!identityMembershipOrg) { if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
} }
if (identityMembershipOrg.identity.authMethod) if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add OIDC Auth to already configured identity" message: "Failed to add OIDC Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -334,13 +333,6 @@ export const identityOidcAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.OIDC_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
@@ -364,11 +356,9 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
}: TUpdateOidcAuthDTO) => { }: TUpdateOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update OIDC Auth" message: "Failed to update OIDC Auth"
}); });
@@ -467,11 +457,9 @@ export const identityOidcAuthServiceFactory = ({
const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => { const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have OIDC Auth attached" message: "The identity does not have OIDC Auth attached"
}); });
@@ -519,7 +507,7 @@ export const identityOidcAuthServiceFactory = ({
throw new NotFoundError({ message: "Failed to find identity" }); throw new NotFoundError({ message: "Failed to find identity" });
} }
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have OIDC auth" message: "The identity does not have OIDC auth"
}); });
@@ -551,7 +539,6 @@ export const identityOidcAuthServiceFactory = ({
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });

View File

@@ -1,12 +1,24 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas"; import {
TableName,
TIdentities,
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types"; import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
import { buildAuthMethods } from "../identity/identity-fns";
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>; export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
export const identityProjectDALFactory = (db: TDbClient) => { export const identityProjectDALFactory = (db: TDbClient) => {
@@ -33,11 +45,48 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.id`, `${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
) )
.leftJoin(
TableName.IdentityUniversalAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin(
TableName.IdentityGcpAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin(
TableName.IdentityAwsAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin(
TableName.IdentityKubernetesAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(
TableName.IdentityOidcAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin(
TableName.IdentityAzureAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin(
TableName.IdentityTokenAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
@@ -52,12 +101,33 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership), db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
); );
const members = sqlNestRelationships({ const members = sqlNestRelationships({
data: docs, data: docs,
parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({ parentMapper: ({
identityName,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
id,
createdAt,
updatedAt,
projectId,
projectName
}) => ({
id, id,
identityId, identityId,
createdAt, createdAt,
@@ -65,7 +135,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
}, },
project: { project: {
id: projectId, id: projectId,
@@ -150,7 +228,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
}) })
.where((qb) => { .where((qb) => {
if (filter.identityId) { if (filter.identityId) {
void qb.where("identityId", filter.identityId); void qb.where(`${TableName.IdentityProjectMembership}.identityId`, filter.identityId);
} }
}) })
.join( .join(
@@ -168,6 +246,43 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.id`, `${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
) )
.leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
@@ -186,7 +301,14 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
); );
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point // TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
@@ -204,7 +326,21 @@ export const identityProjectDALFactory = (db: TDbClient) => {
const members = sqlNestRelationships({ const members = sqlNestRelationships({
data: docs, data: docs,
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({ parentMapper: ({
identityId,
identityName,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
id,
createdAt,
updatedAt,
projectName
}) => ({
id, id,
identityId, identityId,
createdAt, createdAt,
@@ -212,7 +348,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
}, },
project: { project: {
id: projectId, id: projectId,

View File

@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -32,11 +31,10 @@ type TIdentityTokenAuthServiceFactoryDep = {
TIdentityTokenAuthDALFactory, TIdentityTokenAuthDALFactory,
"transaction" | "create" | "findOne" | "updateById" | "delete" "transaction" | "create" | "findOne" | "updateById" | "delete"
>; >;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick< identityAccessTokenDAL: Pick<
TIdentityAccessTokenDALFactory, TIdentityAccessTokenDALFactory,
"create" | "find" | "update" | "findById" | "findOne" | "updateById" "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete"
>; >;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -46,7 +44,7 @@ export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAu
export const identityTokenAuthServiceFactory = ({ export const identityTokenAuthServiceFactory = ({
identityTokenAuthDAL, identityTokenAuthDAL,
identityDAL, // identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -65,10 +63,12 @@ export const identityTokenAuthServiceFactory = ({
}: TAttachTokenAuthDTO) => { }: TAttachTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Token Auth to already configured identity" message: "Failed to add Token Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -112,13 +112,6 @@ export const identityTokenAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
@@ -137,10 +130,12 @@ export const identityTokenAuthServiceFactory = ({
}: TUpdateTokenAuthDTO) => { }: TUpdateTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Token Auth" message: "The identity does not have token auth"
}); });
}
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@@ -197,10 +192,12 @@ export const identityTokenAuthServiceFactory = ({
const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => { const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth attached" message: "The identity does not have Token Auth attached"
}); });
}
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@@ -225,10 +222,12 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthDTO) => { }: TRevokeTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -254,7 +253,11 @@ export const identityTokenAuthServiceFactory = ({
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx); await identityAccessTokenDAL.delete({
identityId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
});
return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityTokenAuth; return revokedIdentityTokenAuth;
@@ -270,10 +273,12 @@ export const identityTokenAuthServiceFactory = ({
}: TCreateTokenAuthTokenDTO) => { }: TCreateTokenAuthTokenDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -307,7 +312,8 @@ export const identityTokenAuthServiceFactory = ({
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
name name,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}, },
tx tx
); );
@@ -344,10 +350,12 @@ export const identityTokenAuthServiceFactory = ({
}: TGetTokenAuthTokensDTO) => { }: TGetTokenAuthTokensDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -359,7 +367,8 @@ export const identityTokenAuthServiceFactory = ({
const tokens = await identityAccessTokenDAL.find( const tokens = await identityAccessTokenDAL.find(
{ {
identityId identityId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}, },
{ offset, limit, sort: [["updatedAt", "desc"]] } { offset, limit, sort: [["updatedAt", "desc"]] }
); );
@@ -375,16 +384,21 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TUpdateTokenAuthTokenDTO) => { }: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findById(tokenId); const foundToken = await identityAccessTokenDAL.findOne({
id: tokenId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
});
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) {
throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` });
} }
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -409,6 +423,7 @@ export const identityTokenAuthServiceFactory = ({
const [token] = await identityAccessTokenDAL.update( const [token] = await identityAccessTokenDAL.update(
{ {
authMethod: IdentityAuthMethod.TOKEN_AUTH,
identityId: foundToken.identityId, identityId: foundToken.identityId,
id: tokenId id: tokenId
}, },
@@ -429,7 +444,8 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthTokenDTO) => { }: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({ const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false isAccessTokenRevoked: false,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}); });
if (!identityAccessToken) if (!identityAccessToken)
throw new NotFoundError({ throw new NotFoundError({
@@ -453,9 +469,15 @@ export const identityTokenAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { const [revokedToken] = await identityAccessTokenDAL.update(
isAccessTokenRevoked: true {
}); id: identityAccessToken.id,
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
{
isAccessTokenRevoked: true
}
);
return { revokedToken }; return { revokedToken };
}; };

View File

@@ -14,7 +14,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -36,7 +35,6 @@ type TIdentityUaServiceFactoryDep = {
identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory;
identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -48,7 +46,6 @@ export const identityUaServiceFactory = ({
identityUaClientSecretDAL, identityUaClientSecretDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityUaServiceFactoryDep) => { }: TIdentityUaServiceFactoryDep) => {
@@ -115,7 +112,8 @@ export const identityUaServiceFactory = ({
accessTokenTTL: identityUa.accessTokenTTL, accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL, accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
}, },
tx tx
); );
@@ -156,10 +154,12 @@ export const identityUaServiceFactory = ({
}: TAttachUaDTO) => { }: TAttachUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add universal auth to already configured identity" message: "Failed to add universal auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -221,13 +221,6 @@ export const identityUaServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.Univeral
},
tx
);
return doc; return doc;
}); });
return { ...identityUa, orgId: identityMembershipOrg.orgId }; return { ...identityUa, orgId: identityMembershipOrg.orgId };
@@ -247,10 +240,12 @@ export const identityUaServiceFactory = ({
}: TUpdateUaDTO) => { }: TUpdateUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to updated universal auth" message: "The identity does not have universal auth"
}); });
}
const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
@@ -321,10 +316,12 @@ export const identityUaServiceFactory = ({
const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
@@ -348,10 +345,12 @@ export const identityUaServiceFactory = ({
}: TRevokeUaDTO) => { }: TRevokeUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -375,7 +374,6 @@ export const identityUaServiceFactory = ({
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityUniversalAuth; return revokedIdentityUniversalAuth;
@@ -393,10 +391,13 @@ export const identityUaServiceFactory = ({
}: TCreateUaClientSecretDTO) => { }: TCreateUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -422,12 +423,11 @@ export const identityUaServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecret = crypto.randomBytes(32).toString("hex");
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS); const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
const identityUniversalAuth = await identityUaDAL.findOne({
identityId const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId });
});
const identityUaClientSecret = await identityUaClientSecretDAL.create({ const identityUaClientSecret = await identityUaClientSecretDAL.create({
identityUAId: identityUniversalAuth.id, identityUAId: identityUaAuth.id,
description, description,
clientSecretPrefix: clientSecret.slice(0, 4), clientSecretPrefix: clientSecret.slice(0, 4),
clientSecretHash, clientSecretHash,
@@ -439,7 +439,6 @@ export const identityUaServiceFactory = ({
return { return {
clientSecret, clientSecret,
clientSecretData: identityUaClientSecret, clientSecretData: identityUaClientSecret,
uaAuth: identityUniversalAuth,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
}; };
}; };
@@ -453,10 +452,12 @@ export const identityUaServiceFactory = ({
}: TGetUaClientSecretsDTO) => { }: TGetUaClientSecretsDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -500,10 +501,13 @@ export const identityUaServiceFactory = ({
}: TGetUniversalAuthClientSecretByIdDTO) => { }: TGetUniversalAuthClientSecretByIdDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -539,10 +543,13 @@ export const identityUaServiceFactory = ({
}: TRevokeUaClientSecretDTO) => { }: TRevokeUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,

View File

@@ -0,0 +1,29 @@
import { IdentityAuthMethod } from "@app/db/schemas";
export const buildAuthMethods = ({
uaId,
gcpId,
awsId,
kubernetesId,
oidcId,
azureId,
tokenId
}: {
uaId?: string;
gcpId?: string;
awsId?: string;
kubernetesId?: string;
oidcId?: string;
azureId?: string;
tokenId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};

View File

@@ -1,12 +1,25 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentityOrgMemberships, TOrgRoles } from "@app/db/schemas"; import {
TableName,
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
TIdentityTokenAuths,
TIdentityUniversalAuths,
TOrgRoles
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
import { buildAuthMethods } from "./identity-fns";
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>; export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
export const identityOrgDALFactory = (db: TDbClient) => { export const identityOrgDALFactory = (db: TDbClient) => {
@@ -15,14 +28,73 @@ export const identityOrgDALFactory = (db: TDbClient) => {
const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => { const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
try { try {
const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter) .where((queryBuilder) => {
Object.entries(filter).forEach(([key, value]) => {
void queryBuilder.where(`${TableName.IdentityOrgMembership}.${key}`, value);
});
})
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.select(selectAllTableCols(TableName.IdentityOrgMembership))
.select(db.ref("name").withSchema(TableName.Identity)) .leftJoin<TIdentityUniversalAuths>(
.select(db.ref("authMethod").withSchema(TableName.Identity)); TableName.IdentityUniversalAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("name").withSchema(TableName.Identity)
);
if (data) { if (data) {
const { name, authMethod } = data; const { name } = data;
return { ...data, identity: { id: data.identityId, name, authMethod } }; return {
...data,
identity: {
id: data.identityId,
name,
authMethods: buildAuthMethods(data)
}
};
} }
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindOne" }); throw new DatabaseError({ error, name: "FindOne" });
@@ -51,8 +123,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select( .select(
selectAllTableCols(TableName.IdentityOrgMembership), selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("name").withSchema(TableName.Identity).as("identityName")
db.ref("authMethod").withSchema(TableName.Identity).as("identityAuthMethod")
) )
.where(filter) .where(filter)
.as("paginatedIdentity"); .as("paginatedIdentity");
@@ -70,11 +141,49 @@ export const identityOrgDALFactory = (db: TDbClient) => {
const query = (tx || db.replicaNode()) const query = (tx || db.replicaNode())
.from<TSubquery[number], TSubquery>(paginatedIdentity) .from<TSubquery[number], TSubquery>(paginatedIdentity)
.leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`) .leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`) .on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`); .andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema("paginatedIdentity"), db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"),
@@ -82,9 +191,16 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("orgId").withSchema("paginatedIdentity"), db.ref("orgId").withSchema("paginatedIdentity"),
db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"),
db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
db.ref("identityName").withSchema("paginatedIdentity"), db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("identityAuthMethod").withSchema("paginatedIdentity")
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
) )
// cr stands for custom role // cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -114,11 +230,17 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
identityAuthMethod,
role, role,
roleId, roleId,
id, id,
orgId, orgId,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
createdAt, createdAt,
updatedAt updatedAt
}) => ({ }) => ({
@@ -126,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
roleId, roleId,
identityId, identityId,
id, id,
orgId, orgId,
createdAt, createdAt,
updatedAt, updatedAt,
@@ -141,7 +264,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod as string authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
} }
}), }),
childrenMapper: [ childrenMapper: [

View File

@@ -93,7 +93,7 @@ export const identityServiceFactory = ({
tx tx
); );
} }
return newIdentity; return { ...newIdentity, authMethods: [] };
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);

View File

@@ -2540,9 +2540,9 @@ const syncSecretsAzureDevops = async ({
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name); const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
const variables: Record<string, { value: string }> = {}; const variables: Record<string, { value: string; isSecret: boolean }> = {};
for (const key of Object.keys(secrets)) { for (const key of Object.keys(secrets)) {
variables[key] = { value: secrets[key].value }; variables[key] = { value: secrets[key].value, isSecret: true };
} }
if (!groupId) { if (!groupId) {

View File

@@ -291,7 +291,7 @@ export const orgServiceFactory = ({
} }
if (authEnforced !== undefined) { if (authEnforced !== undefined) {
if (!plan?.samlSSO || !plan.oidcSSO) if (!plan?.samlSSO && !plan.oidcSSO)
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO." message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO."
}); });

View File

@@ -8,6 +8,8 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
export const validateFolderName = (folderName: string) => { export const validateFolderName = (folderName: string) => {
const validNameRegex = /^[a-zA-Z0-9-_]+$/; const validNameRegex = /^[a-zA-Z0-9-_]+$/;
return validNameRegex.test(folderName); return validNameRegex.test(folderName);
@@ -444,6 +446,48 @@ export const secretFolderDALFactory = (db: TDbClient) => {
} }
}; };
const findByEnvsDeep = async ({ parentIds }: TFindFoldersDeepByParentIdsDTO, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())
.withRecursive("parents", (qb) =>
qb
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("0 as depth"),
db.raw(`'/' as path`),
db.ref(`${TableName.Environment}.slug`).as("environment")
)
.from(TableName.SecretFolder)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereIn(`${TableName.SecretFolder}.id`, parentIds)
.union((un) => {
void un
.select(
selectAllTableCols(TableName.SecretFolder),
db.raw("parents.depth + 1 as depth"),
db.raw(
`CONCAT(
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
)`
),
db.ref("parents.environment")
)
.from(TableName.SecretFolder)
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
})
)
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
.from("parents")
.orderBy("depth")
.orderBy(`name`);
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "FindByEnvsDeep" });
}
};
return { return {
...secretFolderOrm, ...secretFolderOrm,
update, update,
@@ -454,6 +498,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
findSecretPathByFolderIds, findSecretPathByFolderIds,
findClosestFolder, findClosestFolder,
findByProjectId, findByProjectId,
findByMultiEnv findByMultiEnv,
findByEnvsDeep
}; };
}; };

View File

@@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -17,6 +17,7 @@ import {
TDeleteFolderDTO, TDeleteFolderDTO,
TGetFolderByIdDTO, TGetFolderByIdDTO,
TGetFolderDTO, TGetFolderDTO,
TGetFoldersDeepByEnvsDTO,
TUpdateFolderDTO, TUpdateFolderDTO,
TUpdateManyFoldersDTO TUpdateManyFoldersDTO
} from "./secret-folder-types"; } from "./secret-folder-types";
@@ -511,6 +512,30 @@ export const secretFolderServiceFactory = ({
}; };
}; };
const getFoldersDeepByEnvs = async (
{ projectId, environments, secretPath }: TGetFoldersDeepByEnvsDTO,
actor: ProjectServiceActor
) => {
// folder list is allowed to be read by anyone
// permission to check does user have access
await permissionService.getProjectPermission(actor.type, actor.id, projectId, actor.authMethod, actor.orgId);
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
if (!envs.length)
throw new NotFoundError({
message: `Environments '${environments.join(", ")}' not found`,
name: "GetFoldersDeep"
});
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!parentFolders.length) return [];
const folders = await folderDAL.findByEnvsDeep({ parentIds: parentFolders.map((parent) => parent.id) });
return folders;
};
return { return {
createFolder, createFolder,
updateFolder, updateFolder,
@@ -519,6 +544,7 @@ export const secretFolderServiceFactory = ({
getFolders, getFolders,
getFolderById, getFolderById,
getProjectFolderCount, getProjectFolderCount,
getFoldersMultiEnv getFoldersMultiEnv,
getFoldersDeepByEnvs
}; };
}; };

View File

@@ -47,3 +47,13 @@ export type TGetFolderDTO = {
export type TGetFolderByIdDTO = { export type TGetFolderByIdDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetFoldersDeepByEnvsDTO = {
projectId: string;
environments: string[];
secretPath: string;
};
export type TFindFoldersDeepByParentIdsDTO = {
parentIds: string[];
};

View File

@@ -272,11 +272,11 @@ export const secretSharingServiceFactory = ({
? await secretSharingDAL.findById(sharedSecretId) ? await secretSharingDAL.findById(sharedSecretId)
: await secretSharingDAL.findOne({ identifier: sharedSecretId }); : await secretSharingDAL.findOne({ identifier: sharedSecretId });
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" }); throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
return deletedSharedSecret; return deletedSharedSecret;
}; };

View File

@@ -14,6 +14,7 @@ import {
} from "@app/lib/knex"; } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>; export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
@@ -339,14 +340,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
folderIds: string[], folderIds: string[],
userId?: string, userId?: string,
tx?: Knex, tx?: Knex,
filters?: { filters?: TFindSecretsByFolderIdsFilter
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
}
) => { ) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
@@ -356,14 +350,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
const query = (tx || db.replicaNode())(TableName.SecretV2) const query = (tx || db.replicaNode())(TableName.SecretV2)
.whereIn("folderId", folderIds) .whereIn(`${TableName.SecretV2}.folderId`, folderIds)
.where((bd) => { .where((bd) => {
if (filters?.search) { if (filters?.search) {
void bd.whereILike("key", `%${filters?.search}%`); if (filters?.includeTagsInSearch) {
void bd
.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`)
.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`);
} else {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
}
} }
}) })
.where((bd) => { .where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null }); void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
}) })
.leftJoin( .leftJoin(
TableName.SecretV2JnTag, TableName.SecretV2JnTag,
@@ -385,7 +385,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where((bd) => { .where((bd) => {
const slugs = filters?.tagSlugs?.filter(Boolean); const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) { if (slugs && slugs.length > 0) {
void bd.whereIn("slug", slugs); void bd.whereIn(`${TableName.SecretTag}.slug`, slugs);
} }
}) })
.orderBy( .orderBy(

View File

@@ -43,6 +43,7 @@ import {
TGetASecretDTO, TGetASecretDTO,
TGetSecretReferencesTreeDTO, TGetSecretReferencesTreeDTO,
TGetSecretsDTO, TGetSecretsDTO,
TGetSecretsRawByFolderMappingsDTO,
TGetSecretVersionsDTO, TGetSecretVersionsDTO,
TMoveSecretsDTO, TMoveSecretsDTO,
TSecretReference, TSecretReference,
@@ -652,6 +653,56 @@ export const secretV2BridgeServiceFactory = ({
return count; return count;
}; };
const getSecretsByFolderMappings = async (
{ projectId, userId, filters, folderMappings }: TGetSecretsRawByFolderMappingsDTO,
projectPermission: Awaited<ReturnType<typeof permissionService.getProjectPermission>>["permission"]
) => {
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
const secrets = await secretDAL.findByFolderIds(
folderMappings.map((folderMapping) => folderMapping.folderId),
userId,
undefined,
filters
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) =>
projectPermission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: groupedFolderMappings[el.folderId][0].environment,
secretPath: groupedFolderMappings[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
)
)
.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedFolderMappings[secret.folderId][0].environment,
groupedFolderMappings[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets;
};
// get secrets for multiple envs // get secrets for multiple envs
const getSecretsMultiEnv = async ({ const getSecretsMultiEnv = async ({
actorId, actorId,
@@ -678,59 +729,28 @@ export const secretV2BridgeServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
} }
let paths: { folderId: string; path: string; environment: string }[] = [];
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
if (!folders.length) { if (!folders.length) {
return []; return [];
} }
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug })); const folderMappings = folders.map((folder) => ({
folderId: folder.id,
path,
environment: folder.environment.slug
}));
const groupedPaths = groupBy(paths, (p) => p.folderId); const decryptedSecrets = await getSecretsByFolderMappings(
{
const secrets = await secretDAL.findByFolderIds( projectId,
paths.map((p) => p.folderId), folderMappings,
actorId, filters: params,
undefined, userId: actorId
params },
permission
); );
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedSecrets = secrets
.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: groupedPaths[el.folderId][0].environment,
secretPath: groupedPaths[el.folderId][0].path,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
)
)
.map((secret) =>
reshapeBridgeSecret(
projectId,
groupedPaths[secret.folderId][0].environment,
groupedPaths[secret.folderId][0].path,
{
...secret,
value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "",
comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: ""
}
)
);
return decryptedSecrets; return decryptedSecrets;
}; };
@@ -2027,6 +2047,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsCount, getSecretsCount,
getSecretsCountMultiEnv, getSecretsCountMultiEnv,
getSecretsMultiEnv, getSecretsMultiEnv,
getSecretReferenceTree getSecretReferenceTree,
getSecretsByFolderMappings
}; };
}; };

View File

@@ -285,3 +285,20 @@ export type TGetSecretReferencesTreeDTO = {
environment: string; environment: string;
secretPath: string; secretPath: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TFindSecretsByFolderIdsFilter = {
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
includeTagsInSearch?: boolean;
};
export type TGetSecretsRawByFolderMappingsDTO = {
projectId: string;
folderMappings: { folderId: string; path: string; environment: string }[];
userId: string;
filters: TFindSecretsByFolderIdsFilter;
};

View File

@@ -27,6 +27,8 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { groupBy, pick } from "@app/lib/fn"; import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectServiceActor } from "@app/lib/types";
import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -2845,6 +2847,27 @@ export const secretServiceFactory = ({
return { message: "Migrating project to new KMS architecture" }; return { message: "Migrating project to new KMS architecture" };
}; };
const getSecretsRawByFolderMappings = async (
params: Omit<TGetSecretsRawByFolderMappingsDTO, "userId">,
actor: ProjectServiceActor
) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(params.projectId);
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
params.projectId,
actor.authMethod,
actor.orgId
);
const secrets = secretV2BridgeService.getSecretsByFolderMappings({ ...params, userId: actor.id }, permission);
return secrets;
};
return { return {
attachTags, attachTags,
detachTags, detachTags,
@@ -2871,6 +2894,7 @@ export const secretServiceFactory = ({
getSecretsCount, getSecretsCount,
getSecretsCountMultiEnv, getSecretsCountMultiEnv,
getSecretsRawMultiEnv, getSecretsRawMultiEnv,
getSecretReferenceTree getSecretReferenceTree,
getSecretsRawByFolderMappings
}; };
}; };

View File

@@ -14,9 +14,31 @@ export const superAdminDALFactory = (db: TDbClient) => {
const config = await (tx || db)(TableName.SuperAdmin) const config = await (tx || db)(TableName.SuperAdmin)
.where(`${TableName.SuperAdmin}.id`, id) .where(`${TableName.SuperAdmin}.id`, id)
.leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`,
"=",
db.raw("true")
);
})
.leftJoin(TableName.OidcConfig, (qb) => {
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.OidcConfig}.isActive`,
"=",
db.raw("true")
);
})
.select( .select(
db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin, db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug") db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug"),
db.ref("authEnforced").withSchema(TableName.Organization).as("defaultAuthOrgAuthEnforced"),
db.raw(`
CASE
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml'
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc'
ELSE NULL
END as "defaultAuthOrgAuthMethod"
`)
) )
.first(); .first();
@@ -27,7 +49,11 @@ export const superAdminDALFactory = (db: TDbClient) => {
return { return {
...config, ...config,
defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
} as TSuperAdmin & { defaultAuthOrgSlug: string | null }; } as TSuperAdmin & {
defaultAuthOrgSlug: string | null;
defaultAuthOrgAuthEnforced?: boolean | null;
defaultAuthOrgAuthMethod?: string | null;
};
}; };
const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => { const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {

View File

@@ -29,7 +29,13 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>; export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
// eslint-disable-next-line // eslint-disable-next-line
export let getServerCfg: () => Promise<TSuperAdmin & { defaultAuthOrgSlug: string | null }>; export let getServerCfg: () => Promise<
TSuperAdmin & {
defaultAuthOrgSlug: string | null;
defaultAuthOrgAuthEnforced?: boolean | null;
defaultAuthOrgAuthMethod?: string | null;
}
>;
const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s const ADMIN_CONFIG_KEY_EXP = 60; // 60s

View File

@@ -0,0 +1,71 @@
---
title: "Infisical Organizational Structure Blueprint"
sidebarTitle: "Organization Structure"
description: "Learn how to structure your projects, secrets, and other resources within Infisical."
---
Infisical is designed to provide comprehensive, centralized, and efficient management of secrets, certificates, and encryption keys within organizations. Below is an overview of Infisical's structured components, which developers and administrators can leverage for optimal project management and security posture.
### 1. Projects
- **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities.
- **Correspondence to Code Repositories**: Projects typically align with specific code repositories.
- **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources.
### 2. Environments
- **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects.
- **Customization Options**: Environments can be tailored to align with existing infrastructure setups of any project. Default options include **Development**, **Staging**, and **Production**.
- **Structure**: Each environment inherently has a root level for storing secrets, but additional sub-organizations can be created through [folders](/documentation/platform/folder) for better secret management.
### 3. Folders
- **Use Case**: Folders are available for more advanced organizational needs, allowing logical separation of secrets.
- **Typical Structure**: Folders can correspond to specific logical units, such as microservices or different layers of an application, providing refined control over secrets.
### 4. Imports
- **Purpose and Benefits**: To promote reusability and avoid redundancy, Infisical supports the use of imports. This allows secrets, folders, or entire environments to be referenced across multiple projects as needed.
- **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead.
### 5. Approval Workflows
- **Importance**: Implementing approval workflows is recommended for organizations aiming to enhance efficiency and strengthen their security posture.
- **Types of Workflows**:
- **[Access Requests](/documentation/platform/pr-workflows)**: This workflow allows developers to request access to sensitive resources. Such access can be configured for temporary use, a practice known as "just-in-time" access.
- **[Change Requests](/documentation/platform/access-controls/access-requests)**: Facilitates reviews and approvals when changes are proposed for sensitive environments or specific folders, ensuring proper oversight.
### 6. Access Controls
Infisicals access control framework is unified for both human users and machine identities, ensuring consistent management across the board.
### 6.1 Roles
- **2 Role Types**:
- **Organization-Level Roles**: Provide broad access across the organization (e.g., ability to manage billing, configure settings, etc.).
- **Project-Level Roles**: Essential for configuring access to specific secrets and other sensitive assets within a project.
- **Granular Permissions**: While default roles are available, [custom roles](/documentation/platform/access-controls/role-based-access-controls#creating-custom-roles) can be created for more tailored access controls.
- **Admin Considerations**: Note that admin users are able to access all projects. This role should be assigned judiciously to prevent unintended overreach.
<Note>Project access is defined not via an organization-level role, but rather through specific project memberships of both human and machine identities. Admin roles bypass this by default. </Note>
### 6.2 Additional Privileges
[Additional privileges](/documentation/platform/access-controls/additional-privileges) can be assigned to users and machines on an ad-hoc basis for specific scenarios where roles alone are insufficient. If you find yourself using additional privileges too much, it is recommended to create custom roles. Additional privileges can be temporary or permanent.
### 6.3 Attribute-Based Access Control (ABAC)
[Attribute-based Access Controls](/documentation/platform/access-controls/attribute-based-access-controls) allow restrictions based on tags or attributes linked to secrets. These can be integrated with SAML assertions and other security frameworks for dynamic access management.
### 6.4 User Groups
- **Application**: Organizations should use users groups in situations when they have a lot of developers with the same level of access (e.g., separated by team, department, seniority, etc.).
- **Synchronization**: [User groups](/documentation/platform/groups) can be synced with an identity provider to maintain consistency and reduce manual management.
### **Implementation Note**
For larger-scale organizations, automating configurations through **Terraform** or other infrastructure-as-code (IaC) tools is advisable. Manual configurations may lead to errors, so leveraging IaC enhances reliability and consistency in managing Infisical's robust capabilities.
This structured approach ensures that Infisical's functionalities are fully leveraged, providing both flexibility and rigorous control over an organization's sensitive information and access needs.

View File

@@ -33,7 +33,7 @@ Signup can be restricted to users matching one or more email domains, such as yo
### Default Organization ### Default Organization
If you're using SAML/LDAP for only one organization on your instance, you can specify a default organization to use at login to skip requiring users to manually enter the organization slug. If you're using SAML/LDAP/OIDC for only one organization on your instance, you can specify a default organization to use at login to skip requiring users to manually enter the organization slug.
### Trust Emails ### Trust Emails

View File

@@ -0,0 +1,124 @@
---
title: "Snowflake"
description: "Learn how to dynamically generate Snowflake user credentials."
---
Infisical's Snowflake dynamic secrets allow you to generate Snowflake user credentials on demand.
## Snowflake Prerequisites
<Note>
Infisical requires a Snowflake user in your account with the USERADMIN role. This user will act as a service account for Infisical and facilitate the creation of new users as needed.
</Note>
<Steps>
<Step title="Navigate to Snowflake's User Dashboard and press the '+ User' button">
![Snowflake User Dashboard](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-users-page.png)
</Step>
<Step title="Create a Snowflake user with the USERADMIN role for Infisical">
<Warning>
Be sure to uncheck "Force user to change password on first time login"
</Warning>
![Snowflake Create Service User](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-create-service-user.png)
</Step>
<Step title="Click on the Account Menu in the bottom left and take note of your Account and Organization identifiers">
![Snowflake Account And Organization Identifiers](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-identifiers.png)
</Step>
</Steps>
## Set up Dynamic Secrets with Snowflake
<Steps>
<Step title="Open the Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select the Snowflake option in the grid list">
![Dynamic Secret Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-modal.png)
</Step>
<Step title="Provide the required parameters for the Snowflake dynamic secret">
<ParamField path="Secret Name" type="string" required>
The name you want to reference this secret by
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when generating a secret)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret
</ParamField>
<ParamField path="Account Identifier" type="string" required>
Snowflake account identifier
</ParamField>
<ParamField path="Organization Identifier" type="string" required>
Snowflake organization identifier
</ParamField>
<ParamField path="User" type="string" required>
Username of the Infisical Service User
</ParamField>
<ParamField path="Password" type="string" required>
Password of the Infisical Service User
</ParamField>
![Dynamic Secret Setup Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-setup-modal.png)
</Step>
<Step title="(Optional) Modify SQL Statements">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL
statement to your needs.
![Modify SQL Statements Modal](/images/platform/dynamic-secrets/snowflake/dynamic-secret-snowflake-sql-statements.png)
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret
lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how
long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in
step 4.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be
shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
secret.
</Warning>

View File

@@ -4,7 +4,7 @@ sidebarTitle: "Overview"
description: "Learn more about identities to interact with resources in Infisical." description: "Learn more about identities to interact with resources in Infisical."
--- ---
To interact with secrets and resource with Infisical, it is important to undrestand the concept of identities. To interact with secrets and resource with Infisical, it is important to understand the concept of identities.
Identities can be of two types: Identities can be of two types:
- **People** (e.g., developers, platform engineers, administrators) - **People** (e.g., developers, platform engineers, administrators)
- **Machines** (e.g., machine entities for managing secrets in CI/CD pipelines, production applications, and more) - **Machines** (e.g., machine entities for managing secrets in CI/CD pipelines, production applications, and more)

View File

@@ -0,0 +1,5 @@
---
title: "Kubernetes Encryption with KMS"
sidebarTitle: "Kubernetes Encryption"
url: "https://github.com/Infisical/k8-kms-plugin"
---

View File

@@ -1,6 +1,6 @@
--- ---
title: "Key Management Service (KMS)" title: "Key Management Service (KMS)"
sidebarTitle: "Key Management (KMS)" sidebarTitle: "Overview"
description: "Learn how to manage and use cryptographic keys with Infisical." description: "Learn how to manage and use cryptographic keys with Infisical."
--- ---

View File

@@ -0,0 +1,139 @@
---
title: "Microsoft SQL Server"
description: "Learn how to automatically rotate Microsoft SQL Server user passwords."
---
The Infisical SQL Server secret rotation allows you to automatically rotate your database users' passwords at a predefined interval.
## Prerequisites
1. Create two SQL Server logins and database users with the required permissions. We'll refer to them as `user-a` and `user-b`.
2. Create another SQL Server login with permissions to alter logins for `user-a` and `user-b`. We'll refer to this as the `admin` login.
Here's how to set up the prerequisites:
```sql
-- Create the logins (at server level)
CREATE LOGIN [user-a] WITH PASSWORD = 'ComplexPassword1';
CREATE LOGIN [user-b] WITH PASSWORD = 'ComplexPassword2';
-- Create database users for the logins (in your specific database)
USE [YourDatabase];
CREATE USER [user-a] FOR LOGIN [user-a];
CREATE USER [user-b] FOR LOGIN [user-b];
-- Grant necessary permissions to the users
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-a];
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-b];
-- Create admin login with permission to alter other logins
CREATE LOGIN [admin] WITH PASSWORD = 'AdminComplexPassword';
CREATE USER [admin] FOR LOGIN [admin];
-- Grant permission to alter any login
GRANT ALTER ANY LOGIN TO [admin];
```
To learn more about SQL Server's permission system, please visit this [documentation](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/getting-started-with-database-engine-permissions).
## How it works
1. Infisical connects to your database using the provided `admin` login credentials.
2. A random value is generated and the password for `user-a` is updated with the new value.
3. The new password is then tested by logging into the database.
4. If test is successful, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
5. The process is then repeated for `user-b` on the next rotation.
6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration
<Steps>
<Step title="Open Secret Rotation Page">
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
</Step>
<Step title="Click on Microsoft SQL Server card" />
<Step title="Provide the inputs">
<ParamField path="Admin Username" type="string" required>
SQL Server admin username
</ParamField>
<ParamField path="Admin password" type="string" required>
SQL Server admin password
</ParamField>
<ParamField path="Host" type="string" required>
SQL Server host url (e.g., your-server.database.windows.net)
</ParamField>
<ParamField path="Port" type="number" required>
Database port number (default: 1433)
</ParamField>
<ParamField path="Database" type="string" required>
Database name (default: master)
</ParamField>
<ParamField path="Username1" type="string" required>
The first login name to rotate - `user-a`
</ParamField>
<ParamField path="Username2" type="string" required>
The second login name to rotate - `user-b`
</ParamField>
<ParamField path="CA" type="string">
Optional database certificate to connect with database
</ParamField>
</Step>
<Step title="Configure the output secret mapping">
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
<ParamField path="Environment" type="string" required>
The environment where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Secret Path" type="string" required>
The secret path where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Interval" type="number" required>
What interval should the credentials be rotated in days.
</ParamField>
<ParamField path="DB USERNAME" type="string" required>
Select an existing secret key where the rotated database username value should be saved to.
</ParamField>
<ParamField path="DB PASSWORD" type="string" required>
Select an existing select key where the rotated database password value should be saved to.
</ParamField>
</Step>
</Steps>
## FAQ
<AccordionGroup>
<Accordion title="Why can't we delete the other user when rotating?">
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
</Accordion>
<Accordion title="Why do you need an admin account?">
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
You don't need to grant all permissions for your admin account but rather just the permission to alter logins (ALTER ANY LOGIN).
</Accordion>
<Accordion title="How does this work with Azure SQL Database?">
When using Azure SQL Database, you'll need to:
1. Use the full server name as your host (e.g., your-server.database.windows.net)
2. Ensure your admin account is either the Azure SQL Server admin or an Azure AD account with appropriate permissions
3. Configure your Azure SQL Server firewall rules to allow connections from Infisical's IP addresses
</Accordion>
</AccordionGroup>

View File

@@ -69,11 +69,18 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note> <Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64 can be a random 32-byte base64 string generated with `openssl rand -base64
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should 32`.
be an absolute URL including the protocol (e.g. https://app.infisical.com) <div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

View File

@@ -109,12 +109,20 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. <div class="height:1px;"/>
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>
<Note> <Note>

View File

@@ -20,11 +20,11 @@ Prerequisites:
<Steps> <Steps>
<Step title="Setup Identity Provider"> <Step title="Setup Identity Provider">
1.1. Register your application with the IdP to obtain a **Client ID** and **Client Secret**. These credentials are used by Infisical to authenticate with your IdP. 1.1. Register your application with the IdP to obtain a **Client ID** and **Client Secret**. These credentials are used by Infisical to authenticate with your IdP.
1.2. Configure **Redirect URL** to be `https://app.infisical.com/api/v1/sso/oidc/callback`. If you're self-hosting Infisical, replace the domain with your own. 1.2. Configure **Redirect URL** to be `https://app.infisical.com/api/v1/sso/oidc/callback`. If you're self-hosting Infisical, replace the domain with your own.
1.3. Configure the scopes needed by Infisical (email, profile, openid) and ensure that they are mapped to the ID token claims. 1.3. Configure the scopes needed by Infisical (email, profile, openid) and ensure that they are mapped to the ID token claims.
1.4. Access the IdPs OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. 1.4. Access the IdPs OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
</Step> </Step>
<Step title="Finish configuring OIDC in Infisical"> <Step title="Finish configuring OIDC in Infisical">
@@ -70,11 +70,19 @@ Prerequisites:
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note> <Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64 can be a random 32-byte base64 string generated with `openssl rand -base64
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should 32`.
be an absolute URL including the protocol (e.g. https://app.infisical.com) <div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

View File

@@ -85,13 +85,20 @@ description: "Learn how to configure Google SAML for Infisical SSO."
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make If you're configuring SAML SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This work:
can be a random 32-byte base64 string generated with `openssl rand -base64 <div class="height:1px;"/>
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
be an absolute URL including the protocol (e.g. https://app.infisical.com) can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>
References: References:

View File

@@ -89,10 +89,18 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. <div class="height:1px;"/>
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

View File

@@ -95,11 +95,18 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note> <Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64 can be a random 32-byte base64 string generated with `openssl rand -base64
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should 32`.
be an absolute URL including the protocol (e.g. https://app.infisical.com) <div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

View File

@@ -130,10 +130,18 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. <div class="height:1px;"/>
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

View File

@@ -98,11 +98,18 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
</Step> </Step>
</Steps> </Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite SAML login.
</Tip>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make If you're configuring SAML SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This work:
can be a random 32-byte base64 string generated with `openssl rand -base64 <div class="height:1px;"/>
32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
be an absolute URL including the protocol (e.g. https://app.infisical.com) can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note> </Note>

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -82,7 +82,8 @@
"documentation/guides/node", "documentation/guides/node",
"documentation/guides/python", "documentation/guides/python",
"documentation/guides/nextjs-vercel", "documentation/guides/nextjs-vercel",
"documentation/guides/microsoft-power-apps" "documentation/guides/microsoft-power-apps",
"documentation/guides/organization-structure"
] ]
} }
] ]
@@ -113,7 +114,13 @@
"documentation/platform/pki/alerting" "documentation/platform/pki/alerting"
] ]
}, },
"documentation/platform/kms", {
"group": "Key Management (KMS)",
"pages": [
"documentation/platform/kms/overview",
"documentation/platform/kms/kubernetes-encryption"
]
},
{ {
"group": "KMS Configuration", "group": "KMS Configuration",
"pages": [ "pages": [
@@ -158,6 +165,7 @@
"documentation/platform/secret-rotation/sendgrid", "documentation/platform/secret-rotation/sendgrid",
"documentation/platform/secret-rotation/postgres", "documentation/platform/secret-rotation/postgres",
"documentation/platform/secret-rotation/mysql", "documentation/platform/secret-rotation/mysql",
"documentation/platform/secret-rotation/mssql",
"documentation/platform/secret-rotation/aws-iam" "documentation/platform/secret-rotation/aws-iam"
] ]
}, },
@@ -179,7 +187,8 @@
"documentation/platform/dynamic-secrets/mongo-db", "documentation/platform/dynamic-secrets/mongo-db",
"documentation/platform/dynamic-secrets/azure-entra-id", "documentation/platform/dynamic-secrets/azure-entra-id",
"documentation/platform/dynamic-secrets/ldap", "documentation/platform/dynamic-secrets/ldap",
"documentation/platform/dynamic-secrets/sap-hana" "documentation/platform/dynamic-secrets/sap-hana",
"documentation/platform/dynamic-secrets/snowflake"
] ]
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -16,6 +16,7 @@ export type CheckboxProps = Omit<
checkIndicatorBg?: string | undefined; checkIndicatorBg?: string | undefined;
isError?: boolean; isError?: boolean;
isIndeterminate?: boolean; isIndeterminate?: boolean;
containerClassName?: string;
}; };
export const Checkbox = ({ export const Checkbox = ({
@@ -28,10 +29,11 @@ export const Checkbox = ({
checkIndicatorBg, checkIndicatorBg,
isError, isError,
isIndeterminate, isIndeterminate,
containerClassName,
...props ...props
}: CheckboxProps): JSX.Element => { }: CheckboxProps): JSX.Element => {
return ( return (
<div className="flex items-center font-inter text-bunker-300"> <div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500", "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",

View File

@@ -127,8 +127,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2 cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2
pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary", isSelected && "bg-primary",
isDisabled && isDisabled && "cursor-not-allowed text-gray-600 opacity-80 hover:!bg-transparent",
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className className
)} )}
ref={forwardedRef} ref={forwardedRef}

View File

@@ -0,0 +1,5 @@
export const reverseTruncate = (text: string, maxLength = 42) => {
if (text.length < maxLength) return text;
return `...${text.substring(text.length - maxLength + 3)}`;
};

View File

@@ -19,6 +19,8 @@ export type TServerConfig = {
isSecretScanningDisabled: boolean; isSecretScanningDisabled: boolean;
defaultAuthOrgSlug: string | null; defaultAuthOrgSlug: string | null;
defaultAuthOrgId: string | null; defaultAuthOrgId: string | null;
defaultAuthOrgAuthMethod?: string | null;
defaultAuthOrgAuthEnforced?: boolean | null;
enabledLoginMethods: LoginMethod[]; enabledLoginMethods: LoginMethod[];
}; };

View File

@@ -1 +1,5 @@
export { useGetProjectSecretsDetails } from "./queries"; export {
useGetProjectSecretsDetails,
useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch
} from "./queries";

View File

@@ -10,12 +10,15 @@ import {
DashboardProjectSecretsOverview, DashboardProjectSecretsOverview,
DashboardProjectSecretsOverviewResponse, DashboardProjectSecretsOverviewResponse,
DashboardSecretsOrderBy, DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
} from "@app/hooks/api/dashboard/types"; } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { unique } from "@app/lib/fn/array"; import { groupBy, unique } from "@app/lib/fn/array";
export const dashboardKeys = { export const dashboardKeys = {
all: () => ["dashboard"] as const, all: () => ["dashboard"] as const,
@@ -42,8 +45,18 @@ export const dashboardKeys = {
}: TGetDashboardProjectSecretsDetailsDTO) => }: TGetDashboardProjectSecretsDetailsDTO) =>
[ [
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
environment,
"secrets-details", "secrets-details",
environment,
params
] as const,
getProjectSecretsQuickSearch: ({
projectId,
secretPath,
...params
}: TGetDashboardProjectSecretsQuickSearchDTO) =>
[
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search",
params params
] as const ] as const
}; };
@@ -256,3 +269,101 @@ export const useGetProjectSecretsDetails = (
keepPreviousData: true keepPreviousData: true
}); });
}; };
export const fetchProjectSecretsQuickSearch = async ({
environments,
tags,
...params
}: TGetDashboardProjectSecretsQuickSearchDTO) => {
const { data } = await apiRequest.get<TDashboardProjectSecretsQuickSearchResponse>(
"/api/v1/dashboard/secrets-deep-search",
{
params: {
...params,
environments: encodeURIComponent(environments.join(",")),
tags: encodeURIComponent(
Object.entries(tags)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, enabled]) => enabled)
.map(([tag]) => tag)
.join(",")
)
}
}
);
return data;
};
export const useGetProjectSecretsQuickSearch = (
{
projectId,
secretPath,
search = "",
environments,
tags
}: TGetDashboardProjectSecretsQuickSearchDTO,
options?: Omit<
UseQueryOptions<
TDashboardProjectSecretsQuickSearchResponse,
unknown,
TDashboardProjectSecretsQuickSearch,
ReturnType<typeof dashboardKeys.getProjectSecretsQuickSearch>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
...options,
enabled:
Boolean(search?.trim() || Object.values(tags).length) &&
(options?.enabled ?? true) &&
Boolean(environments.length),
queryKey: dashboardKeys.getProjectSecretsQuickSearch({
secretPath,
search,
projectId,
environments,
tags
}),
queryFn: () =>
fetchProjectSecretsQuickSearch({
secretPath,
search,
projectId,
environments,
tags
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
createNotification({
title: "Error fetching secrets deep search",
type: "error",
text: serverResponse.message
});
}
},
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsQuickSearch>>) => {
const { secrets, folders, dynamicSecrets } = data;
const groupedFolders = groupBy(folders, (folder) => folder.path);
const groupedSecrets = groupBy(
mergePersonalSecrets(secrets),
(secret) => `${secret.path === "/" ? "" : secret.path}/${secret.key}`
);
const groupedDynamicSecrets = groupBy(
dynamicSecrets,
(dynamicSecret) =>
`${dynamicSecret.path === "/" ? "" : dynamicSecret.path}/${dynamicSecret.name}`
);
return {
folders: groupedFolders,
secrets: groupedSecrets,
dynamicSecrets: groupedDynamicSecrets
};
}, []),
keepPreviousData: true
});
};

View File

@@ -69,3 +69,23 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
includeImports?: boolean; includeImports?: boolean;
tags: Record<string, boolean>; tags: Record<string, boolean>;
}; };
export type TDashboardProjectSecretsQuickSearchResponse = {
folders: (TSecretFolder & { environment: string; path: string })[];
dynamicSecrets: (TDynamicSecret & { environment: string; path: string })[];
secrets: SecretV3Raw[];
};
export type TDashboardProjectSecretsQuickSearch = {
folders: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
secrets: Record<string, SecretV3RawSanitized[]>;
dynamicSecrets: Record<string, TDashboardProjectSecretsQuickSearchResponse["folders"]>;
};
export type TGetDashboardProjectSecretsQuickSearchDTO = {
projectId: string;
secretPath: string;
tags: Record<string, boolean>;
search: string;
environments: string[];
};

View File

@@ -27,7 +27,8 @@ export enum DynamicSecretProviders {
RabbitMq = "rabbit-mq", RabbitMq = "rabbit-mq",
AzureEntraId = "azure-entra-id", AzureEntraId = "azure-entra-id",
Ldap = "ldap", Ldap = "ldap",
SapHana = "sap-hana" SapHana = "sap-hana",
Snowflake = "snowflake"
} }
export enum SqlProviders { export enum SqlProviders {
@@ -215,6 +216,18 @@ export type TDynamicSecretProvider =
renewStatement?: string; renewStatement?: string;
ca?: string | undefined; ca?: string | undefined;
}; };
}
| {
type: DynamicSecretProviders.Snowflake;
inputs: {
orgId: string;
accountId: string;
username: string;
password: string;
creationStatement: string;
revocationStatement: string;
renewStatement?: string;
};
}; };
export type TCreateDynamicSecretDTO = { export type TCreateDynamicSecretDTO = {
projectSlug: string; projectSlug: string;

View File

@@ -12,7 +12,7 @@ export type IdentityTrustedIp = {
export type Identity = { export type Identity = {
id: string; id: string;
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethods: IdentityAuthMethod[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };

View File

@@ -66,7 +66,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
createdAt: el.createdAt, createdAt: el.createdAt,
updatedAt: el.updatedAt, updatedAt: el.updatedAt,
version: el.version, version: el.version,
skipMultilineEncoding: el.skipMultilineEncoding skipMultilineEncoding: el.skipMultilineEncoding,
path: el.secretPath
}; };
if (el.type === SecretType.Personal) { if (el.type === SecretType.Personal) {

View File

@@ -29,7 +29,7 @@ export type EncryptedSecret = {
tags: WsTag[]; tags: WsTag[];
}; };
// both personal and shared secret stitiched together for dashboard // both personal and shared secret stitched together for dashboard
export type SecretV3RawSanitized = { export type SecretV3RawSanitized = {
id: string; id: string;
version: number; version: number;
@@ -42,6 +42,7 @@ export type SecretV3RawSanitized = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
env: string; env: string;
path?: string;
valueOverride?: string; valueOverride?: string;
idOverride?: string; idOverride?: string;
overrideAction?: string; overrideAction?: string;
@@ -57,6 +58,7 @@ export type SecretV3Raw = {
version: number; version: number;
type: string; type: string;
secretKey: string; secretKey: string;
secretPath: string;
secretValue?: string; secretValue?: string;
secretComment?: string; secretComment?: string;
secretReminderNote?: string; secretReminderNote?: string;

View File

@@ -11,7 +11,8 @@ export enum AuthMethod {
JUMPCLOUD_SAML = "jumpcloud-saml", JUMPCLOUD_SAML = "jumpcloud-saml",
KEYCLOAK_SAML = "keycloak-saml", KEYCLOAK_SAML = "keycloak-saml",
LDAP = "ldap", LDAP = "ldap",
OIDC = "oidc" OIDC = "oidc",
SAML = "saml"
} }
export type User = { export type User = {

View File

@@ -1,4 +1,4 @@
import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { FormEvent, useEffect, useRef, 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";
@@ -17,6 +17,7 @@ import { Button, IconButton, Input, Tooltip } from "@app/components/v2";
import { useServerConfig } from "@app/context"; import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types"; import { LoginMethod } from "@app/hooks/api/admin/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { useNavigateToSelectOrganization } from "../../Login.utils"; import { useNavigateToSelectOrganization } from "../../Login.utils";
@@ -51,17 +52,33 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
router.push(redirectUrl); router.push(redirectUrl);
}; };
const redirectToOidc = (orgSlug: string) => {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/oidc/login?orgSlug=${orgSlug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
router.push(redirectUrl);
};
useEffect(() => { useEffect(() => {
if (serverDetails?.samlDefaultOrgSlug) redirectToSaml(serverDetails.samlDefaultOrgSlug); if (serverDetails?.samlDefaultOrgSlug) redirectToSaml(serverDetails.samlDefaultOrgSlug);
}, [serverDetails?.samlDefaultOrgSlug]); }, [serverDetails?.samlDefaultOrgSlug]);
const handleSaml = useCallback((step: number) => { const handleSaml = () => {
if (config.defaultAuthOrgSlug) { if (config.defaultAuthOrgSlug) {
redirectToSaml(config.defaultAuthOrgSlug); redirectToSaml(config.defaultAuthOrgSlug);
} else { } else {
setStep(step); setStep(2);
} }
}, []); };
const handleOidc = () => {
if (config.defaultAuthOrgSlug) {
redirectToOidc(config.defaultAuthOrgSlug);
} else {
setStep(3);
}
};
const shouldDisplayLoginMethod = (method: LoginMethod) => const shouldDisplayLoginMethod = (method: LoginMethod) =>
!config.enabledLoginMethods || config.enabledLoginMethods.includes(method); !config.enabledLoginMethods || config.enabledLoginMethods.includes(method);
@@ -142,6 +159,46 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
setIsLoading(false); setIsLoading(false);
}; };
if (config.defaultAuthOrgAuthEnforced && config.defaultAuthOrgAuthMethod) {
return (
<form
onSubmit={handleLogin}
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>
<RegionSelect />
{config.defaultAuthOrgAuthMethod === AuthMethod.SAML && (
<div className="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={handleSaml}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML
</Button>
</div>
)}
{config.defaultAuthOrgAuthMethod === AuthMethod.OIDC && (
<div className="mt-2 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={handleOidc}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
)}
</form>
);
}
return ( return (
<form <form
onSubmit={handleLogin} onSubmit={handleLogin}
@@ -156,9 +213,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={handleSaml}
handleSaml(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full" className="mx-0 h-10 w-full"
> >
@@ -171,9 +226,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
<Button <Button
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={handleOidc}
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full" className="mx-0 h-10 w-full"
> >

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -19,12 +20,15 @@ import {
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { import {
IdentityAuthMethod,
useDeleteIdentity, useDeleteIdentity,
useGetIdentityById, useGetIdentityById,
useRevokeIdentityTokenAuthToken, useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api"; useRevokeIdentityUniversalAuthClientSecret
} from "@app/hooks/api";
import { Identity } from "@app/hooks/api/identities/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from"@app/views/Org/Types"; import { TabSections } from "@app/views/Org/Types";
import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
@@ -49,6 +53,10 @@ export const IdentityPage = withPermission(
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken(); const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
Identity["authMethods"][number] | null
>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity", "identity",
"deleteIdentity", "deleteIdentity",
@@ -124,7 +132,7 @@ export const IdentityPage = withPermission(
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => { const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
try { try {
if (!data?.identity.id) return; if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
await revokeClientSecret({ await revokeClientSecret({
identityId: data?.identity.id, identityId: data?.identity.id,
@@ -208,12 +216,12 @@ export const IdentityPage = withPermission(
handlePopUpOpen("identityAuthMethod", { handlePopUpOpen("identityAuthMethod", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
authMethod: data.identity.authMethod allAuthMethods: data.identity.authMethods
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
{`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`} Add new auth method
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -247,6 +255,8 @@ export const IdentityPage = withPermission(
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection <IdentityAuthenticationSection
selectedAuthMethod={selectedAuthMethod}
setSelectedAuthMethod={setSelectedAuthMethod}
identityId={identityId} identityId={identityId}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
/> />

View File

@@ -1,15 +1,13 @@
import { faPencil } from "@fortawesome/free-solid-svg-icons"; import { useEffect } from "react";
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
IconButton,
// Button,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetIdentityById } from "@app/hooks/api"; import { useGetIdentityById } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { Identity } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityClientSecrets } from "./IdentityClientSecrets"; import { IdentityClientSecrets } from "./IdentityClientSecrets";
@@ -17,6 +15,8 @@ import { IdentityTokens } from "./IdentityTokens";
type Props = { type Props = {
identityId: string; identityId: string;
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
selectedAuthMethod: Identity["authMethods"][number] | null;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
[ [
@@ -33,16 +33,34 @@ type Props = {
) => void; ) => void;
}; };
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => { export const IdentityAuthenticationSection = ({
identityId,
setSelectedAuthMethod,
selectedAuthMethod,
handlePopUpOpen
}: Props) => {
const { data } = useGetIdentityById(identityId); const { data } = useGetIdentityById(identityId);
useEffect(() => {
if (!data?.identity) return;
if (data.identity.authMethods?.length) {
setSelectedAuthMethod(data.identity.authMethods[0]);
}
// eslint-disable-next-line consistent-return
return () => setSelectedAuthMethod(null);
}, [data?.identity]);
return data ? ( return data ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip content={`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}> <Tooltip content="Add new auth method">
<IconButton <IconButton
isDisabled={!isAllowed} isDisabled={!isAllowed}
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -52,32 +70,85 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
handlePopUpOpen("identityAuthMethod", { handlePopUpOpen("identityAuthMethod", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
authMethod: data.identity.authMethod allAuthMethods: data.identity.authMethods
}) })
} }
> >
<FontAwesomeIcon icon={faPencil} /> <FontAwesomeIcon icon={faPlus} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
}} }}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<div className="py-4"> {data.identity.authMethods.length > 0 ? (
<div className="flex justify-between"> <>
<p className="text-sm font-semibold text-mineshaft-300">Auth Method</p> <div className="py-4">
<div className="flex justify-between">
<p className="ml-px mb-0.5 text-sm font-semibold text-mineshaft-300">Auth Method</p>
</div>
<div className="flex items-center gap-2">
<div className="w-full">
<Select
className="w-full"
value={selectedAuthMethod as string}
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
>
{(data.identity?.authMethods || []).map((authMethod) => (
<SelectItem key={authMethod || authMethod} value={authMethod}>
{identityAuthToNameMap[authMethod]}
</SelectItem>
))}
</Select>
</div>
<div>
<Tooltip content="Edit auth method">
<IconButton
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: selectedAuthMethod,
allAuthMethods: data.identity.authMethods
});
}}
ariaLabel="copy icon"
variant="plain"
className="group relative"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>{" "}
</div>
</div>
</div>
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
</>
) : (
<div className="w-full space-y-2 pt-2">
<p className="text-sm text-mineshaft-300">
No authentication methods configured. Get started by creating a new auth method.
</p>
<Button
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
variant="outline_bg"
className="w-full"
size="xs"
>
Create Auth Method
</Button>
</div> </div>
<p className="text-sm text-mineshaft-300">
{data.identity.authMethod
? identityAuthToNameMap[data.identity.authMethod]
: "Not configured"}
</p>
</div>
{data.identity.authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{data.identity.authMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)} )}
</div> </div>
) : ( ) : (

View File

@@ -1,38 +1,10 @@
import { useEffect } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { Modal, ModalContent } from "@app/components/v2";
import {
DeleteActionModal,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = { type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
@@ -43,229 +15,13 @@ type Props = {
) => void; ) => void;
}; };
const identityAuthMethods = [
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
];
const schema = yup
.object({
authMethod: yup
.mixed<IdentityAuthMethod>()
.oneOf(Object.values(IdentityAuthMethod))
.required("Auth method is required")
})
.required();
export type FormData = yup.InferType<typeof schema>;
export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization(); const [selectedAuthMethod, setSelectedAuthMethod] = useState<IdentityAuthMethod | null>(null);
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod; const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod;
const { control, watch, setValue, reset } = useForm<FormData>({ const isSelectedAuthAlreadyConfigured =
resolver: yupResolver(schema), popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod);
defaultValues: {
authMethod: initialAuthMethod
}
});
useEffect(() => {
// reset form on open
if (popUp.identityAuthMethod.isOpen)
reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod });
}, [popUp.identityAuthMethod.isOpen]);
const identityAuthMethodData = {
identityId: popUp?.identityAuthMethod.data?.identityId,
name: popUp?.identityAuthMethod?.data?.name,
authMethod: watch("authMethod")
} as {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
useEffect(() => {
if (identityAuthMethodData?.authMethod) {
setValue("authMethod", identityAuthMethodData.authMethod);
return;
}
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
}, [identityAuthMethodData?.authMethod]);
const onRevokeAuthMethodSubmit = async (authMethod: IdentityAuthMethod) => {
if (!orgId || !authMethod) return;
try {
switch (authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH: {
await revokeUniversalAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.TOKEN_AUTH: {
await revokeTokenAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
await revokeKubernetesAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.GCP_AUTH: {
await revokeGcpAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AWS_AUTH: {
await revokeAwsAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AZURE_AUTH: {
await revokeAzureAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.OIDC_AUTH: {
await revokeOidcAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
default:
break;
}
createNotification({
text: `Successfully removed ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to remove ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
type: "error"
});
}
};
const renderIdentityAuthForm = () => {
switch (identityAuthMethodData.authMethod) {
case IdentityAuthMethod.AWS_AUTH: {
return (
<IdentityAwsAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
return (
<IdentityKubernetesAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.AZURE_AUTH: {
return (
<IdentityAzureAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.UNIVERSAL_AUTH: {
return (
<IdentityUniversalAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.OIDC_AUTH: {
return (
<IdentityOidcAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.TOKEN_AUTH: {
return (
<IdentityTokenAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
default: {
return <div />;
}
}
};
return ( return (
<Modal <Modal
@@ -275,52 +31,23 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
}} }}
> >
<ModalContent <ModalContent
title={`${ title={
identityAuthMethodData.authMethod === initialAuthMethod ? "Update" : "Configure" isSelectedAuthAlreadyConfigured
} Identity Auth Method for ${ ? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
identityAuthToNameMap[identityAuthMethodData.authMethod!] ?? "" : `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
}`} }
> >
<Controller <IdentityAuthMethodModalContent
control={control} popUp={popUp}
name="authMethod" handlePopUpOpen={handlePopUpOpen}
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH} handlePopUpToggle={handlePopUpToggle}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( identity={{
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}> name: popUp?.identityAuthMethod?.data?.name,
<Select authMethods: popUp?.identityAuthMethod?.data?.allAuthMethods,
defaultValue={field.value} id: popUp?.identityAuthMethod.data?.identityId
{...field} }}
onValueChange={(e) => { initialAuthMethod={initialAuthMethod}
onChange(e); setSelectedAuthMethod={setSelectedAuthMethod}
}}
className="w-full"
>
{identityAuthMethods.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{renderIdentityAuthForm()}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)}
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@@ -0,0 +1,317 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DeleteActionModal,
FormControl,
Select,
SelectItem,
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
state?: boolean
) => void;
identity: {
name: string;
id: string;
authMethods: IdentityAuthMethod[];
};
initialAuthMethod: IdentityAuthMethod;
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
};
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
type TRevokeMethods = {
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
render: () => JSX.Element;
};
const identityAuthMethods = [
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
];
const schema = yup
.object({
authMethod: yup
.mixed<IdentityAuthMethod>()
.oneOf(Object.values(IdentityAuthMethod))
.required("Auth method is required")
})
.required();
export type FormData = yup.InferType<typeof schema>;
export const IdentityAuthMethodModalContent = ({
popUp,
handlePopUpOpen,
handlePopUpToggle,
identity,
initialAuthMethod,
setSelectedAuthMethod
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { control, watch } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: async () => {
let authMethod = initialAuthMethod;
if (!authMethod) {
const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find(
({ value }) => !identity?.authMethods?.includes(value)
);
if (firstAuthMethodNotConfiguredAuthMethod) {
authMethod = firstAuthMethodNotConfiguredAuthMethod.value;
}
}
setSelectedAuthMethod(authMethod);
return {
authMethod
};
}
});
const watchedAuthMethod = watch("authMethod");
const identityAuthMethodData = {
identityId: identity.id,
name: identity.name,
authMethod: watch("authMethod"),
configuredAuthMethods: identity.authMethods
} as {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
configuredAuthMethods?: IdentityAuthMethod[];
};
const isSelectedAuthAlreadyConfigured =
identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod);
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
revokeMethod: revokeUniversalAuth,
render: () => (
<IdentityUniversalAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.OIDC_AUTH]: {
revokeMethod: revokeOidcAuth,
render: () => (
<IdentityOidcAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.TOKEN_AUTH]: {
revokeMethod: revokeTokenAuth,
render: () => (
<IdentityTokenAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.AZURE_AUTH]: {
revokeMethod: revokeAzureAuth,
render: () => (
<IdentityAzureAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.GCP_AUTH]: {
revokeMethod: revokeGcpAuth,
render: () => (
<IdentityGcpAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.KUBERNETES_AUTH]: {
revokeMethod: revokeKubernetesAuth,
render: () => (
<IdentityKubernetesAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.AWS_AUTH]: {
revokeMethod: revokeAwsAuth,
render: () => (
<IdentityAwsAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
}
};
const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => {
return identityAuthMethodData?.configuredAuthMethods?.includes(method);
}, []);
const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!];
return (
<>
<Controller
control={control}
name="authMethod"
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}>
<Select
isDisabled={isSelectedAuthAlreadyConfigured}
defaultValue={field.value}
{...field}
onValueChange={(e) => {
if (!isAlreadyConfigured(e as IdentityAuthMethod)) {
setSelectedAuthMethod(e as IdentityAuthMethod);
onChange(e);
}
}}
className="w-full"
>
{identityAuthMethods.map(({ label, value }) => {
const alreadyConfigured = isAlreadyConfigured(value);
return (
<Tooltip
key={`auth-method-${value}`}
content="Authentication method already configured"
isDisabled={!alreadyConfigured}
>
<SelectItem
isDisabled={alreadyConfigured}
value={String(value || "")}
key={label}
>
{label}{" "}
{alreadyConfigured && !isSelectedAuthAlreadyConfigured && (
<Badge>Configured</Badge>
)}
</SelectItem>
</Tooltip>
);
})}
</Select>
</FormControl>
)}
/>
{selectedMethodItem?.render ? selectedMethodItem.render() : <div />}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={async () => {
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
return;
}
try {
await selectedMethodItem.revokeMethod({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch (err) {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
}}
/>
</>
);
};

View File

@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAwsAuth, useAddIdentityAwsAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
.object({ .object({
@@ -62,18 +62,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAwsAuthForm = ({ export const IdentityAwsAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -82,13 +79,13 @@ export const IdentityAwsAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -184,230 +181,204 @@ export const IdentityAwsAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="allowedPrincipalArns"
name="allowedPrincipalArns" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Principal ARNs"
label="Allowed Principal ARNs" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input
<Input {...field}
{...field} placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..." type="text"
type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/> />
<IconButton </FormControl>
onClick={() => { )}
if (subscription?.ipAllowlisting) { />
removeAccessTokenTrustedIp(index); <Controller
return; control={control}
} name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" placeholder="123.456.789.0"
colorSchema="danger" />
variant="plain" </FormControl>
ariaLabel="update" );
className="p-3" }}
> />
<FontAwesomeIcon icon={faXmark} /> <IconButton
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };

View File

@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAzureAuth, useAddIdentityAzureAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -50,18 +50,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAzureAuthForm = ({ export const IdentityAzureAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -70,18 +67,18 @@ export const IdentityAzureAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -173,239 +170,204 @@ export const IdentityAzureAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="tenantId"
name="tenantId" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Tenant ID"
label="Tenant ID" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} isRequired
isRequired >
> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="resource"
name="resource" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Resource / Audience"
label="Resource / Audience" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="https://management.azure.com/" />
<Input {...field} placeholder="https://management.azure.com/" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="allowedServicePrincipalIds"
name="allowedServicePrincipalIds" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Service Principal IDs"
label="Allowed Service Principal IDs" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenTTL"
name="accessTokenTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token TTL (seconds)"
label="Access Token TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenMaxTTL"
name="accessTokenMaxTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max TTL (seconds)"
label="Access Token Max TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="0"
defaultValue="0" name="accessTokenNumUsesLimit"
name="accessTokenNumUsesLimit" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max Number of Uses"
label="Access Token Max Number of Uses" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="0" type="number" min="0" step="1" />
<Input {...field} placeholder="0" type="number" min="0" step="1" /> </FormControl>
</FormControl> )}
)} />
/> {accessTokenTrustedIpsFields.map(({ id }, index) => (
{accessTokenTrustedIpsFields.map(({ id }, index) => ( <div className="mb-3 flex items-end space-x-2" key={id}>
<div className="mb-3 flex items-end space-x-2" key={id}> <Controller
<Controller control={control}
control={control} name={`accessTokenTrustedIps.${index}.ipAddress`}
name={`accessTokenTrustedIps.${index}.ipAddress`} defaultValue="0.0.0.0/0"
defaultValue="0.0.0.0/0" render={({ field, fieldState: { error } }) => {
render={({ field, fieldState: { error } }) => { return (
return ( <FormControl
<FormControl className="mb-0 flex-grow"
className="mb-0 flex-grow" label={index === 0 ? "Access Token Trusted IPs" : undefined}
label={index === 0 ? "Access Token Trusted IPs" : undefined} isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input
<Input value={field.value}
value={field.value} onChange={(e) => {
onChange={(e) => { if (subscription?.ipAllowlisting) {
if (subscription?.ipAllowlisting) { field.onChange(e);
field.onChange(e); return;
return; }
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };

View File

@@ -6,15 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityGcpAuth, useAddIdentityGcpAuth,
@@ -23,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -59,18 +51,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityGcpAuthForm = ({ export const IdentityGcpAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
revokeAuth,
initialAuthMethod
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -79,11 +68,12 @@ export const IdentityGcpAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -189,258 +179,228 @@ export const IdentityGcpAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} name="type"
name="type" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( <FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}> <Select
<Select defaultValue={field.value}
defaultValue={field.value} {...field}
{...field} onValueChange={(e) => onChange(e)}
onValueChange={(e) => onChange(e)} className="w-full"
className="w-full"
>
<SelectItem value="gce" key="gce">
GCP ID Token Auth (Recommended)
</SelectItem>
<SelectItem value="iam" key="iam">
GCP IAM Auth
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="allowedServiceAccounts"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
> >
<Input <SelectItem value="gce" key="gce">
{...field} GCP ID Token Auth (Recommended)
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com" </SelectItem>
type="text" <SelectItem value="iam" key="iam">
/> GCP IAM Auth
</FormControl> </SelectItem>
)} </Select>
/> </FormControl>
{watchedType === "gce" && (
<Controller
control={control}
name="allowedProjects"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Projects"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="my-gcp-project, ..." />
</FormControl>
)}
/>
)} )}
{watchedType === "gce" && ( />
<Controller <Controller
control={control} control={control}
name="allowedZones" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="allowedServiceAccounts"
<FormControl render={({ field, fieldState: { error } }) => (
label="Allowed Zones" <FormControl
isError={Boolean(error)} label="Allowed Service Account Emails"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." /> >
</FormControl> <Input
)} {...field}
/> placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
)} type="text"
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/> />
<IconButton </FormControl>
onClick={() => { )}
if (subscription?.ipAllowlisting) { />
removeAccessTokenTrustedIp(index); {watchedType === "gce" && (
return; <Controller
} control={control}
name="allowedProjects"
handlePopUpOpen("upgradePlan"); render={({ field, fieldState: { error } }) => (
}} <FormControl
size="lg" label="Allowed Projects"
colorSchema="danger" isError={Boolean(error)}
variant="plain" errorText={error?.message}
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> <Input {...field} placeholder="my-gcp-project, ..." />
</IconButton> </FormControl>
</div> )}
))} />
<div className="my-4 ml-1"> )}
<Button {watchedType === "gce" && (
variant="outline_bg" <Controller
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
buttonText="Overwrite"
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };

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