1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-23 03:03:05 +00:00

Compare commits

..

57 Commits

Author SHA1 Message Date
b9a9b6b4d9 misc: applied ui/ux changes 2024-05-22 00:06:06 +08:00
9b404c215b adjustment: ui changes to sync button 2024-05-21 16:04:36 +08:00
d6dae04959 misc: removed unnecessary notification 2024-05-21 14:01:15 +08:00
629bd9b7c6 added support for manual syncing of integrations 2024-05-21 13:56:44 +08:00
9253c69325 misc: finalized ui design of integration sync status 2024-05-21 02:35:23 +08:00
7d3a62cc4c feat: added integration sync status 2024-05-20 20:56:29 +08:00
554dbf6c23 Merge pull request from Infisical/create-pull-request/patch-1716042374
GH Action: rename new migration file timestamp
2024-05-18 07:33:38 -07:00
d1997f04c0 chore: renamed new migration files to latest timestamp (gh-action) 2024-05-18 14:26:13 +00:00
deefaa0961 Merge pull request from Infisical/k8s-auth
Kubernetes Native Authentication Method
2024-05-18 07:25:52 -07:00
a392c9f022 Move k8s migration to front 2024-05-17 22:41:33 -07:00
34222b83ee review fixes for k8s auth 2024-05-17 21:44:02 -04:00
ef36852a47 Add access token trusted ip support to k8s auth 2024-05-17 15:41:32 -07:00
d79fd826a4 Merge remote-tracking branch 'origin' into k8s-auth 2024-05-17 15:39:52 -07:00
18aaa423a9 Merge pull request from Infisical/patch-gcp-id-token-auth
Patch Identity Access Token Trusted IPs validation for AWS/GCP Auth
2024-05-17 18:38:15 -04:00
32c33eaf6e Patch identity token trusted ips validation for aws/gcp auths 2024-05-17 11:58:08 -07:00
702699b4f0 Update faq.mdx 2024-05-17 12:13:11 -04:00
35ee03d347 Merge pull request from akhilmhdh/fix/validation-permission
feat: added validation for project permission body in identity specific privilege
2024-05-17 11:50:35 -04:00
=
9c5deee688 feat: added validation for project permission body in identity specific privilege 2024-05-17 21:09:50 +05:30
1438415d0c Merge pull request from Cristobal-M/feat-support-imports-in-cli-export
feat(cli): support of include-imports in export command
2024-05-17 14:21:34 +05:30
eca0e62764 Merge pull request from akhilmhdh/feat/revoke-access-token
Revoke access token endpoint
2024-05-16 23:41:38 +05:30
e4186f0317 Merge pull request from akhilmhdh/fix/aws-parameter-stoer
fix: get all secrets from aws ssm
2024-05-16 12:27:20 -04:00
=
704c630797 feat: added rate limit for sync secrets 2024-05-16 21:34:31 +05:30
f398fee2b8 make var readable 2024-05-16 11:43:32 -04:00
=
7fce51e8c1 fix: get all secrets from aws ssm 2024-05-16 20:51:07 +05:30
a6fe233122 Feat: missing documentation for include-imports in export and run command 2024-05-16 11:44:29 +02:00
5e678b1ad2 Merge pull request from akhilmhdh/fix/create-secret-fail-reference
fix: resolved create secret failing for reference
2024-05-15 22:34:37 -04:00
cf453e87d8 Merge pull request from Infisical/daniel/fix-expansion
Fix: Fix secret expansion II
2024-05-16 08:02:41 +05:30
=
4af703df5b fix: resolved create secret failing for reference 2024-05-16 07:35:05 +05:30
75b8b521b3 Update secret-service.ts 2024-05-16 03:31:01 +02:00
58c1d3b0ac Merge pull request from Infisical/daniel/fix-secret-expand-with-recursive
Fix: Secret expansion with recursive mode enabled
2024-05-16 02:33:28 +02:00
6b5cafa631 Merge pull request from Infisical/patch-update-project-identity
patch project identity update
2024-05-15 20:23:09 -04:00
4a35623956 remove for of with for await 2024-05-15 20:19:10 -04:00
74fe673724 patch project identity update 2024-05-15 20:12:45 -04:00
2f92719771 Fix: Secret expansion with recursive mode 2024-05-16 00:29:07 +02:00
399ca7a221 Merge pull request from justin1121/patch-1
Update secret-versioning.mdx
2024-05-15 15:34:03 +05:30
=
29f37295e1 docs: added revoke token api to api-reference 2024-05-15 15:27:26 +05:30
=
e3184a5f40 feat(api): added revoke access token endpoint 2024-05-15 15:26:38 +05:30
ace008f44e Make rejectUnauthorized true if ca cert is passed for k8s auth method 2024-05-14 22:49:37 -07:00
4afd95fe1a Merge pull request from akhilmhdh/feat/sync-integration-inline
Secret reference and integration sync support
2024-05-15 01:36:19 -04:00
3cd719f6b0 update index secret references button 2024-05-15 09:57:24 +05:30
c6352cc970 updated texts and comments 2024-05-15 09:57:24 +05:30
=
d4555f9698 feat: ui for reindex secret reference 2024-05-15 09:57:24 +05:30
=
393964c4ae feat: implemented inline secret reference integration sync 2024-05-15 09:57:23 +05:30
e4afbe8662 Update k8s auth docs 2024-05-14 20:44:09 -07:00
0d89aa8607 Add docs for K8s auth method 2024-05-14 18:02:05 -07:00
2b91ec5ae9 Fix merge conflicts 2024-05-14 13:37:39 -07:00
c438479246 update prod pipeline names 2024-05-14 16:14:42 -04:00
9828cbbfbe Update secret-versioning.mdx 2024-05-14 16:28:43 -03:00
cd910a2fac Update k8s auth impl to be able to test ca, tokenReviewerjwt locally 2024-05-14 11:42:26 -07:00
fc1dffd7e2 Merge pull request from Infisical/snyk-fix-a2a4b055e42c14d5cbdb505e7670d300
[Snyk] Security upgrade bullmq from 5.3.3 to 5.4.2
2024-05-14 12:02:13 -04:00
55f8198a2d Merge pull request from matthewaerose/patch-1
Fix: Correct typo from 'Halm' to 'Helm'
2024-05-14 11:46:49 -04:00
4d166402df Merge pull request from Infisical/create-pull-request/patch-1715660210
GH Action: rename new migration file timestamp
2024-05-14 00:17:34 -04:00
19edf83dbc chore: renamed new migration files to latest timestamp (gh-action) 2024-05-14 04:16:49 +00:00
0c8ef13d8d Fix: Correct typo from 'Halm' to 'Helm' 2024-05-13 13:38:09 -05:00
c45dae4137 Merge remote-tracking branch 'origin' into k8s-auth 2024-05-12 16:16:44 -07:00
bd57a068d1 Fix merge conflicts 2024-05-12 08:43:29 -07:00
c276c44c08 Finish preliminary backend endpoints / db structure for k8s auth 2024-05-05 19:14:49 -07:00
71 changed files with 2967 additions and 275 deletions
.github/workflows
backend/src
docker-compose.dev.yml
docs
frontend/src
hooks/api
views
IntegrationsPage/components/IntegrationsSection
Org/MembersPage/components/OrgIdentityTab/components/IdentitySection
Settings/ProjectSettingsPage/components

@ -122,13 +122,13 @@ jobs:
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-prod-platform
container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service

@ -34,6 +34,7 @@ import { TIdentityServiceFactory } from "@app/services/identity/identity-service
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@ -117,6 +118,7 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;

@ -65,6 +65,9 @@ import {
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate,
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
@ -231,6 +234,7 @@ import {
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
declare module "knex/types/tables" {
interface Tables {
@ -304,6 +308,11 @@ declare module "knex/types/tables" {
>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretReference]: Knex.CompositeTableType<
TSecretReferences,
TSecretReferencesInsert,
TSecretReferencesUpdate
>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
@ -332,6 +341,11 @@ declare module "knex/types/tables" {
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate
>;
[TableName.IdentityKubernetesAuth]: Knex.CompositeTableType<
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate
>;
[TableName.IdentityGcpAuth]: Knex.CompositeTableType<
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,

@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.SecretReference))) {
await knex.schema.createTable(TableName.SecretReference, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("environment").notNullable();
t.string("secretPath").notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.SecretReference);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretReference);
await dropOnUpdateTrigger(knex, TableName.SecretReference);
}

@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityKubernetesAuth))) {
await knex.schema.createTable(TableName.IdentityKubernetesAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("kubernetesHost").notNullable();
t.text("encryptedCaCert").notNullable();
t.string("caCertIV").notNullable();
t.string("caCertTag").notNullable();
t.text("encryptedTokenReviewerJwt").notNullable();
t.string("tokenReviewerJwtIV").notNullable();
t.string("tokenReviewerJwtTag").notNullable();
t.string("allowedNamespaces").notNullable();
t.string("allowedNames").notNullable();
t.string("allowedAudience").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityKubernetesAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityKubernetesAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityKubernetesAuth);
}

@ -0,0 +1,43 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasIsSyncedColumn = await knex.schema.hasColumn(TableName.Integration, "isSynced");
const hasSyncMessageColumn = await knex.schema.hasColumn(TableName.Integration, "syncMessage");
const hasLastSyncJobId = await knex.schema.hasColumn(TableName.Integration, "lastSyncJobId");
await knex.schema.alterTable(TableName.Integration, (t) => {
if (!hasIsSyncedColumn) {
t.boolean("isSynced").nullable();
}
if (!hasSyncMessageColumn) {
t.text("syncMessage").nullable();
}
if (!hasLastSyncJobId) {
t.string("lastSyncJobId").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasIsSyncedColumn = await knex.schema.hasColumn(TableName.Integration, "isSynced");
const hasSyncMessageColumn = await knex.schema.hasColumn(TableName.Integration, "syncMessage");
const hasLastSyncJobId = await knex.schema.hasColumn(TableName.Integration, "lastSyncJobId");
await knex.schema.alterTable(TableName.Integration, (t) => {
if (hasIsSyncedColumn) {
t.dropColumn("isSynced");
}
if (hasSyncMessageColumn) {
t.dropColumn("syncMessage");
}
if (hasLastSyncJobId) {
t.dropColumn("lastSyncJobId");
}
});
}

@ -0,0 +1,35 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityKubernetesAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
kubernetesHost: z.string(),
encryptedCaCert: z.string(),
caCertIV: z.string(),
caCertTag: z.string(),
encryptedTokenReviewerJwt: z.string(),
tokenReviewerJwtIV: z.string(),
tokenReviewerJwtTag: z.string(),
allowedNamespaces: z.string(),
allowedNames: z.string(),
allowedAudience: z.string()
});
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
export type TIdentityKubernetesAuthsInsert = Omit<z.input<typeof IdentityKubernetesAuthsSchema>, TImmutableDBKeys>;
export type TIdentityKubernetesAuthsUpdate = Partial<
Omit<z.input<typeof IdentityKubernetesAuthsSchema>, TImmutableDBKeys>
>;

@ -19,6 +19,7 @@ export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-aws-auths";
export * from "./identity-gcp-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";

@ -28,7 +28,10 @@ export const IntegrationsSchema = z.object({
secretPath: z.string().default("/"),
createdAt: z.date(),
updatedAt: z.date(),
lastUsed: z.date().nullable().optional()
lastUsed: z.date().nullable().optional(),
isSynced: z.boolean().nullable().optional(),
syncMessage: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional()
});
export type TIntegrations = z.infer<typeof IntegrationsSchema>;

@ -28,6 +28,7 @@ export enum TableName {
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
Secret = "secrets",
SecretReference = "secret_references",
SecretBlindIndex = "secret_blind_indexes",
SecretVersion = "secret_versions",
SecretFolder = "secret_folders",
@ -44,6 +45,7 @@ export enum TableName {
Identity = "identities",
IdentityAccessToken = "identity_access_tokens",
IdentityUniversalAuth = "identity_universal_auths",
IdentityKubernetesAuth = "identity_kubernetes_auths",
IdentityGcpAuth = "identity_gcp_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
@ -145,6 +147,7 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod {
Univeral = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth"
}

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretReferencesSchema = z.object({
id: z.string().uuid(),
environment: z.string(),
secretPath: z.string(),
secretId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretReferences = z.infer<typeof SecretReferencesSchema>;
export type TSecretReferencesInsert = Omit<z.input<typeof SecretReferencesSchema>, TImmutableDBKeys>;
export type TSecretReferencesUpdate = Partial<Omit<z.input<typeof SecretReferencesSchema>, TImmutableDBKeys>>;

@ -8,7 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { PermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
import { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@ -39,7 +39,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
}),
response: {
200: z.object({
@ -90,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@ -155,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)

@ -51,6 +51,7 @@ export enum EventType {
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration",
MANUAL_SYNC_INTEGRATION = "manual-sync-integration",
ADD_TRUSTED_IP = "add-trusted-ip",
UPDATE_TRUSTED_IP = "update-trusted-ip",
DELETE_TRUSTED_IP = "delete-trusted-ip",
@ -63,6 +64,10 @@ export enum EventType {
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
@ -277,6 +282,25 @@ interface DeleteIntegrationEvent {
};
}
interface ManualSyncIntegrationEvent {
type: EventType.MANUAL_SYNC_INTEGRATION;
metadata: {
integrationId: string;
integration: string;
environment: string;
secretPath: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
path?: string;
region?: string;
};
}
interface AddTrustedIPEvent {
type: EventType.ADD_TRUSTED_IP;
metadata: {
@ -391,6 +415,50 @@ interface GetIdentityUniversalAuthEvent {
};
}
interface LoginIdentityKubernetesAuthEvent {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
metadata: {
identityId: string;
identityKubernetesAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityKubernetesAuthEvent {
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH;
metadata: {
identityId: string;
kubernetesHost: string;
allowedNamespaces: string;
allowedNames: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityKubernetesAuthEvent {
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH;
metadata: {
identityId: string;
kubernetesHost?: string;
allowedNamespaces?: string;
allowedNames?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityKubernetesAuthEvent {
type: EventType.GET_IDENTITY_KUBERNETES_AUTH;
metadata: {
identityId: string;
};
}
interface CreateIdentityUniversalAuthClientSecretEvent {
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
metadata: {
@ -743,6 +811,7 @@ export type Event =
| UnauthorizeIntegrationEvent
| CreateIntegrationEvent
| DeleteIntegrationEvent
| ManualSyncIntegrationEvent
| AddTrustedIPEvent
| UpdateTrustedIPEvent
| DeleteTrustedIPEvent
@ -755,6 +824,10 @@ export type Event =
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| LoginIdentityKubernetesAuthEvent
| AddIdentityKubernetesAuthEvent
| UpdateIdentityKubernetesAuthEvent
| GetIdentityKubernetesAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent

@ -7,12 +7,15 @@ import {
SecretType,
TSecretApprovalRequestsSecretsInsert
} from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { getAllNestedSecretReferences } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
@ -53,6 +56,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretService: Pick<
TSecretServiceFactory,
| "fnSecretBulkInsert"
@ -80,7 +84,8 @@ export const secretApprovalRequestServiceFactory = ({
snapshotService,
secretService,
secretVersionDAL,
secretQueueService
secretQueueService,
projectBotService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -352,7 +357,7 @@ export const secretApprovalRequestServiceFactory = ({
}
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Delete);
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
? await secretService.fnSecretBulkInsert({
@ -379,7 +384,17 @@ export const secretApprovalRequestServiceFactory = ({
]),
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared
type: SecretType.Shared,
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
})),
secretDAL,
secretVersionDAL,
@ -414,7 +429,17 @@ export const secretApprovalRequestServiceFactory = ({
"secretReminderNote",
"secretReminderRepeatDays",
"secretBlindIndex"
])
]),
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
}
})),
secretDAL,

@ -89,6 +89,9 @@ export const UNIVERSAL_AUTH = {
},
RENEW_ACCESS_TOKEN: {
accessToken: "The access token to renew."
},
REVOKE_ACCESS_TOKEN: {
accessToken: "The access token to revoke."
}
} as const;
@ -646,6 +649,9 @@ export const INTEGRATION = {
},
DELETE: {
integrationId: "The ID of the integration object."
},
SYNC: {
integrationId: "The ID of the integration object to manually sync"
}
};

@ -65,7 +65,13 @@ export type TQueueJobTypes = {
};
[QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync;
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
payload: {
projectId: string;
environment: string;
secretPath: string;
depth?: number;
deDupeQueue?: Record<string, boolean>;
};
};
[QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan;

@ -82,6 +82,8 @@ import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/ident
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -207,6 +209,7 @@ export const registerRoutes = async (
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
@ -607,6 +610,7 @@ export const registerRoutes = async (
});
const sarService = secretApprovalRequestServiceFactory({
permissionService,
projectBotService,
folderDAL,
secretDAL,
secretTagDAL,
@ -711,6 +715,15 @@ export const registerRoutes = async (
identityUaDAL,
licenseService
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
});
const identityGcpAuthService = identityGcpAuthServiceFactory({
identityGcpAuthDAL,
identityOrgMembershipDAL,
@ -797,6 +810,7 @@ export const registerRoutes = async (
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityUa: identityUaService,
identityKubernetesAuth: identityKubernetesAuthService,
identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService,
secretApprovalPolicy: sapService,

@ -8,6 +8,7 @@ import {
UsersSchema
} from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
@ -64,14 +65,12 @@ export const secretRawSchema = z.object({
secretComment: z.string().optional()
});
export const PermissionSchema = z.object({
export const ProjectPermissionSchema = z.object({
action: z
.string()
.min(1)
.nativeEnum(ProjectPermissionActions)
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read"),
subject: z
.string()
.min(1)
.nativeEnum(ProjectPermissionSub)
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
conditions: z
.object({

@ -36,4 +36,29 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
};
}
});
server.route({
url: "/token/revoke",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
description: "Revoke access token",
body: z.object({
accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken)
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.identityAccessToken.revokeAccessToken(req.body.accessToken);
return {
message: "Successfully revoked access token"
};
}
});
};

@ -0,0 +1,283 @@
import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({
encryptedCaCert: true,
caCertIV: true,
caCertTag: true,
encryptedTokenReviewerJwt: true,
tokenReviewerJwtIV: true,
tokenReviewerJwtTag: true
}).extend({
caCert: z.string(),
tokenReviewerJwt: z.string()
});
export const registerIdentityKubernetesRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/kubernetes-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with Kubernetes Auth",
body: z.object({
identityId: z.string().trim(),
jwt: z.string().trim()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityKubernetesAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH,
metadata: {
identityId: identityKubernetesAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityKubernetesAuthId: identityKubernetesAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/kubernetes-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach Kubernetes Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
kubernetesHost: z.string().trim().min(1),
caCert: z.string().trim().default(""),
tokenReviewerJwt: z.string().trim().min(1),
allowedNamespaces: z.string(), // TODO: validation
allowedNames: z.string(),
allowedAudience: z.string(),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
})
}
},
handler: async (req) => {
const identityKubernetesAuth = await server.services.identityKubernetesAuth.attachKubernetesAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityKubernetesAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
metadata: {
identityId: identityKubernetesAuth.identityId,
kubernetesHost: identityKubernetesAuth.kubernetesHost,
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
allowedNames: identityKubernetesAuth.allowedNames,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityKubernetesAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
}
}
});
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
}
});
server.route({
method: "PATCH",
url: "/kubernetes-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Kubernetes Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
body: z.object({
kubernetesHost: z.string().trim().min(1).optional(),
caCert: z.string().trim().optional(),
tokenReviewerJwt: z.string().trim().min(1).optional(),
allowedNamespaces: z.string().optional(), // TODO: validation
allowedNames: z.string().optional(),
allowedAudience: z.string().optional(),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
accessTokenTTL: z.number().int().min(0).optional(),
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthsSchema
})
}
},
handler: async (req) => {
const identityKubernetesAuth = await server.services.identityKubernetesAuth.updateKubernetesAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityKubernetesAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
metadata: {
identityId: identityKubernetesAuth.identityId,
kubernetesHost: identityKubernetesAuth.kubernetesHost,
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
allowedNames: identityKubernetesAuth.allowedNames,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityKubernetesAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
}
}
});
return { identityKubernetesAuth };
}
});
server.route({
method: "GET",
url: "/kubernetes-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve Kubernetes Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
})
}
},
handler: async (req) => {
const identityKubernetesAuth = await server.services.identityKubernetesAuth.getKubernetesAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityKubernetesAuth.orgId,
event: {
type: EventType.GET_IDENTITY_KUBERNETES_AUTH,
metadata: {
identityId: identityKubernetesAuth.identityId
}
}
});
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
}
});
};

@ -4,6 +4,7 @@ import { registerProjectBotRouter } from "./bot-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
@ -29,6 +30,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (authRouter) => {
await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityKubernetesRouter);
await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter);

@ -262,5 +262,64 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}
});
// TODO(akhilmhdh-pg): manual sync
server.route({
method: "POST",
url: "/:integrationId/sync",
config: {
rateLimit: writeLimit
},
schema: {
description: "Manually trigger sync of an integration by integration id",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.SYNC.integrationId)
}),
response: {
200: z.object({
integration: IntegrationsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.syncIntegration({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: integration.projectId,
event: {
type: EventType.MANUAL_SYNC_INTEGRATION,
// eslint-disable-next-line
metadata: shake({
integrationId: integration.id,
integration: integration.integration,
environment: integration.environment.slug,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
// eslint-disable-next-line
}) as any
}
});
return { integration };
}
});
};

@ -1926,4 +1926,41 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
return { secrets };
}
});
server.route({
method: "POST",
url: "/backfill-secret-references",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Backfill secret references",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId } = req.body;
const message = await server.services.secret.backfillSecretReferences({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId
});
return message;
}
});
};

@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
@ -15,23 +15,56 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const doc = await (tx || db)(TableName.IdentityAccessToken)
.where(filter)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.leftJoin(
TableName.IdentityUaClientSecret,
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
`${TableName.IdentityUaClientSecret}.id`
)
.leftJoin(
TableName.IdentityUniversalAuth,
`${TableName.IdentityUaClientSecret}.identityUAId`,
`${TableName.IdentityUniversalAuth}.id`
)
.leftJoin(TableName.IdentityUaClientSecret, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn(
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
`${TableName.IdentityUaClientSecret}.id`
);
})
.leftJoin(TableName.IdentityUniversalAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn(
`${TableName.IdentityUaClientSecret}.identityUAId`,
`${TableName.IdentityUniversalAuth}.id`
);
})
.leftJoin(TableName.IdentityGcpAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.GCP_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityGcpAuth}.identityId`
);
})
.leftJoin(TableName.IdentityAwsAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AWS_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityAwsAuth}.identityId`
);
})
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
);
})
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
return doc;
if (!doc) return;
return {
...doc,
accessTokenTrustedIps:
doc.accessTokenTrustedIpsUa ||
doc.accessTokenTrustedIpsGcp ||
doc.accessTokenTrustedIpsAws ||
doc.accessTokenTrustedIpsK8s
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
}

@ -106,6 +106,24 @@ export const identityAccessTokenServiceFactory = ({
return { accessToken, identityAccessToken: updatedIdentityAccessToken };
};
const revokeAccessToken = async (accessToken: string) => {
const appCfg = getConfig();
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & {
identityAccessTokenId: string;
};
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw new UnauthorizedError();
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: decodedToken.identityAccessTokenId,
isAccessTokenRevoked: false
});
if (!identityAccessToken) throw new UnauthorizedError();
const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id);
return { revokedToken };
};
const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId,
@ -132,5 +150,5 @@ export const identityAccessTokenServiceFactory = ({
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
};
return { renewAccessToken, fnValidateIdentityAccessToken };
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
};

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityKubernetesAuthDALFactory = ReturnType<typeof identityKubernetesAuthDALFactory>;
export const identityKubernetesAuthDALFactory = (db: TDbClient) => {
const kubernetesAuthOrm = ormify(db, TableName.IdentityKubernetesAuth);
return kubernetesAuthOrm;
};

@ -0,0 +1,15 @@
/**
* Extracts the K8s service account name and namespace
* from the username in this format: system:serviceaccount:default:infisical-auth
*/
export const extractK8sUsername = (username: string) => {
const parts = username.split(":");
// Ensure that the username format is correct
if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount") {
return {
namespace: parts[2],
name: parts[3]
};
}
throw new Error("Invalid username format");
};

@ -0,0 +1,515 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
TAttachKubernetesAuthDTO,
TCreateTokenReviewResponse,
TGetKubernetesAuthDTO,
TLoginKubernetesAuthDTO,
TUpdateKubernetesAuthDTO
} from "./identity-kubernetes-auth-types";
type TIdentityKubernetesAuthServiceFactoryDep = {
identityKubernetesAuthDAL: Pick<
TIdentityKubernetesAuthDALFactory,
"create" | "findOne" | "transaction" | "updateById"
>;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
export const identityKubernetesAuthServiceFactory = ({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
}: TIdentityKubernetesAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityKubernetesAuth.identityId
});
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
identityKubernetesAuth;
let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}
let tokenReviewerJwt = "";
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
tokenReviewerJwt = decryptSymmetric({
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag,
key
});
}
const { data }: { data: TCreateTokenReviewResponse } = await axios.post(
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
);
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error });
// check the response to determine if the token is valid
if (!(data.status && data.status.authenticated)) throw new UnauthorizedError();
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
if (identityKubernetesAuth.allowedNamespaces) {
// validate if [targetNamespace] is in the list of allowed namespaces
const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces
.split(",")
.map((namespace) => namespace.trim())
.some((namespace) => namespace === targetNamespace);
if (!isNamespaceAllowed) throw new UnauthorizedError();
}
if (identityKubernetesAuth.allowedNames) {
// validate if [targetName] is in the list of allowed names
const isNameAllowed = identityKubernetesAuth.allowedNames
.split(",")
.map((name) => name.trim())
.some((name) => name === targetName);
if (!isNameAllowed) throw new UnauthorizedError();
}
if (identityKubernetesAuth.allowedAudience) {
// validate if [audience] is in the list of allowed audiences
const isAudienceAllowed = data.status.audiences.some(
(audience) => audience === identityKubernetesAuth.allowedAudience
);
if (!isAudienceAllowed) throw new UnauthorizedError();
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityKubernetesAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityKubernetesAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };
};
const attachKubernetesAuth = async ({
identityId,
kubernetesHost,
caCert,
tokenReviewerJwt,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add Kubernetes Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: identityMembershipOrg.orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
const {
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag
} = encryptSymmetric(tokenReviewerJwt, key);
const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
const doc = await identityKubernetesAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
kubernetesHost,
encryptedCaCert,
caCertIV,
caCertTag,
encryptedTokenReviewerJwt,
tokenReviewerJwtIV,
tokenReviewerJwtTag,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
);
return doc;
});
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
const updateKubernetesAuth = async ({
identityId,
kubernetesHost,
caCert,
tokenReviewerJwt,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
throw new BadRequestError({
message: "Failed to update Kubernetes Auth"
});
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityKubernetesAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityKubernetesAuth.accessTokenMaxTTL) >
(accessTokenMaxTTL || identityKubernetesAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (caCert !== undefined) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCaCert = encryptedCACert;
updateQuery.caCertIV = caCertIV;
updateQuery.caCertTag = caCertTag;
}
if (tokenReviewerJwt !== undefined) {
const {
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag
} = encryptSymmetric(tokenReviewerJwt, key);
updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt;
updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV;
updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag;
}
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
return { ...updatedKubernetesAuth, orgId: identityMembershipOrg.orgId };
};
const getKubernetesAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
throw new BadRequestError({
message: "The identity does not have Kubernetes Auth attached"
});
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
identityKubernetesAuth;
let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}
let tokenReviewerJwt = "";
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
tokenReviewerJwt = decryptSymmetric({
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag,
key
});
}
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachKubernetesAuth,
updateKubernetesAuth,
getKubernetesAuth
};
};

@ -0,0 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginKubernetesAuthDTO = {
identityId: string;
jwt: string;
};
export type TAttachKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
caCert: string;
tokenReviewerJwt: string;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateKubernetesAuthDTO = {
identityId: string;
kubernetesHost?: string;
caCert?: string;
tokenReviewerJwt?: string;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetKubernetesAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
type TCreateTokenReviewSuccessResponse = {
authenticated: true;
user: {
username: string;
uid: string;
groups: string[];
};
audiences: string[];
};
type TCreateTokenReviewErrorResponse = {
error: string;
};
export type TCreateTokenReviewResponse = {
apiVersion: "authentication.k8s.io/v1";
kind: "TokenReview";
spec: {
token: string;
};
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
};

@ -82,6 +82,7 @@ export const identityProjectServiceFactory = ({
role,
project.id
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
@ -135,16 +136,18 @@ export const identityProjectServiceFactory = ({
message: `Identity with id ${identityId} doesn't exists in project with id ${projectId}`
});
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY,
projectIdentity.identityId,
projectIdentity.projectId,
actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
for await (const { role: requestedRoleChange } of roles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
requestedRoleChange,
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
}
// validate custom roles input
const customInputRoles = roles.filter(

@ -462,27 +462,39 @@ const syncSecretsAWSParameterStore = async ({
ssm.config.update(config);
const metadata = z.record(z.any()).parse(integration.metadata || {});
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
const params = {
Path: integration.path as string,
Recursive: false,
WithDecryption: true
};
// now fetch all aws parameter store secrets
let hasNext = true;
let nextToken: string | undefined;
while (hasNext) {
const parameters = await ssm
.getParametersByPath({
Path: integration.path as string,
Recursive: false,
WithDecryption: true,
MaxResults: 10,
NextToken: nextToken
})
.promise();
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters;
if (parameters.Parameters) {
parameters.Parameters.forEach((parameter) => {
if (parameter.Name) {
const secKey = parameter.Name.substring((integration.path as string).length);
awsParameterStoreSecretsObj[secKey] = parameter;
}
});
}
hasNext = Boolean(parameters.NextToken);
nextToken = parameters.NextToken;
}
const awsParameterStoreSecretsObj = (parameterList || [])
.filter(({ Name }) => Boolean(Name))
.reduce(
(obj, secret) => ({
...obj,
[(secret.Name as string).substring((integration.path as string).length)]: secret
}),
{} as Record<string, AWS.SSM.Parameter>
);
// Identify secrets to create
await Promise.all(
Object.keys(secrets).map(async (key) => {
// don't use Promise.all() and promise map here
// it will cause rate limit
for (const key in secrets) {
if (Object.hasOwn(secrets, key)) {
if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store
// -> create secret
@ -517,13 +529,16 @@ const syncSecretsAWSParameterStore = async ({
})
.promise();
}
})
);
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
if (!metadata.shouldDisableDelete) {
// Identify secrets to delete
await Promise.all(
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
for (const key in awsParameterStoreSecretsObj) {
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
if (!(key in secrets)) {
// case:
// -> delete secret
@ -533,8 +548,11 @@ const syncSecretsAWSParameterStore = async ({
})
.promise();
}
})
);
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
}
};

@ -9,7 +9,12 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TIntegrationDALFactory } from "./integration-dal";
import { TCreateIntegrationDTO, TDeleteIntegrationDTO, TUpdateIntegrationDTO } from "./integration-types";
import {
TCreateIntegrationDTO,
TDeleteIntegrationDTO,
TSyncIntegrationDTO,
TUpdateIntegrationDTO
} from "./integration-types";
type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory;
@ -201,10 +206,35 @@ export const integrationServiceFactory = ({
return integrations;
};
const syncIntegration = async ({ id, actorId, actor, actorOrgId, actorAuthMethod }: TSyncIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) {
throw new BadRequestError({ message: "Integration not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integration.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
await secretQueueService.syncIntegrations({
environment: integration.environment.slug,
secretPath: integration.secretPath,
projectId: integration.projectId
});
return { ...integration, envId: integration.environment.id };
};
return {
createIntegration,
updateIntegration,
deleteIntegration,
listIntegrationByProject
listIntegrationByProject,
syncIntegration
};
};

@ -59,3 +59,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

@ -243,6 +243,74 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const upsertSecretReferences = async (
data: {
secretId: string;
references: Array<{ environment: string; secretPath: string }>;
}[] = [],
tx?: Knex
) => {
try {
if (!data.length) return;
await (tx || db)(TableName.SecretReference)
.whereIn(
"secretId",
data.map(({ secretId }) => secretId)
)
.delete();
const newSecretReferences = data
.filter(({ references }) => references.length)
.flatMap(({ secretId, references }) =>
references.map(({ environment, secretPath }) => ({
secretPath,
secretId,
environment
}))
);
if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReference).insert(newSecretReferences);
return secretReferences;
} catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" });
}
};
const findReferencedSecretReferences = async (projectId: string, envSlug: string, secretPath: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretReference)
.where({
secretPath,
environment: envSlug
})
.join(TableName.Secret, `${TableName.Secret}.id`, `${TableName.SecretReference}.secretId`)
.join(TableName.SecretFolder, `${TableName.Secret}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
.select(selectAllTableCols(TableName.SecretReference))
.select("folderId");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindReferencedSecretReferences" });
}
};
// special query to backfill secret value
const findAllProjectSecretValues = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.Secret)
.join(TableName.SecretFolder, `${TableName.Secret}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
// not empty
.whereNotNull("secretValueCiphertext")
.select("secretValueTag", "secretValueCiphertext", "secretValueIV", `${TableName.Secret}.id` as "id");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindAllProjectSecretValues" });
}
};
return {
...secretOrm,
update,
@ -252,6 +320,9 @@ export const secretDALFactory = (db: TDbClient) => {
getSecretTags,
findByFolderId,
findByFolderIds,
findByBlindIndexes
findByBlindIndexes,
upsertSecretReferences,
findReferencedSecretReferences,
findAllProjectSecretValues
};
};

@ -194,6 +194,7 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
@ -235,7 +236,6 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
};
};
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
@ -353,7 +353,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
};
export const decryptSecretRaw = (
secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
secret: TSecrets & { workspace: string; environment: string; secretPath: string },
key: string
) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
@ -396,6 +396,37 @@ export const decryptSecretRaw = (
};
};
/**
* Grabs and processes nested secret references from a string
*
* This function looks for patterns that match the interpolation syntax in the input string.
* It filters out references that include nested paths, splits them into environment and
* secret path parts, and then returns an array of objects with the environment and the
* joined secret path.
*
* @param {string} maybeSecretReference - The string that has the potential secret references.
* @returns {Array<{ environment: string, secretPath: string }>} - An array of objects
* with the environment and joined secret path.
*
* @example
* const value = "Hello ${dev.someFolder.OtherFolder.SECRET_NAME} and ${prod.anotherFolder.SECRET_NAME}";
* const result = getAllNestedSecretReferences(value);
* // result will be:
* // [
* // { environment: 'dev', secretPath: '/someFolder/OtherFolder' },
* // { environment: 'prod', secretPath: '/anotherFolder' }
* // ]
*/
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
return references
.filter((el) => el.includes("."))
.map((el) => {
const [environment, ...secretPathList] = el.split(".");
return { environment, secretPath: path.join("/", ...secretPathList.slice(0, -1)) };
});
};
/**
* Checks and handles secrets using a blind index method.
* The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes.
@ -467,7 +498,7 @@ export const fnSecretBulkInsert = async ({
tx
}: TFnSecretBulkInsert) => {
const newSecrets = await secretDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })),
inputSecrets.map(({ tags, references, ...el }) => ({ ...el, folderId })),
tx
);
const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string);
@ -478,13 +509,20 @@ export const fnSecretBulkInsert = async ({
}))
);
const secretVersions = await secretVersionDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({
inputSecrets.map(({ tags, references, ...el }) => ({
...el,
folderId,
secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id
})),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], secretBlindIndex }) => ({
secretId: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id,
references
})),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
@ -509,7 +547,7 @@ export const fnSecretBulkUpdate = async ({
secretVersionTagDAL
}: TFnSecretBulkUpdate) => {
const newSecrets = await secretDAL.bulkUpdate(
inputSecrets.map(({ filter, data: { tags, ...data } }) => ({
inputSecrets.map(({ filter, data: { tags, references, ...data } }) => ({
filter: { ...filter, folderId },
data
})),
@ -522,6 +560,15 @@ export const fnSecretBulkUpdate = async ({
})),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets
.filter(({ data: { references } }) => Boolean(references))
.map(({ data: { references = [] } }, i) => ({
secretId: newSecrets[i].id,
references
})),
tx
);
const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags !== undefined ? { tags, secretId: newSecrets[i].id } : []
);
@ -591,50 +638,39 @@ export const createManySecretsRawFnFactory = ({
folderId,
isNew: true,
blindIndexCfg,
userId,
secretDAL
});
const inputSecrets = await Promise.all(
secrets.map(async (secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
const inputSecrets = secrets.map((secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretReferences = getAllNestedSecretReferences(secret.secretValue || "");
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags,
references: secretReferences
};
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to create personal secret override for no corresponding shared secret"
});
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
// get all tags
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" });
const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({
@ -703,56 +739,35 @@ export const updateManySecretsRawFnFactory = ({
userId
});
const inputSecrets = await Promise.all(
secrets.map(async (secret) => {
if (secret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
}
const inputSecrets = secrets.map((secret) => {
if (secret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
}
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretReferences = getAllNestedSecretReferences(secret.secretValue || "");
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to update personal secret override for no corresponding shared secret"
});
if (secret.newSecretName)
throw new BadRequestError({ message: "Personal secret cannot change the key name" });
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
newSecretName: secret.newSecretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
newSecretName: secret.newSecretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags,
references: secretReferences
};
});
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];

@ -59,6 +59,7 @@ export type TGetSecrets = {
};
const MAX_SYNC_SECRET_DEPTH = 5;
const uniqueIntegrationKey = (environment: string, secretPath: string) => `integration-${environment}-${secretPath}`;
export const secretQueueFactory = ({
queueService,
@ -102,28 +103,35 @@ export const secretQueueFactory = ({
folderDAL
});
const syncIntegrations = async (dto: TGetSecrets) => {
const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 5,
attempts: 3,
delay: 1000,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: {
count: 5 // keep the most recent jobs
}
removeOnFail: true
});
};
const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => {
const syncSecrets = async ({
deDupeQueue = {},
...dto
}: TGetSecrets & { depth?: number; deDupeQueue?: Record<string, boolean> }) => {
const deDuplicationKey = uniqueIntegrationKey(dto.environment, dto.secretPath);
if (deDupeQueue?.[deDuplicationKey]) {
return;
}
// eslint-disable-next-line
deDupeQueue[deDuplicationKey] = true;
logger.info(
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]`
);
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
removeOnFail: { count: 5 },
removeOnFail: true,
removeOnComplete: true,
delay: 1000,
attempts: 5,
@ -132,7 +140,7 @@ export const secretQueueFactory = ({
delay: 3000
}
});
await syncIntegrations(dto);
await syncIntegrations({ ...dto, deDupeQueue });
};
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
@ -326,7 +334,7 @@ export const secretQueueFactory = ({
};
queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath, depth = 1 } = job.data;
const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) {
@ -349,21 +357,68 @@ export const secretQueueFactory = ({
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id);
logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
.map(({ folderId }) => {
const syncDto = {
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueIntegrationKey(
foldersGroupedById[folderId][0].environmentSlug,
foldersGroupedById[folderId][0].path
)
]
)
.map(({ folderId }) =>
syncSecrets({
depth: depth + 1,
projectId,
secretPath: foldersGroupedById[folderId][0].path,
environment: foldersGroupedById[folderId][0].environmentSlug
};
logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
return syncSecrets(syncDto);
})
environment: foldersGroupedById[folderId][0].environmentSlug,
deDupeQueue
})
)
);
}
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
if (secretReferences.length) {
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders, (i) => i.child || i.id);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
secretReferences
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0].path))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueIntegrationKey(
referencedFoldersGroupedById[folderId][0].environmentSlug,
referencedFoldersGroupedById[folderId][0].path
)
]
)
.map(({ folderId }) =>
syncSecrets({
depth: depth + 1,
projectId,
secretPath: referencedFoldersGroupedById[folderId][0].path,
environment: referencedFoldersGroupedById[folderId][0].environmentSlug,
deDupeQueue
})
)
);
}
} else {
@ -408,20 +463,37 @@ export const secretQueueFactory = ({
});
}
await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
accessToken,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
try {
await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
accessToken,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: "",
isSynced: true
});
} catch (err: unknown) {
logger.info("Secret integration sync error:", err);
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: (err as Error)?.message,
isSynced: false
});
}
}
logger.info("Secret integration sync ended: %s", job.id);

@ -2,12 +2,22 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
import {
ProjectMembershipRole,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretsSchema,
SecretType
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import {
buildSecretBlindIndexFromName,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
@ -27,12 +37,14 @@ import {
fnSecretBlindIndexCheck,
fnSecretBulkInsert,
fnSecretBulkUpdate,
getAllNestedSecretReferences,
interpolateSecrets,
recursivelyGetSecretPaths
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
TAttachSecretTagsDTO,
TBackFillSecretReferencesDTO,
TCreateBulkSecretDTO,
TCreateManySecretRawDTO,
TCreateSecretDTO,
@ -91,6 +103,22 @@ export const secretServiceFactory = ({
secretImportDAL,
secretVersionTagDAL
}: TSecretServiceFactoryDep) => {
const getSecretReference = async (projectId: string) => {
// if bot key missing means e2e still exist
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
return (el: { ciphertext?: string; iv: string; tag: string }) =>
botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.ciphertext || "",
iv: el.iv,
tag: el.tag,
key: botKey
})
)
: undefined;
};
// utility function to get secret blind index data
const interalGenSecBlindIndexByName = async (projectId: string, secretName: string) => {
const appCfg = getConfig();
@ -225,6 +253,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, type, ...el } = inputSecret;
const references = await getSecretReference(projectId);
const secret = await secretDAL.transaction((tx) =>
fnSecretBulkInsert({
folderId,
@ -237,7 +266,12 @@ export const secretServiceFactory = ({
userId: inputSecret.type === SecretType.Personal ? actorId : null,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
tags: inputSecret.tags
tags: inputSecret.tags,
references: references({
ciphertext: inputSecret.secretValueCiphertext,
iv: inputSecret.secretValueIV,
tag: inputSecret.secretValueTag
})
}
],
secretDAL,
@ -251,7 +285,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
return { ...secret[0], environment, workspace: projectId, tags };
return { ...secret[0], environment, workspace: projectId, tags, secretPath: path };
};
const updateSecret = async ({
@ -335,6 +369,7 @@ export const secretServiceFactory = ({
const { secretName, ...el } = inputSecret;
const references = await getSecretReference(projectId);
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
@ -360,7 +395,12 @@ export const secretServiceFactory = ({
"secretReminderRepeatDays",
"tags"
]),
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName]
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName],
references: references({
ciphertext: inputSecret.secretValueCiphertext,
iv: inputSecret.secretValueIV,
tag: inputSecret.secretValueTag
})
}
}
],
@ -375,7 +415,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
return { ...updatedSecret[0], workspace: projectId, environment };
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
};
const deleteSecret = async ({
@ -444,7 +484,7 @@ export const secretServiceFactory = ({
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment };
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
};
const getSecrets = async ({
@ -641,7 +681,8 @@ export const secretServiceFactory = ({
return {
...importedSecrets[i].secrets[j],
workspace: projectId,
environment: importedSecrets[i].environment
environment: importedSecrets[i].environment,
secretPath: importedSecrets[i].secretPath
};
}
}
@ -649,7 +690,7 @@ export const secretServiceFactory = ({
}
if (!secret) throw new BadRequestError({ message: "Secret not found" });
return { ...secret, workspace: projectId, environment };
return { ...secret, workspace: projectId, environment, secretPath: path };
};
const createManySecret = async ({
@ -700,6 +741,7 @@ export const secretServiceFactory = ({
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" });
const references = await getSecretReference(projectId);
const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({
inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({
@ -708,7 +750,12 @@ export const secretServiceFactory = ({
secretBlindIndex: keyName2BlindIndex[secretName],
type: SecretType.Shared,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
keyEncoding: SecretKeyEncoding.UTF8,
references: references({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag
})
})),
folderId,
secretDAL,
@ -783,6 +830,8 @@ export const secretServiceFactory = ({
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
@ -799,7 +848,15 @@ export const secretServiceFactory = ({
? newKeyName2BlindIndex[newSecretName]
: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
keyEncoding: SecretKeyEncoding.UTF8,
references:
el.secretValueIV && el.secretValueTag
? references({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag
})
: undefined
}
})),
secretDAL,
@ -924,34 +981,40 @@ export const secretServiceFactory = ({
});
const batchSecretsExpand = async (
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
}[]
secretBatch: { secretKey: string; secretValue: string; secretComment?: string; secretPath: string }[]
) => {
const secretRecord: Record<
string,
{
value: string;
comment?: string;
skipMultilineEncoding?: boolean;
// Group secrets by secretPath
const secretsByPath: Record<string, { secretKey: string; secretValue: string; secretComment?: string }[]> = {};
secretBatch.forEach((secret) => {
if (!secretsByPath[secret.secretPath]) {
secretsByPath[secret.secretPath] = [];
}
> = {};
secretBatch.forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment
};
secretsByPath[secret.secretPath].push(secret);
});
await expandSecrets(secretRecord);
// Expand secrets for each group
for (const secPath in secretsByPath) {
if (!Object.hasOwn(secretsByPath, path)) {
// eslint-disable-next-line no-continue
continue;
}
secretBatch.forEach((decryptedSecret, index) => {
// eslint-disable-next-line no-param-reassign
secretBatch[index].secretValue = secretRecord[decryptedSecret.secretKey].value;
});
const secretRecord: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
secretsByPath[secPath].forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment
};
});
await expandSecrets(secretRecord);
secretsByPath[secPath].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretRecord[decryptedSecret.secretKey].value;
});
}
};
// expand secrets
@ -999,6 +1062,7 @@ export const secretServiceFactory = ({
includeImports,
version
});
return decryptSecretRaw(secret, botKey);
};
@ -1171,7 +1235,9 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
return secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
);
};
const updateManySecretsRaw = async ({
@ -1223,7 +1289,9 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
return secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
);
};
const deleteManySecretsRaw = async ({
@ -1257,7 +1325,9 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(secrets[0].folderId);
await secretQueueService.syncSecrets({ secretPath, projectId, environment });
return secrets.map((secret) => decryptSecretRaw({ ...secret, workspace: projectId, environment }, botKey));
return secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey)
);
};
const getSecretVersions = async ({
@ -1488,6 +1558,52 @@ export const secretServiceFactory = ({
};
};
// this is a backfilling API for secret references
// what it does is it will go through all the secret values and parse all references
// populate the secret reference to do sync integrations
const backfillSecretReferences = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod
}: TBackFillSecretReferencesDTO) => {
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!hasRole(ProjectMembershipRole.Admin))
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey)
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
await secretDAL.transaction(async (tx) => {
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
await secretDAL.upsertSecretReferences(
secrets.map(({ id, secretValueCiphertext, secretValueIV, secretValueTag }) => ({
secretId: id,
references: getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
key: botKey
})
)
})),
tx
);
});
return { message: "Successfully backfilled secret references" };
};
return {
attachTags,
detachTags,
@ -1508,6 +1624,7 @@ export const secretServiceFactory = ({
updateManySecretsRaw,
deleteManySecretsRaw,
getSecretVersions,
backfillSecretReferences,
// external services function
fnSecretBulkDelete,
fnSecretBulkUpdate,

@ -223,11 +223,13 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
secretId: string;
};
export type TSecretReference = { environment: string; secretPath: string };
export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[] }>;
secretDAL: Pick<TSecretDALFactory, "insertMany">;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[]; references?: TSecretReference[] }>;
secretDAL: Pick<TSecretDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
@ -236,8 +238,11 @@ export type TFnSecretBulkInsert = {
export type TFnSecretBulkUpdate = {
folderId: string;
projectId: string;
inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[];
secretDAL: Pick<TSecretDALFactory, "bulkUpdate">;
inputSecrets: {
filter: Partial<TSecrets>;
data: TSecretsUpdate & { tags?: string[]; references?: TSecretReference[] };
}[];
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
@ -294,6 +299,8 @@ export type TRemoveSecretReminderDTO = {
repeatDays: number;
};
export type TBackFillSecretReferencesDTO = TProjectPermission;
// ---
export type TCreateManySecretsRawFnFactory = {

@ -91,6 +91,8 @@ services:
- TELEMETRY_ENABLED=false
volumes:
- ./backend/src:/app/src
extra_hosts:
- "host.docker.internal:host-gateway"
frontend:
container_name: infisical-dev-frontend
@ -128,7 +130,7 @@ services:
ports:
- 1025:1025 # SMTP server
- 8025:8025 # Web UI
openldap: # note: more advanced configuration is available
image: osixia/openldap:1.5.0
restart: always

@ -0,0 +1,4 @@
---
title: "Revoke Access Token"
openapi: "POST /api/v1/auth/token/revoke"
---

@ -128,6 +128,12 @@ infisical export --template=<path to template>
</Accordion>
<Accordion title="--include-imports">
By default imported secrets are available, you can disable it by setting this option to false.
Default value: `true`
</Accordion>
<Accordion title="--format">
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv`, `json` and `yaml`

@ -126,6 +126,12 @@ $ infisical run -- npm run dev
</Accordion>
<Accordion title="--include-imports">
By default imported secrets are available, you can disable it by setting this option to false.
Default value: `true`
</Accordion>
{" "}
<Accordion title="--env">

@ -13,6 +13,7 @@ If none of the available stores work for you, you can try using the `file` store
If you are still experiencing trouble, please seek support.
[Learn more about vault command](./commands/vault)
</Accordion>
<Accordion title="Can I fetch secrets with Infisical if I am offline?">

@ -39,7 +39,7 @@ then Infisical returns a short-lived access token that can be used to make authe
To be more specific:
1. The client IAM principal signs a `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); this is done using the credentials from the AWS environment where the IAM principal is running.
2. The client sends the signed query data to Infisical including the request method, request body, and request headers.
2. The client sends the signed query data to Infisical including the request method, request body, and request headers at the `/api/v1/auth/aws-auth/login` endpoint.
3. Infisical reconstructs the query and sends it to AWS STS API via the [sts:GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) method for verification and obtains the identity associated with the IAM principal.
4. Infisical checks the identity's properties against set criteria such **Allowed Principal ARNs**.
5. If all is well, Infisical returns a short-lived access token that the IAM principal can use to make authenticated requests to the Infisical API.

@ -46,7 +46,7 @@ then Infisical returns a short-lived access token that can be used to make authe
To be more specific:
1. The client running on a GCP service obtains an [ID token](https://cloud.google.com/docs/authentication/get-id-token) constituting the identity for a GCP resource such as a GCE instance or Cloud Function; this is a unique JWT token that includes details about the instance as well as Google's [RS256 signature](https://datatracker.ietf.org/doc/html/rfc7518#section-3.3).
2. The client sends the ID token to Infisical.
2. The client sends the ID token to Infisical at the `/api/v1/auth/gcp-auth/login` endpoint.
3. Infisical verifies the token against Google's [public OAuth2 certificates](https://www.googleapis.com/oauth2/v3/certs).
4. Infisical checks if the entity behind the ID token is allowed to authenticate with Infisical based on set criteria such as **Allowed Service Account Emails**.
5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
@ -123,7 +123,7 @@ access the Infisical API using the GCP ID Token authentication method.
<CodeGroup>
```bash curl
curl -H "Metadata-Flavor: Google" \
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=<identityId>'
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=<identityId>&format=full'
```
</CodeGroup>
@ -208,7 +208,7 @@ then Infisical returns a short-lived access token that can be used to make authe
To be more specific:
1. The client generates a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt); this is done using the service account credentials associated with the client.
2. The client sends the signed JWT token to Infisical.
2. The client sends the signed JWT token to Infisical at the `/api/v1/auth/gcp-auth/login` endpoint.
3. Infisical verifies the signed JWT token.
4. Infisical checks if the service account behind the JWT token is allowed to authenticate with Infisical based **Allowed Service Account Emails**.
5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.

@ -0,0 +1,247 @@
---
title: Kubernetes Auth
description: "Learn how to authenticate with Infisical in Kubernetes"
---
**Kubernetes Auth** is a Kubernetes-native authentication method for applications (e.g. pods) to access Infisical.
## Diagram
The following sequence digram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical.
```mermaid
sequenceDiagram
participant Pod as Pod
participant Infis as Infisical
participant KubernetesServer as K8s API Server
Note over Pod: Step 1: Service Account JWT Token Retrieval
Note over Pod,Infis: Step 2: JWT Token Login Operation
Pod->>Infis: Send JWT token to /api/v1/auth/kubernetes-auth/login
Infis->>KubernetesServer: Forward JWT token for validation
KubernetesServer-->>Infis: Return identity info for JWT
Note over Infis: Step 3: Identity Property Verification
Infis->>Pod: Return short-lived access token
Note over Pod,Infis: Step 4: Access Infisical API with Token
Pod->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates an application in Kubernetes by verifying its identity and checking that it meets specific requirements (e.g. it is bound to an allowed service account) at the `/api/v1/auth/kubernetes-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The application deployed on Kubernetes retrieves its [service account credential](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting) that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` pod path.
2. The application sends the JWT token to Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint after which Infisical forwards the JWT token to the Kubernetes API Server at the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) for verification and to obtain the service account information associated with the JWT token. Infisical is able to authenticate and interact with the TokenReview API by using a long-lived service account JWT token itself (referred to onward as the token reviewer JWT token).
3. Infisical checks the service account properties against set criteria such **Allowed Service Account Names** and **Allowed Namespaces**.
4. If all is well, Infisical returns a short-lived access token that the application can use to make authenticated requests to the Infisical API.
<Note>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
to authenticate with Infisical using Kubernetes Auth as they handle the
authentication process including service account credential retrieval for you.
</Note>
## Guide
In the following steps, we explore how to create and use identities for your applications in Kubernetes to access the Infisical API using the Kubernetes Auth authentication method.
<Steps>
<Step title="Obtaining the token reviewer JWT for Infisical">
1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server.
```yaml infisical-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: infisical-auth
namespace: default
```
```
kubectl apply -f infisical-service-account.yaml
```
1.2. Bind the service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file:
```yaml cluster-role-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: infisical-auth
namespace: default
```
```
kubectl apply -f cluster-role-binding.yaml
```
1.3. Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource:
```yaml service-account-token.yaml
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: infisical-auth-token
annotations:
kubernetes.io/service-account.name: "infisical-auth"
```
```
kubectl apply -f service-account-token.yaml
```
1.4. Link the secret in step 1.3 to the service account in step 1.1:
```bash
kubectl patch serviceaccount infisical-auth -p '{"secrets": [{"name": "infisical-auth-token"}]}' -n default
```
1.5. Finally, retrieve the token reviewer JWT token from the secret.
```bash
kubectl get secret infisical-auth-token -n default -o=jsonpath='{.data.token}' | base64 --decode
```
Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2.
</Step>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**.
![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
Here's some more guidance on each field:
- Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`.
- Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5.
- Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical.
- Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.
- Allowed Audience: An optional audience claim that the service account JWT token must have to authenticate with Infisical.
- CA Certificate: The PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you should first make sure that the pod running your application is bound to a service account specified in the **Allowed Service Account Names** field of the identity's Kubernetes Auth authentication method configuration in step 2.
Once bound, the pod will receive automatically mounted service account credentials that is a JWT token at the `/var/run/secrets/kubernetes.io/serviceaccount/token` path. This token should be used to authenticate with Infisical at the `/api/v1/auth/kubernetes-auth/login` endpoint.
For information on how to configure sevice accounts for pods, refer to the guide [here](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/).
We provide a code example below of how you might retrieve the JWT token and use it to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction).
<Accordion
title="Sample code for inside an application"
>
The shown example uses Node.js but you can use any other language to retrieve the service account JWT token and use it to authenticate with Infisical.
```javascript
const fs = require("fs");
try {
const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
const jwtToken = fs.readFileSync(tokenPath, "utf8");
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
const identityId = "<your-identity-id>";
const { data } = await axios.post(
`{infisicalUrl}/api/v1/auth/kubernetes-auth/login`,
{
identityId,
jwt,
}
);
console.log("result data: ", data); // access token here
} catch(err) {
console.error(err);
}
```
</Accordion>
<Tip>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using Kubernetes Auth as they handle the authentication process including service account credential retrieval for you.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token exceeds its max ttl, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why is the Infisical API rejecting my service account JWT token?">
There are a few reasons for why this might happen:
- The Kubernetes Auth authentication method configuration is invalid.
- The service account JWT token has expired is malformed or invalid.
- The service account associated with the JWT token does not meet the criteria set forth in the Kubernetes Auth authentication method configuration such as **Allowed Service Account Names** and **Allowed Namespaces**.
</Accordion>
<Accordion title="Why is the Infisical API rejecting my access token?">
There are a few reasons for why this might happen:
- The access token has expired.
- The identity is insufficently permissioned to interact with the resources you wish to access.
- The client access token is being used from an untrusted IP.
</Accordion>
<Accordion title="What is access token renewal and TTL/Max TTL?">
A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires.
In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter.
A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
</Accordion>
</AccordionGroup>

@ -7,7 +7,7 @@ description: "Learn how to use Machine Identities to programmatically interact w
An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP).
Each identity must authenticate with the Infisical API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth), [AWS Auth](/documentation/platform/identities/aws-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
Each identity must authenticate with the Infisical API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth), [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth), [AWS Auth](/documentation/platform/identities/aws-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
![organization identities](/images/platform/organization/organization-machine-identities.png)
@ -38,6 +38,7 @@ Machine Identity support for the rest of the clients is planned to be released i
To interact with various resources in Infisical, Machine Identities are able to authenticate using:
- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment.
- [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth): A Kubernetes-native authentication method for applications (e.g. pods) to authenticate with Infisical.
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to authenticate with Infisical.
- [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.) to authenticate with Infisical.

@ -31,7 +31,7 @@ then Infisical returns a short-lived access token that can be used to make authe
To be more specific:
1. The client submits a **Client ID** and **Client Secret** to Infisical.
1. The client submits a **Client ID** and **Client Secret** to Infisical at the `/api/v1/auth/universal-auth/login` endpoint.
2. Infisical verifies the credential pair.
3. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.

@ -3,7 +3,7 @@ title: "Secret Versioning"
description: "Learn how secret versioning works in Infisical."
---
Every time a secret change is persformed, a new version of the same secret is created.
Every time a secret change is performed, a new version of the same secret is created.
Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrieved via API](/api-reference/endpoints/secrets/read)
by specifying the `version` query parameter.

Binary file not shown.

After

(image error) Size: 484 KiB

@ -153,6 +153,7 @@
"documentation/platform/auth-methods/email-password",
"documentation/platform/token",
"documentation/platform/identities/universal-auth",
"documentation/platform/identities/kubernetes-auth",
"documentation/platform/identities/gcp-auth",
"documentation/platform/identities/aws-auth",
"documentation/platform/mfa",
@ -416,7 +417,8 @@
"api-reference/endpoints/universal-auth/create-client-secret",
"api-reference/endpoints/universal-auth/list-client-secrets",
"api-reference/endpoints/universal-auth/revoke-client-secret",
"api-reference/endpoints/universal-auth/renew-access-token"
"api-reference/endpoints/universal-auth/renew-access-token",
"api-reference/endpoints/universal-auth/revoke-access-token"
]
},
{

@ -176,7 +176,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
![infisical-selfhost](/images/self-hosting/applicable-to-all/selfhost-signup.png)
</Step>
<Step title="Upgrade your instance">
To upgrade your instance of Infisical simply update the docker image tag in your Halm values and rerun the command below.
To upgrade your instance of Infisical simply update the docker image tag in your Helm values and rerun the command below.
```bash
helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml

@ -2,6 +2,7 @@ import { IdentityAuthMethod } from "./enums";
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
[IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth",
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth"
};

@ -1,5 +1,6 @@
export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth"
}

@ -3,6 +3,7 @@ export { IdentityAuthMethod } from "./enums";
export {
useAddIdentityAwsAuth,
useAddIdentityGcpAuth,
useAddIdentityKubernetesAuth,
useAddIdentityUniversalAuth,
useCreateIdentity,
useCreateIdentityUniversalAuthClientSecret,
@ -11,10 +12,12 @@ export {
useUpdateIdentity,
useUpdateIdentityAwsAuth,
useUpdateIdentityGcpAuth,
useUpdateIdentityKubernetesAuth,
useUpdateIdentityUniversalAuth} from "./mutations";
export {
useGetIdentityAwsAuth,
useGetIdentityGcpAuth,
useGetIdentityKubernetesAuth,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "./queries";

@ -7,6 +7,7 @@ import { identitiesKeys } from "./queries";
import {
AddIdentityAwsAuthDTO,
AddIdentityGcpAuthDTO,
AddIdentityKubernetesAuthDTO,
AddIdentityUniversalAuthDTO,
ClientSecretData,
CreateIdentityDTO,
@ -17,12 +18,13 @@ import {
Identity,
IdentityAwsAuth,
IdentityGcpAuth,
IdentityKubernetesAuth,
IdentityUniversalAuth,
UpdateIdentityAwsAuthDTO,
UpdateIdentityDTO,
UpdateIdentityGcpAuthDTO,
UpdateIdentityUniversalAuthDTO
} from "./types";
UpdateIdentityKubernetesAuthDTO,
UpdateIdentityUniversalAuthDTO} from "./types";
export const useCreateIdentity = () => {
const queryClient = useQueryClient();
@ -323,3 +325,88 @@ export const useUpdateIdentityAwsAuth = () => {
}
});
};
// --- K8s auth (TODO: add cert and token reviewer JWT fields)
export const useAddIdentityKubernetesAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityKubernetesAuth, {}, AddIdentityKubernetesAuthDTO>({
mutationFn: async ({
identityId,
kubernetesHost,
tokenReviewerJwt,
allowedNames,
allowedNamespaces,
allowedAudience,
caCert,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityKubernetesAuth }
} = await apiRequest.post<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
`/api/v1/auth/kubernetes-auth/identities/${identityId}`,
{
kubernetesHost,
tokenReviewerJwt,
allowedNames,
allowedNamespaces,
allowedAudience,
caCert,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityKubernetesAuth;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};
export const useUpdateIdentityKubernetesAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityKubernetesAuth, {}, UpdateIdentityKubernetesAuthDTO>({
mutationFn: async ({
identityId,
kubernetesHost,
tokenReviewerJwt,
allowedNamespaces,
allowedNames,
allowedAudience,
caCert,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityKubernetesAuth }
} = await apiRequest.patch<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
`/api/v1/auth/kubernetes-auth/identities/${identityId}`,
{
kubernetesHost,
tokenReviewerJwt,
allowedNames,
allowedNamespaces,
allowedAudience,
caCert,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityKubernetesAuth;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};

@ -2,13 +2,21 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { ClientSecretData, IdentityAwsAuth, IdentityGcpAuth, IdentityUniversalAuth } from "./types";
import {
ClientSecretData,
IdentityAwsAuth,
IdentityGcpAuth,
IdentityKubernetesAuth,
IdentityUniversalAuth
} from "./types";
export const identitiesKeys = {
getIdentityUniversalAuth: (identityId: string) =>
[{ identityId }, "identity-universal-auth"] as const,
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
[{ identityId }, "identity-universal-auth-client-secrets"] as const,
getIdentityKubernetesAuth: (identityId: string) =>
[{ identityId }, "identity-kubernetes-auth"] as const,
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const
};
@ -72,3 +80,18 @@ export const useGetIdentityAwsAuth = (identityId: string) => {
}
});
};
export const useGetIdentityKubernetesAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId),
queryFn: async () => {
const {
data: { identityKubernetesAuth }
} = await apiRequest.get<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
`/api/v1/auth/kubernetes-auth/identities/${identityId}`
);
return identityKubernetesAuth;
}
});
};

@ -195,6 +195,54 @@ export type UpdateIdentityAwsAuthDTO = {
}[];
};
export type IdentityKubernetesAuth = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt: string;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
caCert: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityKubernetesAuthDTO = {
organizationId: string;
identityId: string;
kubernetesHost: string;
tokenReviewerJwt: string;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
caCert: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityKubernetesAuthDTO = {
organizationId: string;
identityId: string;
kubernetesHost?: string;
tokenReviewerJwt?: string;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
caCert?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type CreateIdentityUniversalAuthClientSecretDTO = {
identityId: string;
description?: string;

@ -1,5 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
@ -110,3 +111,15 @@ export const useDeleteIntegration = () => {
}
});
};
export const useSyncIntegration = () => {
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
onSuccess: () => {
createNotification({
text: "Successfully triggered manual sync",
type: "success"
});
}
});
};

@ -29,6 +29,9 @@ export type TIntegration = {
secretPath: string;
createdAt: string;
updatedAt: string;
lastUsed?: string;
isSynced?: boolean;
syncMessage?: string;
__v: number;
metadata?: {
secretSuffix?: string;

@ -1,4 +1,5 @@
export {
useBackfillSecretReference,
useCreateSecretBatch,
useCreateSecretV3,
useDeleteSecretBatch,

@ -87,11 +87,11 @@ export const useCreateSecretV3 = ({
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -148,11 +148,11 @@ export const useUpdateSecretV3 = ({
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -244,11 +244,11 @@ export const useCreateSecretBatch = ({
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -297,11 +297,11 @@ export const useUpdateSecretBatch = ({
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -379,3 +379,13 @@ export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;
};
export const useBackfillSecretReference = () =>
useMutation<{ message: string }, {}, { projectId: string }>({
mutationFn: async ({ projectId }) => {
const { data } = await apiRequest.post("/api/v3/secrets/backfill-secret-references", {
projectId
});
return data.message;
}
});

@ -198,7 +198,8 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId),
queryFn: () => fetchWorkspaceIntegrations(workspaceId),
enabled: Boolean(workspaceId)
enabled: Boolean(workspaceId),
refetchInterval: 4000
});
export const createWorkspace = ({

@ -1,21 +1,26 @@
import Link from "next/link";
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
import { faArrowRight, faRefresh, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Alert,
AlertDescription,
Button,
DeleteActionModal,
EmptyState,
FormLabel,
IconButton,
Skeleton,
Tag,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { TIntegration } from "@app/hooks/api/types";
type Props = {
@ -39,6 +44,8 @@ export const IntegrationsSection = ({
"deleteConfirmation"
] as const);
const { mutate: syncIntegration } = useSyncIntegration();
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
@ -74,7 +81,7 @@ export const IntegrationsSection = ({
</div>
)}
{!isLoading && isBotActive && (
<div className="flex flex-col min-w-max space-y-4 p-6 pt-0">
<div className="flex min-w-max flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
@ -137,11 +144,12 @@ export const IntegrationsSection = ({
"App"
}
/>
<div className="min-w-[8rem] max-w-[12rem] overflow-scroll no-scrollbar no-scrollbar::-webkit-scrollbar whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
<div className="no-scrollbar::-webkit-scrollbar min-w-[8rem] max-w-[12rem] overflow-scroll whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200 no-scrollbar">
{(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) ||
(integration.integration === "aws-parameter-store" && `${integration.path}`) ||
(integration.integration === "aws-parameter-store" &&
`${integration.path}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||
integration.app}
@ -187,13 +195,70 @@ export const IntegrationsSection = ({
</div>
)}
</div>
<div className="flex cursor-default items-center">
<div className="mt-[1.5rem] flex cursor-default">
{integration.isSynced != null && integration.lastUsed != null && (
<Tag
key={integration.id}
className={integration.isSynced ? "bg-green-800" : "bg-red/80"}
>
<Tooltip
center
className="max-w-xs whitespace-normal break-words"
content={
<div className="flex max-h-[10rem] flex-col overflow-auto ">
<div className="flex self-start">
<FontAwesomeIcon
icon={faCalendarCheck}
className="pt-0.5 pr-2 text-sm"
/>
<div className="text-sm">Last sync</div>
</div>
<div className="pl-5 text-left text-xs">
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
</div>
{!integration.isSynced && (
<>
<div className="mt-2 flex self-start">
<FontAwesomeIcon icon={faXmark} className="pt-1 pr-2 text-sm" />
<div className="text-sm">Fail reason</div>
</div>
<div className="pl-5 text-left text-xs">
{integration.syncMessage}
</div>
</>
)}
</div>
}
>
<div className="flex items-center space-x-2 text-white">
<div>Sync Status</div>
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
</div>
</Tooltip>
</Tag>
)}
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
<Tooltip className="text-center" content="Manually sync integration secrets">
<Button
onClick={() =>
syncIntegration({
workspaceId,
id: integration.id,
lastUsed: integration.lastUsed as string
})
}
className="max-w-[2.5rem] border-none bg-mineshaft-500"
>
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
</Button>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed: boolean) => (
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
@ -217,7 +282,9 @@ export const IntegrationsSection = ({
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"}?`}
} integration for ${
(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={
(popUp?.deleteConfirmation?.data as TIntegration)?.app ||

@ -16,6 +16,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = {
@ -29,6 +30,7 @@ type Props = {
const identityAuthMethods = [
{ 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 }
];
@ -77,6 +79,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
/>
);
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
return (
<IdentityKubernetesAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm

@ -0,0 +1,407 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityKubernetesAuth,
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
// TODO: Add CA cert and token reviewer JWT fields
const schema = z
.object({
kubernetesHost: z.string(),
tokenReviewerJwt: z.string(),
allowedNames: z.string(),
allowedNamespaces: z.string(),
allowedAudience: z.string(),
caCert: z.string(),
accessTokenTTL: z.string(),
accessTokenMaxTTL: z.string(),
accessTokenNumUsesLimit: z.string(),
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1)
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
};
export const IdentityKubernetesAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "");
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
kubernetesHost: "", // TODO
tokenReviewerJwt: "",
allowedNames: "", // TODO
allowedNamespaces: "", // TODO
allowedAudience: "", // TODO
caCert: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
}
});
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
kubernetesHost: data.kubernetesHost,
tokenReviewerJwt: data.tokenReviewerJwt,
allowedNames: data.allowedNames,
allowedNamespaces: data.allowedNamespaces,
allowedAudience: data.allowedAudience,
caCert: data.caCert,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
({ ipAddress, prefix }: IdentityTrustedIp) => {
return {
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
});
} else {
reset({
kubernetesHost: "", // TODO
tokenReviewerJwt: "",
allowedNames: "",
allowedNamespaces: "",
allowedAudience: "",
caCert: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
kubernetesHost,
tokenReviewerJwt,
allowedNames,
allowedNamespaces,
allowedAudience,
caCert,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (data) {
await updateMutateAsync({
organizationId: orgId,
kubernetesHost,
tokenReviewerJwt,
allowedNames,
allowedNamespaces,
allowedAudience,
caCert,
identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
kubernetesHost: kubernetesHost || "",
tokenReviewerJwt,
allowedNames: allowedNames || "",
allowedNamespaces: allowedNamespaces || "",
allowedAudience: allowedAudience || "",
caCert: caCert || "",
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success"
});
reset();
} catch (err) {
createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue="2592000"
name="kubernetesHost"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Kubernetes Host / Base Kubernetes API URL "
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="allowedNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Names"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="allowedNamespaces"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Namespaces"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="allowedAudience"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Audience" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</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={() => {
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={() => {
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 items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
</div>
</form>
);
};

@ -0,0 +1,43 @@
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useBackfillSecretReference } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
export const BackfillSecretReferenceSecretion = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const backfillSecretReferences = useBackfillSecretReference();
if (!currentWorkspace) return null;
const handleBackfill = async () => {
if (backfillSecretReferences.isLoading) return;
try {
await backfillSecretReferences.mutateAsync({ projectId: currentWorkspace.id || "" });
createNotification({ text: "Successfully re-indexed secret references", type: "success" });
} catch {
createNotification({ text: "Failed to re-index secret references", type: "error" });
}
};
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Index Secret References</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
This will index all secret references, enabling integrations to be triggered when their values change going forward.
</p>
<Button
variant="outline_bg"
isLoading={backfillSecretReferences.isLoading}
onClick={handleBackfill}
isDisabled={!isAdmin}
>
Index Secret References
</Button>
</div>
);
};

@ -0,0 +1 @@
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";

@ -1,4 +1,5 @@
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { E2EESection } from "../E2EESection";
import { EnvironmentSection } from "../EnvironmentSection";
@ -13,6 +14,7 @@ export const ProjectGeneralTab = () => {
<SecretTagsSection />
<AutoCapitalizationSection />
<E2EESection />
<BackfillSecretReferenceSecretion />
<DeleteProjectSection />
</div>
);

@ -1,4 +1,5 @@
export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection";
export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection";