mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-28 02:53:22 +00:00
Compare commits
91 Commits
daniel/jen
...
fix/resolv
Author | SHA1 | Date | |
---|---|---|---|
|
3e9ce79398 | ||
|
1e63604f1e | ||
|
6ce86c4240 | ||
|
fd65936ae7 | ||
|
c894a18797 | ||
|
c170ba6249 | ||
|
c344330c93 | ||
|
a6dd36f684 | ||
|
eb8acba037 | ||
|
c7a8e1102e | ||
|
aca71a7b6f | ||
|
ae075df0ec | ||
|
75927f711c | ||
|
b1b1ce07a3 | ||
|
81f7884d03 | ||
|
b8c35fbf15 | ||
|
42e73d66fc | ||
|
fe40e4f475 | ||
|
b9782c1a85 | ||
|
a0be2985dd | ||
|
86d16c5b9f | ||
|
c1c1471439 | ||
|
527e1d6b79 | ||
|
3e32915a82 | ||
|
4faa9ced04 | ||
|
b6ff07b605 | ||
|
1753cd76be | ||
|
f75fc54e10 | ||
|
966bd77234 | ||
|
c782df1176 | ||
|
efe10e361f | ||
|
e9c5b7f846 | ||
|
008b37c0f4 | ||
|
c9b234dbea | ||
|
049df6abec | ||
|
8497182a7b | ||
|
133841c322 | ||
|
e7c5645aa9 | ||
|
0bc778b9bf | ||
|
b0bc41da14 | ||
|
a234b686c2 | ||
|
6230167794 | ||
|
68d1849ba0 | ||
|
5c10427eaf | ||
|
290d99e02c | ||
|
b75d601754 | ||
|
de2a5b4255 | ||
|
3d65d121c0 | ||
|
663f8abc51 | ||
|
941a71efaf | ||
|
19bbc2ab26 | ||
|
f4de52e714 | ||
|
0b87121b67 | ||
|
e649667da8 | ||
|
6af4b3f64c | ||
|
efcc248486 | ||
|
82eeae6030 | ||
|
440c77965c | ||
|
880289217e | ||
|
d0947f1040 | ||
|
303edadb1e | ||
|
50155a610d | ||
|
c2830a56b6 | ||
|
b9a9b6b4d9 | ||
|
e7f7f271c8 | ||
|
b26e96c5a2 | ||
|
9b404c215b | ||
|
d6dae04959 | ||
|
629bd9b7c6 | ||
|
4e06fa3a0c | ||
|
0f827fc31a | ||
|
3d4aa0fdc9 | ||
|
9253c69325 | ||
|
7189544705 | ||
|
a724ab101c | ||
|
7d3a62cc4c | ||
|
dea67e3cb0 | ||
|
ce66cccd8b | ||
|
91eda2419a | ||
|
b350eef2b9 | ||
|
85725215f2 | ||
|
76c9d642a9 | ||
|
3ed5dd6109 | ||
|
08e7815ec1 | ||
|
04d961b832 | ||
|
9c0a1b7089 | ||
|
9352e8bca0 | ||
|
265932df20 | ||
|
f23056bcbc | ||
|
fdf5fcad0a | ||
|
a85c59e3e2 |
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -33,6 +33,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
|
||||
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 { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-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";
|
||||
@@ -121,6 +122,7 @@ declare module "fastify" {
|
||||
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -62,6 +62,9 @@ import {
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityAzureAuthsInsert,
|
||||
TIdentityAzureAuthsUpdate,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityGcpAuthsInsert,
|
||||
TIdentityGcpAuthsUpdate,
|
||||
@@ -356,6 +359,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAzureAuth]: Knex.CompositeTableType<
|
||||
TIdentityAzureAuths,
|
||||
TIdentityAzureAuthsInsert,
|
||||
TIdentityAzureAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
||||
TIdentityUaClientSecrets,
|
||||
TIdentityUaClientSecretsInsert,
|
||||
|
@@ -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,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesProjectIdExist) t.index("projectId");
|
||||
if (doesOrgIdExist) t.index("orgId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
|
||||
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesProjectIdExist) t.dropIndex("projectId");
|
||||
if (doesOrgIdExist) t.dropIndex("orgId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesEnvIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "envId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesEnvIdExist) t.index("envId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesEnvIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "envId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesEnvIdExist) t.dropIndex("envId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesEnvIdExist = await knex.schema.hasColumn(TableName.SecretVersion, "envId");
|
||||
if (await knex.schema.hasTable(TableName.SecretVersion)) {
|
||||
await knex.schema.alterTable(TableName.SecretVersion, (t) => {
|
||||
if (doesEnvIdExist) t.index("envId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesEnvIdExist = await knex.schema.hasColumn(TableName.SecretVersion, "envId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.SecretVersion)) {
|
||||
await knex.schema.alterTable(TableName.SecretVersion, (t) => {
|
||||
if (doesEnvIdExist) t.dropIndex("envId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "snapshotId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesSnapshotIdExist) t.index("snapshotId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "snapshotId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesSnapshotIdExist) t.dropIndex("snapshotId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotFolder, "snapshotId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotFolder)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotFolder, (t) => {
|
||||
if (doesSnapshotIdExist) t.index("snapshotId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesSnapshotIdExist = await knex.schema.hasColumn(TableName.SnapshotFolder, "snapshotId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotFolder)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotFolder, (t) => {
|
||||
if (doesSnapshotIdExist) t.dropIndex("snapshotId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesFolderIdExist = await knex.schema.hasColumn(TableName.Secret, "folderId");
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.Secret, "userId");
|
||||
if (await knex.schema.hasTable(TableName.Secret)) {
|
||||
await knex.schema.alterTable(TableName.Secret, (t) => {
|
||||
if (doesFolderIdExist && doesUserIdExist) t.index(["folderId", "userId"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesFolderIdExist = await knex.schema.hasColumn(TableName.Secret, "folderId");
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.Secret, "userId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.Secret)) {
|
||||
await knex.schema.alterTable(TableName.Secret, (t) => {
|
||||
if (doesUserIdExist && doesFolderIdExist) t.dropIndex(["folderId", "userId"]);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesExpireAtExist = await knex.schema.hasColumn(TableName.AuditLog, "expiresAt");
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesExpireAtExist) t.index("expiresAt");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesExpireAtExist = await knex.schema.hasColumn(TableName.AuditLog, "expiresAt");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.AuditLog)) {
|
||||
await knex.schema.alterTable(TableName.AuditLog, (t) => {
|
||||
if (doesExpireAtExist) t.dropIndex("expiresAt");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
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.IdentityAzureAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityAzureAuth, (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("tenantId").notNullable();
|
||||
t.string("resource").notNullable();
|
||||
t.string("allowedServicePrincipalIds").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityAzureAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
|
||||
}
|
26
backend/src/db/schemas/identity-azure-auths.ts
Normal file
26
backend/src/db/schemas/identity-azure-auths.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 IdentityAzureAuthsSchema = 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(),
|
||||
tenantId: z.string(),
|
||||
resource: z.string(),
|
||||
allowedServicePrincipalIds: z.string()
|
||||
});
|
||||
|
||||
export type TIdentityAzureAuths = z.infer<typeof IdentityAzureAuthsSchema>;
|
||||
export type TIdentityAzureAuthsInsert = Omit<z.input<typeof IdentityAzureAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityAzureAuthsUpdate = Partial<Omit<z.input<typeof IdentityAzureAuthsSchema>, TImmutableDBKeys>>;
|
@@ -18,6 +18,7 @@ export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
export * from "./identity-aws-auths";
|
||||
export * from "./identity-azure-auths";
|
||||
export * from "./identity-gcp-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
|
@@ -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>;
|
||||
|
@@ -47,6 +47,7 @@ export enum TableName {
|
||||
IdentityUniversalAuth = "identity_universal_auths",
|
||||
IdentityKubernetesAuth = "identity_kubernetes_auths",
|
||||
IdentityGcpAuth = "identity_gcp_auths",
|
||||
IdentityAzureAuth = "identity_azure_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
@@ -149,5 +150,6 @@ export enum IdentityAuthMethod {
|
||||
Univeral = "universal-auth",
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth"
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ import { RawAxiosRequestHeaders } from "axios";
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
@@ -113,35 +112,7 @@ export const auditLogQueueServiceFactory = ({
|
||||
);
|
||||
});
|
||||
|
||||
queueService.start(QueueName.AuditLogPrune, async () => {
|
||||
logger.info(`${QueueName.AuditLogPrune}: queue task started`);
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
logger.info(`${QueueName.AuditLogPrune}: queue task completed`);
|
||||
});
|
||||
|
||||
// we do a repeat cron job in utc timezone at 12 Midnight each day
|
||||
const startAuditLogPruneJob = async () => {
|
||||
// clear previous job
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.AuditLogPrune,
|
||||
QueueJobs.AuditLogPrune,
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
QueueName.AuditLogPrune // just a job id
|
||||
);
|
||||
|
||||
await queueService.queue(QueueName.AuditLogPrune, QueueJobs.AuditLogPrune, undefined, {
|
||||
delay: 5000,
|
||||
jobId: QueueName.AuditLogPrune,
|
||||
repeat: { pattern: "0 0 * * *", utc: true }
|
||||
});
|
||||
};
|
||||
|
||||
queueService.listen(QueueName.AuditLogPrune, "failed", (err) => {
|
||||
logger.error(err?.failedReason, `${QueueName.AuditLogPrune}: log pruning failed`);
|
||||
});
|
||||
|
||||
return {
|
||||
pushToLog,
|
||||
startAuditLogPruneJob
|
||||
pushToLog
|
||||
};
|
||||
};
|
||||
|
@@ -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",
|
||||
@@ -78,6 +79,10 @@ export enum EventType {
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
|
||||
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
|
||||
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
|
||||
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
|
||||
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
@@ -281,6 +286,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: {
|
||||
@@ -552,6 +576,48 @@ interface GetIdentityAwsAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityAzureAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityAzureAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityAzureAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenantId: string;
|
||||
resource: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityAzureAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenantId?: string;
|
||||
resource?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityAzureAuthEvent {
|
||||
type: EventType.GET_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@@ -791,6 +857,7 @@ export type Event =
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| ManualSyncIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
@@ -818,6 +885,10 @@ export type Event =
|
||||
| AddIdentityAwsAuthEvent
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| LoginIdentityAzureAuthEvent
|
||||
| AddIdentityAzureAuthEvent
|
||||
| UpdateIdentityAzureAuthEvent
|
||||
| GetIdentityAzureAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
|
@@ -662,6 +662,7 @@ export const INTEGRATION = {
|
||||
secretPrefix: "The prefix for the saved secret. Used by GCP.",
|
||||
secretSuffix: "The suffix for the saved secret. Used by GCP.",
|
||||
initialSyncBehavoir: "Type of syncing behavoir with the integration.",
|
||||
mappingBehavior: "The mapping behavior of the integration.",
|
||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||
secretGCPLabel: "The label for GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
@@ -683,6 +684,9 @@ export const INTEGRATION = {
|
||||
},
|
||||
DELETE: {
|
||||
integrationId: "The ID of the integration object."
|
||||
},
|
||||
SYNC: {
|
||||
integrationId: "The ID of the integration object to manually sync"
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -104,24 +104,68 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Create" });
|
||||
}
|
||||
},
|
||||
updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
|
||||
updateById: async (
|
||||
id: string,
|
||||
{
|
||||
$incr,
|
||||
$decr,
|
||||
...data
|
||||
}: Tables[Tname]["update"] & {
|
||||
$incr?: { [x in keyof Partial<Tables[Tname]["base"]>]: number };
|
||||
$decr?: { [x in keyof Partial<Tables[Tname]["base"]>]: number };
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const [res] = await (tx || db)(tableName)
|
||||
const query = (tx || db)(tableName)
|
||||
.where({ id } as never)
|
||||
.update(data as never)
|
||||
.returning("*");
|
||||
return res;
|
||||
if ($incr) {
|
||||
Object.entries($incr).forEach(([incrementField, incrementValue]) => {
|
||||
void query.increment(incrementField, incrementValue);
|
||||
});
|
||||
}
|
||||
if ($decr) {
|
||||
Object.entries($decr).forEach(([incrementField, incrementValue]) => {
|
||||
void query.increment(incrementField, incrementValue);
|
||||
});
|
||||
}
|
||||
const [docs] = await query;
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update by id" });
|
||||
}
|
||||
},
|
||||
update: async (filter: TFindFilter<Tables[Tname]["base"]>, data: Tables[Tname]["update"], tx?: Knex) => {
|
||||
update: async (
|
||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||
{
|
||||
$incr,
|
||||
$decr,
|
||||
...data
|
||||
}: Tables[Tname]["update"] & {
|
||||
$incr?: { [x in keyof Partial<Tables[Tname]["base"]>]: number };
|
||||
$decr?: { [x in keyof Partial<Tables[Tname]["base"]>]: number };
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const res = await (tx || db)(tableName)
|
||||
const query = (tx || db)(tableName)
|
||||
.where(buildFindFilter(filter))
|
||||
.update(data as never)
|
||||
.returning("*");
|
||||
return res;
|
||||
// increment and decrement operation in update
|
||||
if ($incr) {
|
||||
Object.entries($incr).forEach(([incrementField, incrementValue]) => {
|
||||
void query.increment(incrementField, incrementValue);
|
||||
});
|
||||
}
|
||||
if ($decr) {
|
||||
Object.entries($decr).forEach(([incrementField, incrementValue]) => {
|
||||
void query.increment(incrementField, incrementValue);
|
||||
});
|
||||
}
|
||||
return await query;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update" });
|
||||
}
|
||||
|
@@ -30,6 +30,37 @@ const loggerConfig = z.object({
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("production")
|
||||
});
|
||||
|
||||
const redactedKeys = [
|
||||
"accessToken",
|
||||
"authToken",
|
||||
"serviceToken",
|
||||
"identityAccessToken",
|
||||
"token",
|
||||
"privateKey",
|
||||
"serverPrivateKey",
|
||||
"plainPrivateKey",
|
||||
"plainProjectKey",
|
||||
"encryptedPrivateKey",
|
||||
"userPrivateKey",
|
||||
"protectedKey",
|
||||
"decryptKey",
|
||||
"encryptedProjectKey",
|
||||
"encryptedSymmetricKey",
|
||||
"encryptedPrivateKey",
|
||||
"backupPrivateKey",
|
||||
"secretKey",
|
||||
"SecretKey",
|
||||
"botPrivateKey",
|
||||
"encryptedKey",
|
||||
"plaintextProjectKey",
|
||||
"accessKey",
|
||||
"botKey",
|
||||
"decryptedSecret",
|
||||
"secrets",
|
||||
"key",
|
||||
"password"
|
||||
];
|
||||
|
||||
export const initLogger = async () => {
|
||||
const cfg = loggerConfig.parse(process.env);
|
||||
const targets: pino.TransportMultiOptions["targets"][number][] = [
|
||||
@@ -74,7 +105,9 @@ export const initLogger = async () => {
|
||||
hostname: bindings.hostname
|
||||
// node_version: process.version
|
||||
})
|
||||
}
|
||||
},
|
||||
// redact until depth of three
|
||||
redact: [...redactedKeys, ...redactedKeys.map((key) => `*.${key}`), ...redactedKeys.map((key) => `*.*.${key}`)]
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
transport
|
||||
|
@@ -12,7 +12,9 @@ export enum QueueName {
|
||||
SecretRotation = "secret-rotation",
|
||||
SecretReminder = "secret-reminder",
|
||||
AuditLog = "audit-log",
|
||||
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
||||
AuditLogPrune = "audit-log-prune",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup",
|
||||
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
||||
IntegrationSync = "sync-integrations",
|
||||
SecretWebhook = "secret-webhook",
|
||||
@@ -26,7 +28,9 @@ export enum QueueJobs {
|
||||
SecretReminder = "secret-reminder-job",
|
||||
SecretRotation = "secret-rotation-job",
|
||||
AuditLog = "audit-log-job",
|
||||
// TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
|
||||
AuditLogPrune = "audit-log-prune-job",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup-job",
|
||||
SecWebhook = "secret-webhook-trigger",
|
||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||
IntegrationSync = "secret-integration-pull",
|
||||
@@ -55,6 +59,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.AuditLog;
|
||||
payload: TCreateAuditLogDTO;
|
||||
};
|
||||
[QueueName.DailyResourceCleanUp]: {
|
||||
name: QueueJobs.DailyResourceCleanUp;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.AuditLogPrune]: {
|
||||
name: QueueJobs.AuditLogPrune;
|
||||
payload: undefined;
|
||||
@@ -172,7 +180,9 @@ export const queueServiceFactory = (redisUrl: string) => {
|
||||
jobId?: string
|
||||
) => {
|
||||
const q = queueContainer[name];
|
||||
return q.removeRepeatable(job, repeatOpt, jobId);
|
||||
if (q) {
|
||||
return q.removeRepeatable(job, repeatOpt, jobId);
|
||||
}
|
||||
};
|
||||
|
||||
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
||||
|
@@ -8,6 +8,8 @@ import cors from "@fastify/cors";
|
||||
import fastifyEtag from "@fastify/etag";
|
||||
import fastifyFormBody from "@fastify/formbody";
|
||||
import helmet from "@fastify/helmet";
|
||||
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||
import ratelimiter from "@fastify/rate-limit";
|
||||
import fasitfy from "fastify";
|
||||
import { Knex } from "knex";
|
||||
import { Logger } from "pino";
|
||||
@@ -17,6 +19,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||
import { fastifyErrHandler } from "./plugins/error-handler";
|
||||
import { registerExternalNextjs } from "./plugins/external-nextjs";
|
||||
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
|
||||
@@ -64,6 +67,10 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
|
||||
await server.register(fastifyFormBody);
|
||||
await server.register(fastifyErrHandler);
|
||||
|
||||
// Rate limiters and security headers
|
||||
if (appCfg.isProductionMode) {
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||
}
|
||||
await server.register(helmet, { contentSecurityPolicy: false });
|
||||
|
||||
await server.register(maintenanceMode);
|
||||
|
@@ -1,34 +1,20 @@
|
||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
const getDistinctRequestActorId = (req: FastifyRequest) => {
|
||||
if (req?.auth?.actor === ActorType.USER) {
|
||||
return req.auth.user.username;
|
||||
}
|
||||
if (req?.auth?.actor === ActorType.IDENTITY) {
|
||||
return `${req.auth.identityId}-machine-identity-`;
|
||||
}
|
||||
if (req?.auth?.actor === ActorType.SERVICE) {
|
||||
return `${req.auth.serviceToken.id}-service-token`; // when user gets removed from system
|
||||
}
|
||||
return req.realIp;
|
||||
};
|
||||
|
||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
const appCfg = getConfig();
|
||||
const redis = appCfg.isRedisConfigured
|
||||
? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 })
|
||||
: null;
|
||||
|
||||
return {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 600,
|
||||
redis,
|
||||
allowList: (req) => req.url === "/healthcheck" || req.url === "/api/status",
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,39 +22,39 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
export const readLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 600,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
// POST, PATCH, PUT, DELETE endpoints
|
||||
export const writeLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 50,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
// special endpoints
|
||||
export const secretsLimit: RateLimitOptions = {
|
||||
// secrets, folders, secret imports
|
||||
timeWindow: 60 * 1000,
|
||||
max: 1000,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
max: 60,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const authRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 60,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const inviteUserRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 30,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const creationLimit: RateLimitOptions = {
|
||||
// identity, project, org
|
||||
timeWindow: 60 * 1000,
|
||||
max: 30,
|
||||
keyGenerator: (req) => getDistinctRequestActorId(req)
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import fp from "fastify-plugin";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
// inject permission type needed based on auth extracted
|
||||
@@ -15,6 +16,10 @@ export const injectPermission = fp(async (server) => {
|
||||
orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY"
|
||||
authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.userId}] [type=${ActorType.USER}]`
|
||||
);
|
||||
} else if (req.auth.actor === ActorType.IDENTITY) {
|
||||
req.permission = {
|
||||
type: ActorType.IDENTITY,
|
||||
@@ -22,6 +27,10 @@ export const injectPermission = fp(async (server) => {
|
||||
orgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.identityId}] [type=${ActorType.IDENTITY}]`
|
||||
);
|
||||
} else if (req.auth.actor === ActorType.SERVICE) {
|
||||
req.permission = {
|
||||
type: ActorType.SERVICE,
|
||||
@@ -29,6 +38,10 @@ export const injectPermission = fp(async (server) => {
|
||||
orgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.serviceTokenId}] [type=${ActorType.SERVICE}]`
|
||||
);
|
||||
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||
req.permission = {
|
||||
type: ActorType.SCIM_CLIENT,
|
||||
@@ -36,6 +49,10 @@ export const injectPermission = fp(async (server) => {
|
||||
orgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`injectPermission: Injecting permissions for [permissionsForIdentity=${req.auth.scimTokenId}] [type=${ActorType.SCIM_CLIENT}]`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -6,6 +6,7 @@ const headersOrder = [
|
||||
"cf-connecting-ip", // Cloudflare
|
||||
"Cf-Pseudo-IPv4", // Cloudflare
|
||||
"x-client-ip", // Most common
|
||||
"x-envoy-external-address", // for envoy
|
||||
"x-forwarded-for", // Mostly used by proxies
|
||||
"fastly-client-ip",
|
||||
"true-client-ip", // Akamai and Cloudflare
|
||||
@@ -23,7 +24,21 @@ export const fastifyIp = fp(async (fastify) => {
|
||||
const forwardedIpHeader = headersOrder.find((header) => Boolean(req.headers[header]));
|
||||
const forwardedIp = forwardedIpHeader ? req.headers[forwardedIpHeader] : undefined;
|
||||
if (forwardedIp) {
|
||||
req.realIp = Array.isArray(forwardedIp) ? forwardedIp[0] : forwardedIp;
|
||||
if (Array.isArray(forwardedIp)) {
|
||||
// eslint-disable-next-line
|
||||
req.realIp = forwardedIp[0];
|
||||
return;
|
||||
}
|
||||
|
||||
if (forwardedIp.includes(",")) {
|
||||
// the ip header when placed with load balancers that proxy request
|
||||
// will attach the internal ips to header by appending with comma
|
||||
// https://github.com/go-chi/chi/blob/master/middleware/realip.go
|
||||
const clientIPFromProxy = forwardedIp.slice(0, forwardedIp.indexOf(",")).trim();
|
||||
req.realIp = clientIPFromProxy;
|
||||
return;
|
||||
}
|
||||
req.realIp = forwardedIp;
|
||||
} else {
|
||||
req.realIp = req.ip;
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import ratelimiter, { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -62,7 +61,7 @@ import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { globalRateLimiterCfg, readLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||
@@ -81,6 +80,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
|
||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
|
||||
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
|
||||
import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-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";
|
||||
@@ -116,6 +117,7 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi
|
||||
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
||||
import { secretServiceFactory } from "@app/services/secret/secret-service";
|
||||
@@ -213,8 +215,8 @@ export const registerRoutes = async (
|
||||
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
@@ -743,6 +745,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityAzureAuthService = identityAzureAuthServiceFactory({
|
||||
identityAzureAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
@@ -770,14 +781,19 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
licenseService
|
||||
});
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
identityAccessTokenDAL
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
//
|
||||
// setup the communication with license key server
|
||||
await licenseService.init();
|
||||
|
||||
await auditLogQueue.startAuditLogPruneJob();
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -814,6 +830,7 @@ export const registerRoutes = async (
|
||||
identityKubernetesAuth: identityKubernetesAuthService,
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
secretApprovalPolicy: sapService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
@@ -840,11 +857,6 @@ export const registerRoutes = async (
|
||||
user: userDAL
|
||||
});
|
||||
|
||||
// Rate limiters and security headers
|
||||
if (appCfg.isProductionMode) {
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||
}
|
||||
|
||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||
await server.register(injectPermission);
|
||||
await server.register(injectAuditLogInfo);
|
||||
|
262
backend/src/server/routes/v1/identity-azure-auth-router.ts
Normal file
262
backend/src/server/routes/v1/identity-azure-auth-router.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityAzureAuthsSchema } 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";
|
||||
import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators";
|
||||
|
||||
export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/azure-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Login with Azure Auth",
|
||||
body: z.object({
|
||||
identityId: z.string(),
|
||||
jwt: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityAzureAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_AZURE_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAzureAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityAzureAuthId: identityAzureAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityAzureAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/azure-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Attach Azure Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
tenantId: z.string().trim(),
|
||||
resource: z.string().trim(),
|
||||
allowedServicePrincipalIds: validateAzureAuthField,
|
||||
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({
|
||||
identityAzureAuth: IdentityAzureAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAzureAuth = await server.services.identityAzureAuth.attachAzureAuth({
|
||||
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: identityAzureAuth.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_AZURE_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAzureAuth.identityId,
|
||||
tenantId: identityAzureAuth.tenantId,
|
||||
resource: identityAzureAuth.resource,
|
||||
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityAzureAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/azure-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update Azure Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
tenantId: z.string().trim().optional(),
|
||||
resource: z.string().trim().optional(),
|
||||
allowedServicePrincipalIds: validateAzureAuthField.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({
|
||||
identityAzureAuth: IdentityAzureAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAzureAuth = await server.services.identityAzureAuth.updateAzureAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityAzureAuth.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_AZURE_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAzureAuth.identityId,
|
||||
tenantId: identityAzureAuth.tenantId,
|
||||
resource: identityAzureAuth.resource,
|
||||
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityAzureAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/azure-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Retrieve Azure Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityAzureAuth: IdentityAzureAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAzureAuth = await server.services.identityAzureAuth.getAzureAuth({
|
||||
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: identityAzureAuth.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_AZURE_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAzureAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityAzureAuth };
|
||||
}
|
||||
});
|
||||
};
|
@@ -160,9 +160,9 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.enum(["iam", "gce"]).optional(),
|
||||
allowedServiceAccounts: validateGcpAuthField,
|
||||
allowedProjects: validateGcpAuthField,
|
||||
allowedZones: validateGcpAuthField,
|
||||
allowedServiceAccounts: validateGcpAuthField.optional(),
|
||||
allowedProjects: validateGcpAuthField.optional(),
|
||||
allowedZones: validateGcpAuthField.optional(),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
|
@@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
@@ -34,6 +35,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityGcpAuthRouter);
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
},
|
||||
{ prefix: "/auth" }
|
||||
);
|
||||
|
@@ -8,6 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
|
||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -49,6 +50,10 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z
|
||||
.nativeEnum(IntegrationMappingBehavior)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
@@ -160,6 +165,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
@@ -262,5 +268,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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -39,6 +39,12 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityAzureAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AZURE_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
@@ -50,6 +56,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
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.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
@@ -63,6 +70,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
doc.accessTokenTrustedIpsUa ||
|
||||
doc.accessTokenTrustedIpsGcp ||
|
||||
doc.accessTokenTrustedIpsAws ||
|
||||
doc.accessTokenTrustedIpsAzure ||
|
||||
doc.accessTokenTrustedIpsK8s
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -70,5 +78,48 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityAccessTokenOrm, findOne };
|
||||
const removeExpiredTokens = async (tx?: Knex) => {
|
||||
try {
|
||||
const docs = (tx || db)(TableName.IdentityAccessToken)
|
||||
.where({
|
||||
isAccessTokenRevoked: true
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb
|
||||
.where("accessTokenNumUsesLimit", ">", 0)
|
||||
.andWhere(
|
||||
"accessTokenNumUses",
|
||||
">=",
|
||||
db.ref("accessTokenNumUsesLimit").withSchema(TableName.IdentityAccessToken)
|
||||
);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb.where("accessTokenTTL", ">", 0).andWhere((qb2) => {
|
||||
void qb2
|
||||
.where((qb3) => {
|
||||
void qb3
|
||||
.whereNotNull("accessTokenLastRenewedAt")
|
||||
// accessTokenLastRenewedAt + convert_integer_to_seconds(accessTokenTTL) < present_date
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => "${TableName.IdentityAccessToken}"."accessTokenTTL") < NOW()`
|
||||
);
|
||||
})
|
||||
.orWhere((qb3) => {
|
||||
void qb3
|
||||
.whereNull("accessTokenLastRenewedAt")
|
||||
// created + convert_integer_to_seconds(accessTokenTTL) < present_date
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => "${TableName.IdentityAccessToken}"."accessTokenTTL") < NOW()`
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
.delete();
|
||||
return await docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityAccessTokenOrm, findOne, removeExpiredTokens };
|
||||
};
|
||||
|
@@ -21,17 +21,18 @@ export const identityAccessTokenServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL
|
||||
}: TIdentityAccessTokenServiceFactoryDep) => {
|
||||
const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => {
|
||||
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
|
||||
const {
|
||||
id: tokenId,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUses,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenLastRenewedAt,
|
||||
accessTokenMaxTTL,
|
||||
createdAt: accessTokenCreatedAt
|
||||
} = identityAccessToken;
|
||||
|
||||
if (accessTokenNumUsesLimit > 0 && accessTokenNumUses > 0 && accessTokenNumUses >= accessTokenNumUsesLimit) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new BadRequestError({
|
||||
message: "Unable to renew because access token number of uses limit reached"
|
||||
});
|
||||
@@ -46,41 +47,26 @@ export const identityAccessTokenServiceFactory = ({
|
||||
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate)
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to TTL expiration"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// access token has never been renewed
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate)
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(tokenId);
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to TTL expiration"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// max ttl checks
|
||||
if (Number(accessTokenMaxTTL) > 0) {
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||
});
|
||||
|
||||
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL));
|
||||
if (extendToDate > expirationDate)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
|
||||
@@ -97,7 +83,32 @@ export const identityAccessTokenServiceFactory = ({
|
||||
});
|
||||
if (!identityAccessToken) throw new UnauthorizedError();
|
||||
|
||||
validateAccessTokenExp(identityAccessToken);
|
||||
await validateAccessTokenExp(identityAccessToken);
|
||||
|
||||
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
|
||||
|
||||
// max ttl checks - will it go above max ttl
|
||||
if (Number(accessTokenMaxTTL) > 0) {
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||
});
|
||||
}
|
||||
|
||||
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL * 1000));
|
||||
if (extendToDate > expirationDate) {
|
||||
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedIdentityAccessToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
||||
accessTokenLastRenewedAt: new Date()
|
||||
@@ -131,7 +142,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
});
|
||||
if (!identityAccessToken) throw new UnauthorizedError();
|
||||
|
||||
if (ipAddress) {
|
||||
if (ipAddress && identityAccessToken) {
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress,
|
||||
trustedIps: identityAccessToken?.accessTokenTrustedIps as TIp[]
|
||||
@@ -146,7 +157,14 @@ export const identityAccessTokenServiceFactory = ({
|
||||
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
|
||||
}
|
||||
|
||||
validateAccessTokenExp(identityAccessToken);
|
||||
await validateAccessTokenExp(identityAccessToken);
|
||||
|
||||
await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
||||
accessTokenLastUsedAt: new Date(),
|
||||
$incr: {
|
||||
accessTokenNumUses: 1
|
||||
}
|
||||
});
|
||||
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityAzureAuthDALFactory = ReturnType<typeof identityAzureAuthDALFactory>;
|
||||
|
||||
export const identityAzureAuthDALFactory = (db: TDbClient) => {
|
||||
const azureAuthOrm = ormify(db, TableName.IdentityAzureAuth);
|
||||
return azureAuthOrm;
|
||||
};
|
@@ -0,0 +1,34 @@
|
||||
import axios from "axios";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TAzureAuthJwtPayload, TAzureJwksUriResponse, TDecodedAzureAuthJwt } from "./identity-azure-auth-types";
|
||||
|
||||
export const validateAzureIdentity = async ({
|
||||
tenantId,
|
||||
resource,
|
||||
jwt: azureJwt
|
||||
}: {
|
||||
tenantId: string;
|
||||
resource: string;
|
||||
jwt: string;
|
||||
}) => {
|
||||
const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/keys`;
|
||||
|
||||
const decodedJwt = jwt.decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt;
|
||||
const { kid } = decodedJwt.header;
|
||||
|
||||
const { data }: { data: TAzureJwksUriResponse } = await axios.get(jwksUri);
|
||||
const signingKeys = data.keys;
|
||||
|
||||
const signingKey = signingKeys.find((key) => key.kid === kid);
|
||||
if (!signingKey) throw new UnauthorizedError();
|
||||
|
||||
const publicKey = `-----BEGIN CERTIFICATE-----\n${signingKey.x5c[0]}\n-----END CERTIFICATE-----`;
|
||||
|
||||
return jwt.verify(azureJwt, publicKey, {
|
||||
audience: resource,
|
||||
issuer: `https://sts.windows.net/${tenantId}/`
|
||||
}) as TAzureAuthJwtPayload;
|
||||
};
|
@@ -0,0 +1,286 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } 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 { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
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 { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal";
|
||||
import { validateAzureIdentity } from "./identity-azure-auth-fns";
|
||||
import {
|
||||
TAttachAzureAuthDTO,
|
||||
TGetAzureAuthDTO,
|
||||
TLoginAzureAuthDTO,
|
||||
TUpdateAzureAuthDTO
|
||||
} from "./identity-azure-auth-types";
|
||||
|
||||
type TIdentityAzureAuthServiceFactoryDep = {
|
||||
identityAzureAuthDAL: Pick<TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TIdentityAzureAuthServiceFactory = ReturnType<typeof identityAzureAuthServiceFactory>;
|
||||
|
||||
export const identityAzureAuthServiceFactory = ({
|
||||
identityAzureAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityAzureAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
||||
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||
if (!identityAzureAuth) throw new UnauthorizedError();
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAzureAuth.identityId });
|
||||
if (!identityMembershipOrg) throw new UnauthorizedError();
|
||||
|
||||
const azureIdentity = await validateAzureIdentity({
|
||||
tenantId: identityAzureAuth.tenantId,
|
||||
resource: identityAzureAuth.resource,
|
||||
jwt: azureJwt
|
||||
});
|
||||
|
||||
if (azureIdentity.tid !== identityAzureAuth.tenantId) throw new UnauthorizedError();
|
||||
|
||||
if (identityAzureAuth.allowedServicePrincipalIds) {
|
||||
// validate if the service principal id is in the list of allowed service principal ids
|
||||
|
||||
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
|
||||
.split(",")
|
||||
.map((servicePrincipalId) => servicePrincipalId.trim())
|
||||
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid);
|
||||
|
||||
if (!isServicePrincipalAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityAzureAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityAzureAuth.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, identityAzureAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachAzureAuth = async ({
|
||||
identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachAzureAuthDTO) => {
|
||||
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 Azure 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 identityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityAzureAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.AZURE_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateAzureAuth = async ({
|
||||
identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update Azure Auth"
|
||||
});
|
||||
|
||||
const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.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 updatedAzureAuth = await identityAzureAuthDAL.updateById(identityGcpAuth.id, {
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedAzureAuth,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
};
|
||||
};
|
||||
|
||||
const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Azure Auth attached"
|
||||
});
|
||||
|
||||
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachAzureAuth,
|
||||
updateAzureAuth,
|
||||
getAzureAuth
|
||||
};
|
||||
};
|
@@ -0,0 +1,120 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginAzureAuthDTO = {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
export type TAttachAzureAuthDTO = {
|
||||
identityId: string;
|
||||
tenantId: string;
|
||||
resource: string;
|
||||
allowedServicePrincipalIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAzureAuthDTO = {
|
||||
identityId: string;
|
||||
tenantId?: string;
|
||||
resource?: string;
|
||||
allowedServicePrincipalIds?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAzureAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TAzureJwksUriResponse = {
|
||||
keys: {
|
||||
kty: string;
|
||||
use: string;
|
||||
kid: string;
|
||||
x5t: string;
|
||||
n: string;
|
||||
e: string;
|
||||
x5c: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type TUserPayload = {
|
||||
aud: string;
|
||||
iss: string;
|
||||
iat: number;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
acr: string;
|
||||
aio: string;
|
||||
amr: string[];
|
||||
appid: string;
|
||||
appidacr: string;
|
||||
family_name: string;
|
||||
given_name: string;
|
||||
groups: string[];
|
||||
idtyp: string;
|
||||
ipaddr: string;
|
||||
name: string;
|
||||
oid: string;
|
||||
puid: string;
|
||||
rh: string;
|
||||
scp: string;
|
||||
sub: string;
|
||||
tid: string;
|
||||
unique_name: string;
|
||||
upn: string;
|
||||
uti: string;
|
||||
ver: string;
|
||||
wids: string[];
|
||||
xms_cae: string;
|
||||
xms_cc: string[];
|
||||
xms_filter_index: string[];
|
||||
xms_rd: string;
|
||||
xms_ssm: string;
|
||||
xms_tcdt: number;
|
||||
};
|
||||
|
||||
type TAppPayload = {
|
||||
aud: string;
|
||||
iss: string;
|
||||
iat: number;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
aio: string;
|
||||
appid: string;
|
||||
appidacr: string;
|
||||
idp: string;
|
||||
idtyp: string;
|
||||
oid: string; // service principal id
|
||||
rh: string;
|
||||
sub: string;
|
||||
tid: string;
|
||||
uti: string;
|
||||
ver: string;
|
||||
xms_cae: string;
|
||||
xms_cc: string[];
|
||||
xms_rd: string;
|
||||
xms_ssm: string;
|
||||
xms_tcdt: number;
|
||||
};
|
||||
|
||||
export type TAzureAuthJwtPayload = TUserPayload | TAppPayload;
|
||||
|
||||
export type TDecodedAzureAuthJwt = {
|
||||
header: {
|
||||
type: string;
|
||||
alg: string;
|
||||
x5t: string;
|
||||
kid: string;
|
||||
};
|
||||
payload: TAzureAuthJwtPayload;
|
||||
signature: string;
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const validateAzureAuthField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
// Trim each ID and join with ', ' to ensure formatting
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
@@ -259,7 +259,7 @@ export const identityProjectServiceFactory = ({
|
||||
if (!hasRequiredPriviledges)
|
||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||
|
||||
const [deletedIdentity] = await identityProjectDAL.delete({ identityId });
|
||||
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
|
||||
return deletedIdentity;
|
||||
};
|
||||
|
||||
|
@@ -43,6 +43,11 @@ export enum IntegrationInitialSyncBehavior {
|
||||
PREFER_SOURCE = "prefer-source"
|
||||
}
|
||||
|
||||
export enum IntegrationMappingBehavior {
|
||||
ONE_TO_ONE = "one-to-one",
|
||||
MANY_TO_ONE = "many-to-one"
|
||||
}
|
||||
|
||||
export enum IntegrationUrls {
|
||||
// integration oauth endpoints
|
||||
GCP_TOKEN_URL = "https://oauth2.googleapis.com/token",
|
||||
|
@@ -30,7 +30,12 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
Integrations,
|
||||
IntegrationUrls
|
||||
} from "./integration-list";
|
||||
|
||||
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
|
||||
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
||||
@@ -570,134 +575,149 @@ const syncSecretsAWSSecretManager = async ({
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let secretsManager;
|
||||
const secKeyVal = getSecretKeyValuePair(secrets);
|
||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||
try {
|
||||
if (!accessId) return;
|
||||
|
||||
secretsManager = new SecretsManagerClient({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
if (!accessId) return;
|
||||
|
||||
const secretsManager = new SecretsManagerClient({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
accessKeyId: accessId,
|
||||
secretAccessKey: accessToken
|
||||
}
|
||||
});
|
||||
|
||||
const processAwsSecret = async (
|
||||
secretId: string,
|
||||
secretValue: Record<string, string | null | undefined> | string
|
||||
) => {
|
||||
try {
|
||||
const awsSecretManagerSecret = await secretsManager.send(
|
||||
new GetSecretValueCommand({
|
||||
SecretId: secretId
|
||||
})
|
||||
);
|
||||
|
||||
let secretToCompare;
|
||||
if (awsSecretManagerSecret?.SecretString) {
|
||||
if (typeof secretValue === "string") {
|
||||
secretToCompare = awsSecretManagerSecret.SecretString;
|
||||
} else {
|
||||
secretToCompare = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const awsSecretManagerSecret = await secretsManager.send(
|
||||
new GetSecretValueCommand({
|
||||
SecretId: integration.app as string
|
||||
})
|
||||
);
|
||||
if (!isEqual(secretToCompare, secretValue)) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
SecretId: secretId,
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let awsSecretManagerSecretObj: { [key: string]: AWS.SecretsManager } = {};
|
||||
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
|
||||
|
||||
if (awsSecretManagerSecret?.SecretString) {
|
||||
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
}
|
||||
if (secretAWSTag && secretAWSTag.length) {
|
||||
const describedSecret = await secretsManager.send(
|
||||
// requires secretsmanager:DescribeSecret policy
|
||||
new DescribeSecretCommand({
|
||||
SecretId: secretId
|
||||
})
|
||||
);
|
||||
|
||||
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
SecretId: integration.app as string,
|
||||
SecretString: JSON.stringify(secKeyVal)
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!describedSecret.Tags) return;
|
||||
|
||||
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
|
||||
const integrationTagObj = secretAWSTag.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
if (secretAWSTag && secretAWSTag.length) {
|
||||
const describedSecret = await secretsManager.send(
|
||||
// requires secretsmanager:DescribeSecret policy
|
||||
new DescribeSecretCommand({
|
||||
SecretId: integration.app as string
|
||||
})
|
||||
);
|
||||
const awsTagObj = (describedSecret.Tags || []).reduce(
|
||||
(acc, item) => {
|
||||
if (item.Key && item.Value) {
|
||||
acc[item.Key] = item.Value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
if (!describedSecret.Tags) return;
|
||||
const tagsToUpdate: { Key: string; Value: string }[] = [];
|
||||
const tagsToDelete: { Key: string; Value: string }[] = [];
|
||||
|
||||
const integrationTagObj = secretAWSTag.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const awsTagObj = (describedSecret.Tags || []).reduce(
|
||||
(acc, item) => {
|
||||
if (item.Key && item.Value) {
|
||||
acc[item.Key] = item.Value;
|
||||
describedSecret.Tags?.forEach((tag) => {
|
||||
if (tag.Key && tag.Value) {
|
||||
if (!(tag.Key in integrationTagObj)) {
|
||||
// delete tag from AWS secret manager
|
||||
tagsToDelete.push({
|
||||
Key: tag.Key,
|
||||
Value: tag.Value
|
||||
});
|
||||
} else if (tag.Value !== integrationTagObj[tag.Key]) {
|
||||
// update tag in AWS secret manager
|
||||
tagsToUpdate.push({
|
||||
Key: tag.Key,
|
||||
Value: integrationTagObj[tag.Key]
|
||||
});
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
});
|
||||
|
||||
const tagsToUpdate: { Key: string; Value: string }[] = [];
|
||||
const tagsToDelete: { Key: string; Value: string }[] = [];
|
||||
|
||||
describedSecret.Tags?.forEach((tag) => {
|
||||
if (tag.Key && tag.Value) {
|
||||
if (!(tag.Key in integrationTagObj)) {
|
||||
// delete tag from AWS secret manager
|
||||
tagsToDelete.push({
|
||||
Key: tag.Key,
|
||||
Value: tag.Value
|
||||
});
|
||||
} else if (tag.Value !== integrationTagObj[tag.Key]) {
|
||||
// update tag in AWS secret manager
|
||||
secretAWSTag?.forEach((tag) => {
|
||||
if (!(tag.key in awsTagObj)) {
|
||||
// create tag in AWS secret manager
|
||||
tagsToUpdate.push({
|
||||
Key: tag.Key,
|
||||
Value: integrationTagObj[tag.Key]
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
secretAWSTag?.forEach((tag) => {
|
||||
if (!(tag.key in awsTagObj)) {
|
||||
// create tag in AWS secret manager
|
||||
tagsToUpdate.push({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
});
|
||||
if (tagsToUpdate.length) {
|
||||
await secretsManager.send(
|
||||
new TagResourceCommand({
|
||||
SecretId: secretId,
|
||||
Tags: tagsToUpdate
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (tagsToUpdate.length) {
|
||||
await secretsManager.send(
|
||||
new TagResourceCommand({
|
||||
SecretId: integration.app as string,
|
||||
Tags: tagsToUpdate
|
||||
})
|
||||
);
|
||||
if (tagsToDelete.length) {
|
||||
await secretsManager.send(
|
||||
new UntagResourceCommand({
|
||||
SecretId: secretId,
|
||||
TagKeys: tagsToDelete.map((tag) => tag.Key)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tagsToDelete.length) {
|
||||
} catch (err) {
|
||||
// case when AWS manager can't find the specified secret
|
||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||
await secretsManager.send(
|
||||
new UntagResourceCommand({
|
||||
SecretId: integration.app as string,
|
||||
TagKeys: tagsToDelete.map((tag) => tag.Key)
|
||||
new CreateSecretCommand({
|
||||
Name: secretId,
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
|
||||
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
|
||||
: []
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// case when AWS manager can't find the specified secret
|
||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||
await secretsManager.send(
|
||||
new CreateSecretCommand({
|
||||
Name: integration.app as string,
|
||||
SecretString: JSON.stringify(secKeyVal),
|
||||
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
|
||||
: []
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
|
||||
for await (const [key, value] of Object.entries(secrets)) {
|
||||
await processAwsSecret(key, value.value);
|
||||
}
|
||||
} else {
|
||||
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2676,18 +2696,21 @@ const syncSecretsCloudflarePages = async ({
|
||||
})
|
||||
).data.result.deployment_configs[integration.targetEnvironment as string].env_vars;
|
||||
|
||||
// copy the secrets object, so we can set deleted keys to null
|
||||
const secretsObj = Object.fromEntries(
|
||||
Object.entries(getSecretKeyValuePair(secrets)).map(([key, val]) => [
|
||||
key,
|
||||
key in Object.keys(getSecretsRes) ? { type: "secret_text", value: val } : null
|
||||
])
|
||||
);
|
||||
let secretEntries: [string, object | null][] = Object.entries(getSecretKeyValuePair(secrets)).map(([key, val]) => [
|
||||
key,
|
||||
{ type: "secret_text", value: val }
|
||||
]);
|
||||
|
||||
if (getSecretsRes) {
|
||||
const toDeleteKeys = Object.keys(getSecretsRes).filter((key) => !Object.keys(secrets).includes(key));
|
||||
const toDeleteEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
|
||||
secretEntries = [...secretEntries, ...toDeleteEntries];
|
||||
}
|
||||
|
||||
const data = {
|
||||
deployment_configs: {
|
||||
[integration.targetEnvironment as string]: {
|
||||
env_vars: secretsObj
|
||||
env_vars: Object.fromEntries(secretEntries)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2965,7 +2988,7 @@ const syncSecretsDigitalOceanAppPlatform = async ({
|
||||
spec: {
|
||||
name: integration.app,
|
||||
...appSettings,
|
||||
envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value }))
|
||||
envs: Object.entries(secrets).map(([key, data]) => ({ key, value: data.value, type: "SECRET" }))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -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">;
|
||||
|
@@ -3,6 +3,7 @@ import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/en
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TGetPrivateKeyDTO } from "./project-bot-types";
|
||||
|
||||
export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||
@@ -13,11 +14,17 @@ export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => {
|
||||
export const getBotKeyFnFactory = (
|
||||
projectBotDAL: TProjectBotDALFactory,
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">
|
||||
) => {
|
||||
const getBotKeyFn = async (projectId: string) => {
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
|
||||
|
||||
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||
const bot = await projectBotDAL.findOne({ projectId: project.id });
|
||||
|
||||
if (!bot) throw new BadRequestError({ message: "Failed to find bot key" });
|
||||
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||
throw new BadRequestError({ message: "Encryption key missing" });
|
||||
|
@@ -25,7 +25,7 @@ export const projectBotServiceFactory = ({
|
||||
projectDAL,
|
||||
permissionService
|
||||
}: TProjectBotServiceFactoryDep) => {
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||
|
||||
const getBotKey = async (projectId: string) => {
|
||||
return getBotKeyFn(projectId);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
@@ -104,9 +106,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectGhostUser = async (projectId: string) => {
|
||||
const findProjectGhostUser = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const ghostUser = await db(TableName.ProjectMembership)
|
||||
const ghostUser = await (tx || db)(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
|
@@ -340,7 +340,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const deletedProject = await projectDAL.transaction(async (tx) => {
|
||||
const delProject = await projectDAL.deleteById(project.id, tx);
|
||||
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id).catch(() => null);
|
||||
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null);
|
||||
|
||||
// Delete the org membership for the ghost user if it's found.
|
||||
if (projectGhostUser) {
|
||||
|
@@ -0,0 +1,58 @@
|
||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
|
||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||
queueService: TQueueServiceFactory;
|
||||
};
|
||||
|
||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||
|
||||
export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
identityAccessTokenDAL
|
||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await identityAccessTokenDAL.removeExpiredTokens();
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||
});
|
||||
|
||||
// we do a repeat cron job in utc timezone at 12 Midnight each day
|
||||
const startCleanUp = async () => {
|
||||
// TODO(akhilmhdh): remove later
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.AuditLogPrune,
|
||||
QueueJobs.AuditLogPrune,
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
QueueName.AuditLogPrune // just a job id
|
||||
);
|
||||
// clear previous job
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.DailyResourceCleanUp,
|
||||
QueueJobs.DailyResourceCleanUp,
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
QueueName.DailyResourceCleanUp // just a job id
|
||||
);
|
||||
|
||||
await queueService.queue(QueueName.DailyResourceCleanUp, QueueJobs.DailyResourceCleanUp, undefined, {
|
||||
delay: 5000,
|
||||
jobId: QueueName.DailyResourceCleanUp,
|
||||
repeat: { pattern: "0 0 * * *", utc: true }
|
||||
});
|
||||
};
|
||||
|
||||
queueService.listen(QueueName.DailyResourceCleanUp, "failed", (_, err) => {
|
||||
logger.error(err, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`);
|
||||
});
|
||||
|
||||
return {
|
||||
startCleanUp
|
||||
};
|
||||
};
|
@@ -608,7 +608,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
secretVersionTagDAL,
|
||||
folderDAL
|
||||
}: TCreateManySecretsRawFnFactory) => {
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||
const createManySecretsRawFn = async ({
|
||||
projectId,
|
||||
environment,
|
||||
@@ -706,7 +706,7 @@ export const updateManySecretsRawFnFactory = ({
|
||||
secretVersionTagDAL,
|
||||
folderDAL
|
||||
}: TUpdateManySecretsRawFnFactory) => {
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||
const updateManySecretsRawFn = async ({
|
||||
projectId,
|
||||
environment,
|
||||
|
@@ -463,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);
|
||||
|
28
company/handbook/onboarding.mdx
Normal file
28
company/handbook/onboarding.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Onboarding"
|
||||
sidebarTitle: "Onboarding"
|
||||
description: "This handbook explains how we work at Infisical."
|
||||
---
|
||||
|
||||
Welcome to Infisical!
|
||||
|
||||
The first few days of every new joiner are going to be packed with learning lots of new information, meeting new teammates, and understanding Infisical on a deeper level.
|
||||
|
||||
Plus, our team is remote-first and spread across the globe (from San Francisco to Philippines), so having a great onboarding experience is very important for the new joiner to feel part of the team and be excited about what we're doing as a company.
|
||||
|
||||
## Onboarding buddy
|
||||
|
||||
Every new joiner has an onboarding buddy who should ideally be in the the same timezone. The onboarding buddy should be able to help with any questions that pop up during the first few weeks. Of course, everyone is available to help, but it's good to have a dedicated person that you can go to with any questions.
|
||||
|
||||
## Onboarding Checklist
|
||||
|
||||
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
|
||||
2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
|
||||
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
|
||||
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1oy_NP1Q_Zt1oqxLpyNkLIGmhAI3N28AmZq6dDIOONSQ/edit?usp=sharing).
|
||||
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
|
||||
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
|
||||
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
|
||||
8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical.
|
||||
|
||||
|
11
company/handbook/overview.mdx
Normal file
11
company/handbook/overview.mdx
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Infisical Company Handbook"
|
||||
sidebarTitle: "Welcome"
|
||||
description: "This handbook explains how we work at Infisical."
|
||||
---
|
||||
|
||||
Welcome! This handbook explains how we work and what we stand for at Infisical.
|
||||
|
||||
Given that Infisical's core is open source, we decided to make this handbook also availably publicly to everyone.
|
||||
|
||||
You can treat it as a living document as more pages and information will be added over time.
|
27
company/handbook/spending-money.mdx
Normal file
27
company/handbook/spending-money.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: "Spenging Money"
|
||||
sidebarTitle: "Spending Money"
|
||||
description: "The guide to spending money at Infisical."
|
||||
---
|
||||
|
||||
Fairly frequently, you might run into situations when you need to spend company money.
|
||||
|
||||
**Please spend money in a way that you think is in the best interest of the company.**
|
||||
|
||||
## Trivial expenses
|
||||
|
||||
We don't want you to be slowed down because you're waiting for an approval to purchase some SaaS. For trivial expenses – **Just do it**.
|
||||
|
||||
This means expenses that are:
|
||||
1. Non-recurring AND less than $75/month in total.
|
||||
2. Recurring AND less than $20/month.
|
||||
|
||||
## Saving receipts
|
||||
|
||||
Make sure you keep copies for all receipts. If you expense something on a company card and cannot provide a receipt, this may be deducted from your pay.
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
|
||||
## Brex
|
||||
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "Infisical",
|
||||
"openapi": "https://app.infisical.com/api/docs/json",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg",
|
||||
@@ -44,33 +43,21 @@
|
||||
"name": "Start for Free",
|
||||
"url": "https://app.infisical.com/signup"
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
"name": "Integrations",
|
||||
"url": "integrations"
|
||||
},
|
||||
{
|
||||
"name": "CLI",
|
||||
"url": "cli"
|
||||
},
|
||||
{
|
||||
"name": "API Reference",
|
||||
"url": "api-reference"
|
||||
},
|
||||
{
|
||||
"name": "SDKs",
|
||||
"url": "sdks"
|
||||
},
|
||||
{
|
||||
"name": "Changelog",
|
||||
"url": "changelog"
|
||||
}
|
||||
],
|
||||
"primaryTab": {
|
||||
"name": "About"
|
||||
},
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"group": "Handbook",
|
||||
"pages": [
|
||||
"documentation/getting-started/introduction"
|
||||
"handbook/overview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "How we work",
|
||||
"pages": [
|
||||
"handbook/onboarding",
|
||||
"handbook/spending-money"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@@ -37,6 +37,13 @@
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#sidebar li > a.text-primary {
|
||||
border-radius: 0;
|
||||
background-color: #FBFFCC;
|
||||
border-left: 4px solid #EFFF33;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* #sidebar ul > div.mt-12 {
|
||||
padding-top: 30px;
|
||||
position: relative;
|
||||
@@ -49,10 +56,10 @@
|
||||
} */
|
||||
|
||||
#header {
|
||||
border-left: 1px solid #26272b;
|
||||
border-left: 4px solid #EFFF33;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
background-color: #f5f5f5;
|
||||
background-color: #FDFFE5;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
@@ -63,6 +70,13 @@
|
||||
border-color: #ebebeb;
|
||||
}
|
||||
|
||||
#content-area:hover .mt-8 .block:hover{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
background-color: #FDFFE5;
|
||||
border-color: #EFFF33;
|
||||
}
|
||||
|
||||
#content-area .mt-8 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ Initialize a new Node.js project with a default `package.json` file.
|
||||
npm init -y
|
||||
```
|
||||
|
||||
Install `express` and [infisical-node](https://github.com/Infisical/infisical-node), the client Node SDK for Infisical.
|
||||
Install `express` and [@infisical/sdk](https://www.npmjs.com/package/@infisical/sdk), the client Node SDK for Infisical.
|
||||
|
||||
```console
|
||||
npm install express @infisical/sdk
|
||||
@@ -46,16 +46,19 @@ Finally, create an index.js file containing the application code.
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const { InfisicalClient, LogLevel } = require("@infisical/sdk");
|
||||
const { InfisicalClient } = require("@infisical/sdk");
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
const client = new InfisicalClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
logLevel: LogLevel.Error
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
|
@@ -5,7 +5,7 @@ title: "Python"
|
||||
This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses:
|
||||
|
||||
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
|
||||
- The [infisical-python](https://github.com/Infisical/sdk/tree/main/crates/infisical-py) Python client SDK to fetch secrets back to your Python application on demand.
|
||||
- The [infisical-python](https://pypi.org/project/infisical-python/) Python client SDK to fetch secrets back to your Python application on demand.
|
||||
|
||||
## Project Setup
|
||||
|
||||
@@ -36,23 +36,27 @@ python3 -m venv env
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
Install Flask and [infisical-python](https://github.com/Infisical/sdk/tree/main/crates/infisical-py), the client Python SDK for Infisical.
|
||||
Install Flask and [infisical-python](https://pypi.org/project/infisical-python/), the client Python SDK for Infisical.
|
||||
|
||||
```console
|
||||
pip install Flask infisical-python
|
||||
pip install flask infisical-python
|
||||
```
|
||||
|
||||
Finally, create an `app.py` file containing the application code.
|
||||
|
||||
```py
|
||||
from flask import Flask
|
||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions
|
||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
client_id="MACHINE_IDENTITY_CLIENT_ID",
|
||||
client_secret="MACHINE_IDENTITY_CLIENT_SECRET",
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
@app.route("/")
|
||||
|
@@ -280,6 +280,10 @@ access the Infisical API using the AWS Auth authentication method.
|
||||
--data-urlencode 'iamRequestHeaders=...'
|
||||
```
|
||||
|
||||
<Note>
|
||||
Note that you should replace `<identityId>` with the ID of the identity you created in step 1.
|
||||
</Note>
|
||||
|
||||
#### Sample response
|
||||
|
||||
```bash Response
|
||||
|
176
docs/documentation/platform/identities/azure-auth.mdx
Normal file
176
docs/documentation/platform/identities/azure-auth.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Azure Auth
|
||||
description: "Learn how to authenticate with Infisical for services on Azure"
|
||||
---
|
||||
|
||||
**Azure Auth** is an Azure-native authentication method for Azure resources like Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc. to access Infisical.
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence digram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client
|
||||
participant Infis as Infisical
|
||||
participant Azure as Azure AD OpenID
|
||||
|
||||
Note over Client,Azure: Step 1: Instance Identity Token Retrieval
|
||||
Client->>Azure: Request managed identity access token
|
||||
Azure-->>Client: Return managed identity access token
|
||||
|
||||
Note over Client,Infis: Step 2: Identity Token Login Operation
|
||||
Client->>Infis: Send managed identity access token to /api/v1/auth/azure-auth/login
|
||||
Infis->>Azure: Request public key
|
||||
Azure-->>Infis: Return public key
|
||||
|
||||
Note over Infis: Step 3: Identity Token Verification
|
||||
Note over Infis: Step 4: Identity Property Validation
|
||||
Infis->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infis: Step 4: Access Infisical API with Token
|
||||
Client->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
At a high-level, Infisical authenticates an Azure service by verifying its identity and checking that it meets specific requirements (e.g. it is bound to an allowed service principal) at the `/api/v1/auth/azure-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 client running on an Azure service obtains an [access token](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http) that is a JWT token representing the managed identity for the Azure resource such as a Virtual Machine; the managed identity is associated with a service principal in Azure AD.
|
||||
2. The client sends the access token to Infisical.
|
||||
3. Infisical verifies the token against the corresponding public key at the [public Azure AD OpenID configuration endpoint](https://learn.microsoft.com/en-us/answers/questions/793793/azure-ad-validate-access-token).
|
||||
4. Infisical checks if the entity behind the access token is allowed to authenticate with Infisical based on set criteria such as **Allowed Service Principal IDs**.
|
||||
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.
|
||||
|
||||
<Note>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
|
||||
to authenticate with Infisical using Azure Auth as they handle the
|
||||
authentication process including generating the client access token for you.
|
||||
|
||||
Also, note that Infisical needs network-level access to send requests to the Google Cloud API
|
||||
as part of the Azure Auth workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your applications in Azure to
|
||||
access the Infisical API using the Azure Auth authentication method.
|
||||
|
||||
<Steps>
|
||||
<Step title="Creating an identity">
|
||||
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
|
||||
|
||||

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

|
||||
|
||||
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 **Azure Auth**.
|
||||
|
||||

|
||||
|
||||
Here's some more guidance on each field:
|
||||
|
||||
- Tenant ID: The [tenant ID](https://learn.microsoft.com/en-us/entra/fundamentals/how-to-find-tenant) for the Azure AD organization.
|
||||
- Resource / Audience: The resource URL for the application registered in Azure AD. The value is expected to match the `aud` claim of the access token JWT later used in the login operation against Infisical. See the [resource](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http) parameter for how the audience is set when requesting a JWT access token from the Azure Instance Metadata Service (IMDS) endpoint. In most cases, this value should be `https://management.azure.com/` which is the default.
|
||||
- Allowed Service Principal IDs: A comma-separated list of Azure AD service principal IDs that are allowed to authenticate with Infisical.
|
||||
- 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.
|
||||
|
||||

|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Accessing the Infisical API with the identity">
|
||||
To access the Infisical API as the identity, you need to generate a managed identity [access token](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http) that is a JWT token representing the managed identity for the Azure resource such as a Virtual Machine. The client token must be sent to the `/api/v1/auth/azure-auth/login` endpoint in exchange for a separate access token to access the Infisical API.
|
||||
|
||||
We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction).
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion
|
||||
title="Sample code for generating the access token"
|
||||
>
|
||||
Start by making a request from your Azure client such as Virtual Machine to obtain a managed identity access token.
|
||||
|
||||
For more examples of how to obtain the managed identity access token, refer to the [official documentation](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http).
|
||||
|
||||
#### Sample request
|
||||
```bash curl
|
||||
curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s
|
||||
```
|
||||
|
||||
#### Sample response
|
||||
```bash
|
||||
{
|
||||
"access_token": "eyJ0eXAi...",
|
||||
"refresh_token": "",
|
||||
"expires_in": "3599",
|
||||
"expires_on": "1506484173",
|
||||
"not_before": "1506480273",
|
||||
"resource": "https://management.azure.com/",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
Next use send the obtained managed identity access token (i.e. the token from the `access_token` field above) to authenticate with Infisical and obtain a separate access token.
|
||||
|
||||
#### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/auth/gcp-auth/login' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'identityId=...' \
|
||||
--data-urlencode 'jwt=...'
|
||||
```
|
||||
|
||||
<Note>
|
||||
Note that you should replace `<identityId>` with the ID of the identity you created in step 1.
|
||||
</Note>
|
||||
|
||||
#### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"accessToken": "...",
|
||||
"expiresIn": 7200,
|
||||
"accessTokenMaxTTL": 43244
|
||||
"tokenType": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
Next, you can use this access token to access the [Infisical API](/api-reference/overview/introduction)
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Tip>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using Azure Auth as they handle the authentication process including retrieving the client access token.
|
||||
</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 expires, 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>
|
@@ -7,9 +7,9 @@ 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), [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.
|
||||
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), [Azure Auth](/documentation/platform/identities/azure-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
|
||||
|
||||

|
||||

|
||||
|
||||
Key Features:
|
||||
|
||||
@@ -39,11 +39,10 @@ To interact with various resources in Infisical, Machine Identities are able to
|
||||
|
||||
- [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.
|
||||
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for AWS services (e.g. EC2, Lambda functions, etc.) to authenticate with Infisical.
|
||||
- [Azure Auth](/documentation/platform/identities/azure-auth): An Azure-native authentication method for Azure resources (e.g. Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc.) 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.
|
||||
|
||||
IAM service accounts and GCE instances to authenticate with Infisical.
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 142 KiB |
Binary file not shown.
After Width: | Height: | Size: 513 KiB |
@@ -72,6 +72,9 @@ Prerequisites:
|
||||
<ParamField path="AWS Region" type="string" required>
|
||||
The region that you want to integrate with in AWS Secrets Manager.
|
||||
</ParamField>
|
||||
<ParamField path="Mapping Behavior" type="string" required>
|
||||
How you want the integration to map the secrets. The selected value could be either one to one or one to many.
|
||||
</ParamField>
|
||||
<ParamField path="AWS SM Secret Name" type="string" required>
|
||||
The secret name/path in AWS into which you want to sync the secrets from Infisical.
|
||||
</ParamField>
|
||||
|
@@ -51,6 +51,8 @@ description: "How to sync secrets from Infisical to GCP Secret Manager"
|
||||
<Warning>
|
||||
Using Infisical to sync secrets to GCP Secret Manager requires that you enable
|
||||
the Service Usage API and Cloud Resource Manager API in the Google Cloud project you want to sync secrets to. More on that [here](https://cloud.google.com/service-usage/docs/set-up-development-environment).
|
||||
|
||||
Additionally, ensure that your GCP account has sufficient permission to manage secret and service resources (you can assign Secret Manager Admin and Service Usage Admin roles for testing purposes)
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -115,6 +117,7 @@ description: "How to sync secrets from Infisical to GCP Secret Manager"
|
||||
</Steps>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Self-Hosted Setup">
|
||||
Using the GCP Secret Manager integration (via the OAuth2 method) on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP
|
||||
@@ -123,27 +126,27 @@ description: "How to sync secrets from Infisical to GCP Secret Manager"
|
||||
<Steps>
|
||||
<Step title="Create an OAuth2 application in GCP">
|
||||
Navigate to your project API & Services > Credentials to create a new OAuth2 application.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
Create the application. As part of the form, add to **Authorized redirect URIs**: `https://your-domain.com/integrations/gcp-secret-manager/oauth2/callback`.
|
||||
|
||||

|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Add your OAuth2 application credentials to Infisical">
|
||||
Obtain the **Client ID** and **Client Secret** for your GCP OAuth2 application.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
Back in your Infisical instance, add two new environment variables for the credentials of your GCP OAuth2 application:
|
||||
|
||||
- `CLIENT_ID_GCP_SECRET_MANAGER`: The **Client ID** of your GCP OAuth2 application.
|
||||
- `CLIENT_SECRET_GCP_SECRET_MANAGER`: The **Client Secret** of your GCP OAuth2 application.
|
||||
|
||||
|
||||
Once added, restart your Infisical instance and use the GCP Secret Manager integration.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
@@ -160,6 +160,7 @@
|
||||
"documentation/platform/identities/universal-auth",
|
||||
"documentation/platform/identities/kubernetes-auth",
|
||||
"documentation/platform/identities/gcp-auth",
|
||||
"documentation/platform/identities/azure-auth",
|
||||
"documentation/platform/identities/aws-auth",
|
||||
"documentation/platform/mfa",
|
||||
{
|
||||
|
@@ -21,21 +21,28 @@ namespace Example
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
var settings = new ClientSettings
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
ClientId = "CLIENT_ID",
|
||||
ClientSecret = "CLIENT_SECRET",
|
||||
// SiteUrl = "http://localhost:8080", <-- This line can be omitted if you're using Infisical Cloud.
|
||||
};
|
||||
var infisical = new InfisicalClient(settings);
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var options = new GetSecretOptions
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
|
||||
var getSecretOptions = new GetSecretOptions
|
||||
{
|
||||
SecretName = "TEST",
|
||||
ProjectId = "PROJECT_ID",
|
||||
Environment = "dev",
|
||||
};
|
||||
var secret = infisical.GetSecret(options);
|
||||
var secret = infisical.GetSecret(getSecretOptions);
|
||||
|
||||
|
||||
Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}");
|
||||
@@ -52,8 +59,6 @@ This example demonstrates how to use the Infisical C# SDK in a C# application. T
|
||||
|
||||
# Installation
|
||||
|
||||
Run `npm` to add `@infisical/sdk` to your project.
|
||||
|
||||
```console
|
||||
$ dotnet add package Infisical.Sdk
|
||||
```
|
||||
@@ -70,14 +75,20 @@ namespace Example
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
var settings = new ClientSettings
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
ClientId = "CLIENT_ID",
|
||||
ClientSecret = "CLIENT_SECRET",
|
||||
};
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisical = new InfisicalClient(settings); // <-- Your SDK instance!
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings); // <-- Your SDK client is now ready to use
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,14 +98,14 @@ namespace Example
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ClientId" type="string" optional>
|
||||
<ParamField query="ClientId" deprecated type="string" optional>
|
||||
Your machine identity client ID.
|
||||
</ParamField>
|
||||
<ParamField query="ClientSecret" type="string" optional>
|
||||
<ParamField query="ClientSecret" deprecated type="string" optional>
|
||||
Your machine identity client secret.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="AccessToken" type="string" optional>
|
||||
<ParamField query="AccessToken" deprecated type="string" optional>
|
||||
An access token obtained from the machine identity login endpoint.
|
||||
</ParamField>
|
||||
|
||||
@@ -103,13 +114,175 @@ namespace Example
|
||||
If manually set to 0, caching will be disabled, this is not recommended.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SiteUrl()" type="string" default="https://app.infisical.com" optional>
|
||||
<ParamField query="SiteUrl" type="string" default="https://app.infisical.com" optional>
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Auth" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
UniversalAuth = new UniversalAuthMethod
|
||||
{
|
||||
ClientId = "your-client-id",
|
||||
ClientSecret = "your-client-secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
GcpIdToken = new GcpIdTokenAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
GcpIam = new GcpIamAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
ServiceAccountKeyFilePath = "./path/to/your/service-account-key.json"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
AwsIam = new AwsIamAuthMethod
|
||||
{
|
||||
IdentityId = "your-machine-identity-id",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
Azure = new AzureAuthMethod
|
||||
{
|
||||
IdentityId = "YOUR_IDENTITY_ID",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```csharp
|
||||
ClientSettings settings = new ClientSettings
|
||||
{
|
||||
Auth = new AuthenticationOptions
|
||||
{
|
||||
Kubernetes = new KubernetesAuthMethod
|
||||
{
|
||||
ServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token", // Optional
|
||||
IdentityId = "YOUR_IDENTITY_ID",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var infisicalClient = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client.
|
||||
@@ -155,6 +328,14 @@ Retrieve all secrets within the Infisical project and environment that client is
|
||||
<ParamField query="IncludeImports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="ExpandSecretReferences" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
@@ -19,12 +19,19 @@ import com.infisical.sdk.schema.*;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
// Create a new Infisical Client
|
||||
|
||||
// Create the authentication settings for the client
|
||||
ClientSettings settings = new ClientSettings();
|
||||
settings.setClientID("MACHINE_IDENTITY_CLIENT_ID");
|
||||
settings.setClientSecret("MACHINE_IDENTITY_CLIENT_SECRET");
|
||||
settings.setCacheTTL(Long.valueOf(300)); // 300 seconds, 5 minutes
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
// Create a new Infisical Client
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
|
||||
// Create the options for fetching the secret
|
||||
@@ -68,11 +75,18 @@ import com.infisical.sdk.schema.*;
|
||||
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
|
||||
// Create the authentication settings for the client
|
||||
ClientSettings settings = new ClientSettings();
|
||||
settings.setClientID("MACHINE_IDENTITY_CLIENT_ID");
|
||||
settings.setClientSecret("MACHINE_IDENTITY_CLIENT_SECRET");
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
// Create a new Infisical Client
|
||||
InfisicalClient client = new InfisicalClient(settings); // Your client!
|
||||
}
|
||||
}
|
||||
@@ -82,15 +96,21 @@ public class App {
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="setClientID()" type="string" optional>
|
||||
<ParamField query="setClientID()" type="string" deprecated optional>
|
||||
Your machine identity client ID.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
<ParamField query="setClientSecret()" type="string" optional>
|
||||
<ParamField query="setClientSecret()" deprecated type="string" optional>
|
||||
Your machine identity client secret.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setAccessToken()" type="string" optional>
|
||||
<ParamField query="setAccessToken()" deprecatedtype="string" optional>
|
||||
An access token obtained from the machine identity login endpoint.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setCacheTTL()" type="number" default="300" optional>
|
||||
@@ -101,10 +121,155 @@ public class App {
|
||||
<ParamField query="setSiteURL()" type="string" default="https://app.infisical.com" optional>
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setAuth()" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
UniversalAuthMethod authMethod = new UniversalAuthMethod();
|
||||
|
||||
authMethod.setClientID("YOUR_IDENTITY_ID");
|
||||
authMethod.setClientSecret("YOUR_CLIENT_SECRET");
|
||||
|
||||
authOptions.setUniversalAuth(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
GCPIDTokenAuthMethod authMethod = new GCPIDTokenAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
|
||||
authOptions.setGcpIDToken(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
GCPIamAuthMethod authMethod = new GCPIamAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
authMethod.setServiceAccountKeyFilePath("./path/to/your/service-account-key.json");
|
||||
|
||||
authOptions.setGcpIam(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
AWSIamAuthMethod authMethod = new AWSIamAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID");
|
||||
|
||||
authOptions.setAwsIam(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
AzureAuthMethod authMethod = new AzureAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_IDENTITY_ID");
|
||||
|
||||
authOptions.setAzure(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```java
|
||||
ClientSettings settings = new ClientSettings();
|
||||
AuthenticationOptions authOptions = new AuthenticationOptions();
|
||||
KubernetesAuthMethod authMethod = new KubernetesAuthMethod();
|
||||
|
||||
authMethod.setIdentityID("YOUR_IDENTITY_ID");
|
||||
authMethod.setServiceAccountTokenPath("/var/run/secrets/kubernetes.io/serviceaccount/token"); // Optional
|
||||
|
||||
authOptions.setKubernetes(authMethod);
|
||||
settings.setAuth(authOptions);
|
||||
|
||||
InfisicalClient client = new InfisicalClient(settings);
|
||||
```
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client.
|
||||
@@ -119,6 +284,8 @@ options.setEnvironment("dev");
|
||||
options.setProjectID("PROJECT_ID");
|
||||
options.setPath("/foo/bar");
|
||||
options.setIncludeImports(false);
|
||||
options.setRecursive(false);
|
||||
options.setExpandSecretReferences(true);
|
||||
|
||||
SecretElement[] secrets = client.listSecrets(options);
|
||||
```
|
||||
@@ -148,6 +315,14 @@ Retrieve all secrets within the Infisical project and environment that client is
|
||||
<ParamField query="setIncludeImports()" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setRecursive()" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="setExpandSecretReferences()" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
@@ -4,7 +4,7 @@ sidebarTitle: "Node.js"
|
||||
icon: "node"
|
||||
---
|
||||
|
||||
If you're working with Node.js, the official [infisical-node](https://github.com/Infisical/sdk/tree/main/languages/node) package is the easiest way to fetch and work with secrets for your application.
|
||||
If you're working with Node.js, the official [Infisical Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [NPM Package](https://www.npmjs.com/package/@infisical/sdk)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/node)
|
||||
@@ -14,21 +14,25 @@ If you're working with Node.js, the official [infisical-node](https://github.com
|
||||
```js
|
||||
import express from "express";
|
||||
|
||||
import { InfisicalClient, LogLevel } from "@infisical/sdk";
|
||||
import { InfisicalClient } from "@infisical/sdk";
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
const client = new InfisicalClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
logLevel: LogLevel.Error
|
||||
siteUrl: "https://app.infisical.com", // Optional, defaults to https://app.infisical.com
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
// access value
|
||||
|
||||
// Access the secret
|
||||
|
||||
const name = await client.getSecret({
|
||||
environment: "dev",
|
||||
projectId: "PROJECT_ID",
|
||||
@@ -72,8 +76,12 @@ Import the SDK and create a client instance with your [Machine Identity](/docume
|
||||
import { InfisicalClient, LogLevel } from "@infisical/sdk";
|
||||
|
||||
const client = new InfisicalClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET"
|
||||
}
|
||||
},
|
||||
logLevel: LogLevel.Error
|
||||
});
|
||||
```
|
||||
@@ -81,31 +89,40 @@ Import the SDK and create a client instance with your [Machine Identity](/docume
|
||||
</Tab>
|
||||
<Tab title="ES5">
|
||||
```js
|
||||
const { InfisicalClient, LogLevel } = require("@infisical/sdk");
|
||||
const { InfisicalClient } = require("@infisical/sdk");
|
||||
|
||||
const client = new InfisicalClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
logLevel: LogLevel.Error
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET"
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
#### Parameters
|
||||
### Parameters
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="clientId" type="string" optional>
|
||||
<ParamField query="clientId" deprecated type="string" optional>
|
||||
Your machine identity client ID.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth.universalAuth.clientId` field instead.
|
||||
</ParamField>
|
||||
<ParamField query="clientSecret" type="string" optional>
|
||||
<ParamField query="clientSecret" deprecated type="string" optional>
|
||||
Your machine identity client secret.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth.universalAuth.clientSecret` field instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="accessToken" type="string" optional>
|
||||
<ParamField query="accessToken" deprecated type="string" optional>
|
||||
An access token obtained from the machine identity login endpoint.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth.accessToken` field instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="cacheTtl" type="number" default="300" optional>
|
||||
@@ -119,10 +136,138 @@ Import the SDK and create a client instance with your [Machine Identity](/docume
|
||||
<ParamField query="logLevel" type="enum" default="Error" optional>
|
||||
The level of logs you wish to log The logs are derived from Rust, as we have written our base SDK in Rust.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="auth" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
gcpIdToken: {
|
||||
identityId: "YOUR_IDENTITY_ID"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
gcpIam: {
|
||||
identityId: "YOUR_IDENTITY_ID",
|
||||
serviceAccountKeyFilePath: "./path/to/your/service-account-key.json"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
awsIam: {
|
||||
identityId: "YOUR_IDENTITY_ID"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
azure: {
|
||||
identityId: "YOUR_IDENTITY_ID"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```js
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
kubernetes: {
|
||||
identityId: "YOUR_IDENTITY_ID",
|
||||
serviceAccountTokenPathEnvName: "/var/run/secrets/kubernetes.io/serviceaccount/token" // Optional
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTtl" option when creating the client.
|
||||
@@ -161,6 +306,14 @@ Retrieve all secrets within the Infisical project and environment that client is
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `process.env["SECRET_NAME"]`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="expandSecretReferences" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="includeImports" type="false" default="boolean" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
@@ -6,20 +6,24 @@ icon: "python"
|
||||
|
||||
If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [PyPi Package](https://pypi.org/project/infisical-python/)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
|
||||
- [PyPi Package](https://pypi.org/project/infisical-python/)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```py
|
||||
from flask import Flask
|
||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions
|
||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
client_id="MACHINE_IDENTITY_CLIENT_ID",
|
||||
client_secret="MACHINE_IDENTITY_CLIENT_SECRET",
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
@app.route("/")
|
||||
@@ -38,7 +42,7 @@ def hello_world():
|
||||
This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value.
|
||||
|
||||
<Warning>
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||
</Warning>
|
||||
|
||||
## Installation
|
||||
@@ -56,11 +60,15 @@ Note: You need Python 3.7+.
|
||||
Import the SDK and create a client instance with your [Machine Identity](/api-reference/overview/authentication).
|
||||
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
client_id="MACHINE_IDENTITY_CLIENT_ID",
|
||||
client_secret="MACHINE_IDENTITY_CLIENT_SECRET",
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
@@ -68,14 +76,20 @@ client = InfisicalClient(ClientSettings(
|
||||
|
||||
<ParamField query="options" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="client_id" type="string" optional>
|
||||
<ParamField query="client_id" type="string" deprecated optional>
|
||||
Your Infisical Client ID.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
<ParamField query="client_secret" type="string" optional>
|
||||
<ParamField query="client_secret" type="string" deprecated optional>
|
||||
Your Infisical Client Secret.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
<ParamField query="access_token" type="string" optional>
|
||||
<ParamField query="access_token" type="string" deprecated optional>
|
||||
If you want to directly pass an access token obtained from the authentication endpoints, you can do so.
|
||||
|
||||
**This field is deprecated and will be removed in future versions.** Please use the `auth` field instead.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="cache_ttl" type="number" default="300" optional>
|
||||
@@ -85,18 +99,155 @@ client = InfisicalClient(ClientSettings(
|
||||
|
||||
|
||||
<ParamField
|
||||
query="site_url"
|
||||
type="string"
|
||||
default="https://app.infisical.com"
|
||||
optional
|
||||
query="site_url"
|
||||
type="string"
|
||||
default="https://app.infisical.com"
|
||||
optional
|
||||
>
|
||||
Your self-hosted absolute site URL including the protocol (e.g.
|
||||
`https://app.infisical.com`)
|
||||
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="auth" type="AuthenticationOptions">
|
||||
The authentication object to use for the client. This is required unless you're using environment variables.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### Authentication
|
||||
|
||||
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||
|
||||
#### Universal Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python3
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
universal_auth=UniversalAuthMethod(
|
||||
client_id="CLIENT_ID",
|
||||
client_secret="CLIENT_SECRET",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### GCP ID Token Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIDTokenAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
gcp_id_token=GCPIDTokenAuthMethod(
|
||||
identity_id="MACHINE_IDENTITY_ID",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### GCP IAM Auth
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIamAuthMethod
|
||||
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
gcp_iam=GCPIamAuthMethod(
|
||||
identity_id="MACHINE_IDENTITY_ID",
|
||||
service_account_key_file_path="./path/to/service_account_key.json"
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### AWS IAM Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on AWS.
|
||||
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```py
|
||||
from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, AWSIamAuthMethod
|
||||
|
||||
client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
aws_iam=AWSIamAuthMethod(identity_id="MACHINE_IDENTITY_ID")
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
#### Azure Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Azure.
|
||||
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python
|
||||
from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, AzureAuthMethod
|
||||
|
||||
kubernetes_client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
azure=AzureAuthMethod(
|
||||
identity_id="YOUR_IDENTITY_ID",
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
|
||||
#### Kubernetes Auth
|
||||
<Info>
|
||||
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||
</Info>
|
||||
|
||||
**Using environment variables**
|
||||
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||
|
||||
**Using the SDK directly**
|
||||
```python
|
||||
from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, KubernetesAuthMethod
|
||||
|
||||
kubernetes_client = InfisicalClient(ClientSettings(
|
||||
auth=AuthenticationOptions(
|
||||
kubernetes=KubernetesAuthMethod(
|
||||
identity_id="YOUR_IDENTITY_ID",
|
||||
service_account_token_path="/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional
|
||||
)
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cache_ttl" option when creating the client.
|
||||
@@ -133,6 +284,14 @@ Retrieve all secrets within the Infisical project and environment that client is
|
||||
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `process.env["SECRET_NAME"]`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="recursive" type="boolean" default="false" optional>
|
||||
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="expand_secret_references" type="boolean" default="true" optional>
|
||||
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
@@ -156,26 +315,26 @@ By default, `getSecret()` fetches and returns a shared secret. If not found, it
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to retrieve
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
|
||||
</ParamField>
|
||||
<ParamField query="include_imports" type="boolean" default="false" optional>
|
||||
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.createSecret(options)
|
||||
@@ -194,26 +353,26 @@ Create a new secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to create.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be created.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.updateSecret(options)
|
||||
@@ -232,26 +391,26 @@ Update an existing secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string" required>
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="secret_value" type="string" required>
|
||||
The new value of the secret.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.deleteSecret(options)
|
||||
@@ -269,23 +428,23 @@ Delete a secret in Infisical.
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="secret_name" type="string">
|
||||
The key of the secret to update.
|
||||
</ParamField>
|
||||
<ParamField query="project_id" type="string" required>
|
||||
The project ID where the secret lives in.
|
||||
</ParamField>
|
||||
<ParamField query="environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||
</ParamField>
|
||||
<ParamField query="path" type="string" optional>
|
||||
The path from where secret should be deleted.
|
||||
</ParamField>
|
||||
<ParamField query="type" type="string" optional>
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Cryptography
|
||||
@@ -299,9 +458,11 @@ key = client.createSymmetricKey()
|
||||
```
|
||||
|
||||
#### Returns (string)
|
||||
|
||||
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
|
||||
|
||||
### Encrypt symmetric
|
||||
|
||||
```py
|
||||
encryptOptions = EncryptSymmetricOptions(
|
||||
key=key,
|
||||
@@ -314,22 +475,22 @@ encryptedData = client.encryptSymmetric(encryptOptions)
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="plaintext" type="string">
|
||||
The plaintext you want to encrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (object)
|
||||
`tag` (string): A base64-encoded, 128-bit authentication tag.
|
||||
`iv` (string): A base64-encoded, 96-bit initialization vector.
|
||||
`ciphertext` (string): A base64-encoded, encrypted ciphertext.
|
||||
|
||||
`tag` (string): A base64-encoded, 128-bit authentication tag. `iv` (string): A base64-encoded, 96-bit initialization vector. `ciphertext` (string): A base64-encoded, encrypted ciphertext.
|
||||
|
||||
### Decrypt symmetric
|
||||
|
||||
```py
|
||||
decryptOptions = DecryptSymmetricOptions(
|
||||
ciphertext=encryptedData.ciphertext,
|
||||
@@ -344,22 +505,24 @@ decryptedString = client.decryptSymmetric(decryptOptions)
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" required>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ciphertext" type="string">
|
||||
The ciphertext you want to decrypt.
|
||||
</ParamField>
|
||||
<ParamField query="key" type="string" required>
|
||||
The symmetric key to use for encryption.
|
||||
</ParamField>
|
||||
<ParamField query="iv" type="string" required>
|
||||
The initialization vector to use for decryption.
|
||||
</ParamField>
|
||||
<ParamField query="tag" type="string" required>
|
||||
The authentication tag to use for decryption.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
#### Returns (string)
|
||||
|
||||
`plaintext` (string): The decrypted plaintext.
|
||||
|
@@ -27,6 +27,13 @@
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.text-primary {
|
||||
border-radius: 0;
|
||||
background-color: #FBFFCC;
|
||||
border-left: 4px solid #EFFF33;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sidebar li > a.mt-2 {
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
@@ -49,10 +56,10 @@
|
||||
} */
|
||||
|
||||
#header {
|
||||
border-left: 1px solid #26272b;
|
||||
border-left: 4px solid #EFFF33;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
background-color: #f5f5f5;
|
||||
background-color: #FDFFE5;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
@@ -63,6 +70,13 @@
|
||||
border-color: #ebebeb;
|
||||
}
|
||||
|
||||
/* #content-area:hover .mt-8 .block:hover{
|
||||
border-radius: 0;
|
||||
border-width: 1px;
|
||||
background-color: #FDFFE5;
|
||||
border-color: #EFFF33;
|
||||
} */
|
||||
|
||||
#content-area .mt-8 .rounded-xl{
|
||||
border-radius: 0;
|
||||
}
|
||||
|
175
frontend/src/helpers/secret.ts
Normal file
175
frontend/src/helpers/secret.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import path from "path";
|
||||
|
||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { fetchProjectEncryptedSecrets } from "@app/hooks/api/secrets/queries";
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
||||
export const interpolateSecrets = ({
|
||||
projectId,
|
||||
secretEncKey
|
||||
}: {
|
||||
projectId: string;
|
||||
secretEncKey: string;
|
||||
}) => {
|
||||
const fetchSecretsCrossEnv = () => {
|
||||
const fetchCache: Record<string, Record<string, string>> = {};
|
||||
|
||||
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
|
||||
const secRefPathUrl = path.join("/", ...secRefPath);
|
||||
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
|
||||
|
||||
if (fetchCache?.[uniqKey]) {
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
}
|
||||
|
||||
// get secrets by projectId, env, path
|
||||
const encryptedSecrets = await fetchProjectEncryptedSecrets({
|
||||
workspaceId: projectId,
|
||||
environment: secRefEnv,
|
||||
secretPath: secRefPathUrl
|
||||
});
|
||||
|
||||
const decryptedSec = encryptedSecrets.reduce<Record<string, string>>((prev, secret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key: secretEncKey
|
||||
});
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key: secretEncKey
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
prev[secretKey] = secretValue;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
fetchCache[uniqKey] = decryptedSec;
|
||||
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
};
|
||||
};
|
||||
|
||||
const recursivelyExpandSecret = async (
|
||||
expandedSec: Record<string, string>,
|
||||
interpolatedSec: Record<string, string>,
|
||||
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
||||
recursionChainBreaker: Record<string, boolean>,
|
||||
key: string
|
||||
) => {
|
||||
if (expandedSec?.[key] !== undefined) {
|
||||
return expandedSec[key];
|
||||
}
|
||||
if (recursionChainBreaker?.[key]) {
|
||||
return "";
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
recursionChainBreaker[key] = true;
|
||||
|
||||
let interpolatedValue = interpolatedSec[key];
|
||||
if (!interpolatedValue) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Couldn't find referenced value - ${key}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
|
||||
if (refs) {
|
||||
await Promise.all(
|
||||
refs.map(async (interpolationSyntax) => {
|
||||
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
|
||||
const entities = interpolationKey.trim().split(".");
|
||||
|
||||
if (entities.length === 1) {
|
||||
const val = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
fetchCrossEnv,
|
||||
recursionChainBreaker,
|
||||
interpolationKey
|
||||
);
|
||||
if (val) {
|
||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (entities.length > 1) {
|
||||
const secRefEnv = entities[0];
|
||||
const secRefPath = entities.slice(1, entities.length - 1);
|
||||
const secRefKey = entities[entities.length - 1];
|
||||
|
||||
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
|
||||
if (val) {
|
||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
expandedSec[key] = interpolatedValue;
|
||||
return interpolatedValue;
|
||||
};
|
||||
|
||||
// used to convert multi line ones to quotes ones with \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
const expandSecrets = async (
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
|
||||
const crossSecEnvFetch = fetchSecretsCrossEnv();
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
|
||||
interpolatedSec[key] = secrets[key].value;
|
||||
} else {
|
||||
expandedSec[key] = secrets[key].value;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(secrets).map(async (key) => {
|
||||
if (expandedSec?.[key]) {
|
||||
// should not do multi line encoding if user has set it to skip
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedSec[key]
|
||||
: formatMultiValueEnv(expandedSec[key]);
|
||||
return;
|
||||
}
|
||||
|
||||
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
|
||||
// so for any recursion building if there is an entity two times same key meaning it will be looped
|
||||
const recursionChainBreaker: Record<string, boolean> = {};
|
||||
const expandedVal = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
crossSecEnvFetch,
|
||||
recursionChainBreaker,
|
||||
key
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedVal
|
||||
: formatMultiValueEnv(expandedVal);
|
||||
})
|
||||
);
|
||||
|
||||
return secrets;
|
||||
};
|
||||
return expandSecrets;
|
||||
};
|
5
frontend/src/helpers/string.ts
Normal file
5
frontend/src/helpers/string.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const removeTrailingSlash = (str: string) => {
|
||||
if (str === "/") return str;
|
||||
|
||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
||||
};
|
@@ -4,5 +4,6 @@ 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"
|
||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
|
||||
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth"
|
||||
};
|
||||
|
@@ -2,5 +2,6 @@ export enum IdentityAuthMethod {
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth"
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ export { identityAuthToNameMap } from "./constants";
|
||||
export { IdentityAuthMethod } from "./enums";
|
||||
export {
|
||||
useAddIdentityAwsAuth,
|
||||
useAddIdentityAzureAuth,
|
||||
useAddIdentityGcpAuth,
|
||||
useAddIdentityKubernetesAuth,
|
||||
useAddIdentityUniversalAuth,
|
||||
@@ -11,11 +12,14 @@ export {
|
||||
useRevokeIdentityUniversalAuthClientSecret,
|
||||
useUpdateIdentity,
|
||||
useUpdateIdentityAwsAuth,
|
||||
useUpdateIdentityAzureAuth,
|
||||
useUpdateIdentityGcpAuth,
|
||||
useUpdateIdentityKubernetesAuth,
|
||||
useUpdateIdentityUniversalAuth} from "./mutations";
|
||||
useUpdateIdentityUniversalAuth
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetIdentityAwsAuth,
|
||||
useGetIdentityAzureAuth,
|
||||
useGetIdentityGcpAuth,
|
||||
useGetIdentityKubernetesAuth,
|
||||
useGetIdentityUniversalAuth,
|
||||
|
@@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries";
|
||||
import { identitiesKeys } from "./queries";
|
||||
import {
|
||||
AddIdentityAwsAuthDTO,
|
||||
AddIdentityAzureAuthDTO,
|
||||
AddIdentityGcpAuthDTO,
|
||||
AddIdentityKubernetesAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
@@ -17,14 +18,17 @@ import {
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
Identity,
|
||||
IdentityAwsAuth,
|
||||
IdentityAzureAuth,
|
||||
IdentityGcpAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityUniversalAuth,
|
||||
UpdateIdentityAwsAuthDTO,
|
||||
UpdateIdentityAzureAuthDTO,
|
||||
UpdateIdentityDTO,
|
||||
UpdateIdentityGcpAuthDTO,
|
||||
UpdateIdentityKubernetesAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO} from "./types";
|
||||
UpdateIdentityUniversalAuthDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateIdentity = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -326,7 +330,41 @@ export const useUpdateIdentityAwsAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// --- K8s auth (TODO: add cert and token reviewer JWT fields)
|
||||
export const useAddIdentityAzureAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAzureAuth, {}, AddIdentityAzureAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityAzureAuth }
|
||||
} = await apiRequest.post<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||
`/api/v1/auth/azure-auth/identities/${identityId}`,
|
||||
{
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityAzureAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityKubernetesAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -370,6 +408,42 @@ export const useAddIdentityKubernetesAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityAzureAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAzureAuth, {}, UpdateIdentityAzureAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityAzureAuth }
|
||||
} = await apiRequest.patch<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||
`/api/v1/auth/azure-auth/identities/${identityId}`,
|
||||
{
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityAzureAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityKubernetesAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityKubernetesAuth, {}, UpdateIdentityKubernetesAuthDTO>({
|
||||
@@ -403,6 +477,7 @@ export const useUpdateIdentityKubernetesAuth = () => {
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityKubernetesAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
|
@@ -5,10 +5,10 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
ClientSecretData,
|
||||
IdentityAwsAuth,
|
||||
IdentityAzureAuth,
|
||||
IdentityGcpAuth,
|
||||
IdentityKubernetesAuth,
|
||||
IdentityUniversalAuth
|
||||
} from "./types";
|
||||
IdentityUniversalAuth} from "./types";
|
||||
|
||||
export const identitiesKeys = {
|
||||
getIdentityUniversalAuth: (identityId: string) =>
|
||||
@@ -18,7 +18,8 @@ export const identitiesKeys = {
|
||||
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
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
|
||||
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const
|
||||
};
|
||||
|
||||
export const useGetIdentityUniversalAuth = (identityId: string) => {
|
||||
@@ -81,6 +82,21 @@ export const useGetIdentityAwsAuth = (identityId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityAzureAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityAzureAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityAzureAuth }
|
||||
} = await apiRequest.get<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||
`/api/v1/auth/azure-auth/identities/${identityId}`
|
||||
);
|
||||
return identityAzureAuth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityKubernetesAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
|
@@ -195,6 +195,45 @@ export type UpdateIdentityAwsAuthDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityAzureAuth = {
|
||||
identityId: string;
|
||||
tenantId: string;
|
||||
resource: string;
|
||||
allowedServicePrincipalIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityAzureAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
tenantId: string;
|
||||
resource: string;
|
||||
allowedServicePrincipalIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityAzureAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
tenantId?: string;
|
||||
resource?: string;
|
||||
allowedServicePrincipalIds?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityKubernetesAuth = {
|
||||
identityId: string;
|
||||
kubernetesHost: 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";
|
||||
@@ -63,6 +64,7 @@ export const useCreateIntegration = () => {
|
||||
secretSuffix?: string;
|
||||
initialSyncBehavior?: string;
|
||||
shouldAutoRedeploy?: boolean;
|
||||
mappingBehavior?: string;
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -110,3 +112,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,10 +29,14 @@ export type TIntegration = {
|
||||
secretPath: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastUsed?: string;
|
||||
isSynced?: boolean;
|
||||
syncMessage?: string;
|
||||
__v: number;
|
||||
metadata?: {
|
||||
secretSuffix?: string;
|
||||
syncBehavior?: IntegrationSyncBehavior;
|
||||
mappingBehavior?: IntegrationMappingBehavior;
|
||||
scope: string;
|
||||
org: string;
|
||||
project: string;
|
||||
@@ -45,3 +49,8 @@ export enum IntegrationSyncBehavior {
|
||||
PREFER_TARGET = "prefer-target",
|
||||
PREFER_SOURCE = "prefer-source"
|
||||
}
|
||||
|
||||
export enum IntegrationMappingBehavior {
|
||||
ONE_TO_ONE = "one-to-one",
|
||||
MANY_TO_ONE = "many-to-one"
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ export const decryptSecrets = (
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const fetchProjectEncryptedSecrets = async ({
|
||||
export const fetchProjectEncryptedSecrets = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
|
@@ -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 = ({
|
||||
|
@@ -15,6 +15,7 @@ import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -70,6 +71,17 @@ const awsRegions = [
|
||||
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
|
||||
];
|
||||
|
||||
const mappingBehaviors = [
|
||||
{
|
||||
label: "Many to One (All Infisical secrets will be mapped to a single AWS secret)",
|
||||
value: IntegrationMappingBehavior.MANY_TO_ONE
|
||||
},
|
||||
{
|
||||
label: "One to One - (Each Infisical secret will be mapped to its own AWS secret)",
|
||||
value: IntegrationMappingBehavior.ONE_TO_ONE
|
||||
}
|
||||
];
|
||||
|
||||
export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
@@ -84,6 +96,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
|
||||
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
|
||||
IntegrationMappingBehavior.MANY_TO_ONE
|
||||
);
|
||||
const [targetSecretName, setTargetSecretName] = useState("");
|
||||
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
@@ -116,7 +131,14 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (targetSecretName.trim() === "") {
|
||||
if (!selectedMappingBehavior) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
|
||||
targetSecretName.trim() === ""
|
||||
) {
|
||||
setTargetSecretName("Secret name cannot be blank");
|
||||
return;
|
||||
}
|
||||
@@ -143,7 +165,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
]
|
||||
}
|
||||
: {}),
|
||||
...(kmsKeyId && { kmsKeyId })
|
||||
...(kmsKeyId && { kmsKeyId }),
|
||||
mappingBehavior: selectedMappingBehavior
|
||||
}
|
||||
});
|
||||
|
||||
@@ -248,19 +271,40 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="AWS SM Secret Name"
|
||||
errorText={targetSecretNameErrorText}
|
||||
isError={targetSecretNameErrorText !== "" ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder={`${workspace.name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
|
||||
value={targetSecretName}
|
||||
onChange={(e) => setTargetSecretName(e.target.value)}
|
||||
/>
|
||||
<FormControl label="Mapping Behavior">
|
||||
<Select
|
||||
value={selectedMappingBehavior}
|
||||
onValueChange={(val) => {
|
||||
setSelectedMappingBehavior(val as IntegrationMappingBehavior);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 text-left"
|
||||
>
|
||||
{mappingBehaviors.map((option) => (
|
||||
<SelectItem
|
||||
value={option.value}
|
||||
className="text-left"
|
||||
key={`aws-environment-${option.value}`}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
|
||||
<FormControl
|
||||
label="AWS SM Secret Name"
|
||||
errorText={targetSecretNameErrorText}
|
||||
isError={targetSecretNameErrorText !== "" ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder={`${workspace.name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
|
||||
value={targetSecretName}
|
||||
onChange={(e) => setTargetSecretName(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Options}>
|
||||
|
@@ -121,6 +121,14 @@ export default function LoginPage() {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
// Case: User has no organizations.
|
||||
// This can happen if the user was previously a member, but the organization was deleted or the user was removed.
|
||||
useEffect(() => {
|
||||
if (!organizations.isLoading && organizations.data?.length === 0) {
|
||||
router.push("/org/none");
|
||||
}
|
||||
}, [organizations.isLoading, organizations.data]);
|
||||
|
||||
if (userLoading || !user) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
@@ -1,21 +1,27 @@
|
||||
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 { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
type Props = {
|
||||
@@ -39,6 +45,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 +82,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"
|
||||
@@ -124,29 +132,35 @@ export const IntegrationsSection = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(integration.integration === "aws-parameter-store" && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"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">
|
||||
{(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(integration.integration === "aws-parameter-store" && `${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") &&
|
||||
`${integration.owner}/${integration.app}`) ||
|
||||
integration.app}
|
||||
{!(
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) && (
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(integration.integration === "aws-parameter-store" && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<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.scope?.startsWith("github-") &&
|
||||
`${integration.owner}/${integration.app}`) ||
|
||||
integration.app}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "vercel" ||
|
||||
integration.integration === "netlify" ||
|
||||
integration.integration === "railway" ||
|
||||
@@ -187,13 +201,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 +288,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 ||
|
||||
|
@@ -15,6 +15,7 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
|
||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
@@ -32,7 +33,8 @@ 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 }
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
|
||||
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }
|
||||
];
|
||||
|
||||
const schema = yup
|
||||
@@ -97,6 +99,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.AZURE_AUTH: {
|
||||
return (
|
||||
<IdentityAzureAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
|
@@ -0,0 +1,350 @@
|
||||
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 } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAzureAuth,
|
||||
useGetIdentityAzureAuth,
|
||||
useUpdateIdentityAzureAuth
|
||||
} 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";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
tenantId: z.string(),
|
||||
resource: z.string(),
|
||||
allowedServicePrincipalIds: 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 IdentityAzureAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||
|
||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "");
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
tenantId: "",
|
||||
resource: "https://management.azure.com/",
|
||||
allowedServicePrincipalIds: "",
|
||||
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({
|
||||
tenantId: data.tenantId,
|
||||
resource: data.resource,
|
||||
allowedServicePrincipalIds: data.allowedServicePrincipalIds,
|
||||
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({
|
||||
tenantId: "",
|
||||
resource: "https://management.azure.com/",
|
||||
allowedServicePrincipalIds: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityAuthMethodData) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
tenantId,
|
||||
resource,
|
||||
allowedServicePrincipalIds,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
tenantId: tenantId || "",
|
||||
resource: resource || "",
|
||||
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
||||
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="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resource"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Resource / Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="https://management.azure.com/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedServicePrincipalIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Principal IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||
</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>
|
||||
);
|
||||
};
|
@@ -1,9 +1,22 @@
|
||||
import { faKey, faLock, faPencil, faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCopy,
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faLock,
|
||||
faPencil,
|
||||
faServer,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Select,
|
||||
@@ -80,7 +93,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>ID</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Auth Method</Th>
|
||||
<Th className="w-5" />
|
||||
@@ -95,7 +107,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`identity-${id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{id}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
@@ -127,7 +138,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
{authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
||||
<Tooltip content="Manage client ID/secrets">
|
||||
<IconButton
|
||||
@@ -141,7 +152,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
// isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</IconButton>
|
||||
@@ -165,7 +175,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLock} />
|
||||
@@ -173,54 +182,78 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("identity", {
|
||||
identityId: id,
|
||||
name,
|
||||
role,
|
||||
customRole
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
if (!isAllowed) return;
|
||||
handlePopUpOpen("identity", {
|
||||
identityId: id,
|
||||
name,
|
||||
role,
|
||||
customRole
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faPencil} />}
|
||||
>
|
||||
Update identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isAllowed) return;
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faXmark} />}
|
||||
>
|
||||
Delete identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
navigator.clipboard.writeText(id);
|
||||
createNotification({
|
||||
text: "Copied identity internal ID to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
Copy Identity ID
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
@@ -43,6 +43,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { removeTrailingSlash } from "@app/helpers/string";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
TProjectUserPrivilege,
|
||||
@@ -104,7 +105,9 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
? {
|
||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
||||
// secret path will be inside $glob operator
|
||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob
|
||||
? removeTrailingSlash(privilege.permissions?.[0]?.conditions?.secretPath?.$glob)
|
||||
: "",
|
||||
read: privilege.permissions?.some(({ action }) =>
|
||||
action.includes(ProjectPermissionActions.Read)
|
||||
),
|
||||
@@ -183,7 +186,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
];
|
||||
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
||||
if (data.secretPath) {
|
||||
conditions.secretPath = { $glob: data.secretPath };
|
||||
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
|
||||
}
|
||||
await updateUserPrivilege.mutateAsync({
|
||||
privilegeId: privilege.id,
|
||||
|
@@ -23,6 +23,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { decryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -43,8 +44,9 @@ import {
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import { interpolateSecrets } from "@app/helpers/secret";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||
import { useCreateFolder, useDeleteSecretBatch, useGetUserWsKey } from "@app/hooks/api";
|
||||
import { DecryptedSecret, TImportedSecrets, WsTag } from "@app/hooks/api/types";
|
||||
import { debounce } from "@app/lib/fn/debounce";
|
||||
|
||||
@@ -112,6 +114,7 @@ export const ActionBar = ({
|
||||
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
|
||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||
@@ -144,30 +147,59 @@ export const ActionBar = ({
|
||||
const handleSecretDownload = async () => {
|
||||
const secPriority: Record<string, boolean> = {};
|
||||
const downloadedSecrets: Array<{ key: string; value: string; comment?: string }> = [];
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const workspaceKey = decryptAssymmetric({
|
||||
ciphertext: decryptFileKey!.encryptedKey,
|
||||
nonce: decryptFileKey!.nonce,
|
||||
publicKey: decryptFileKey!.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const expandSecrets = interpolateSecrets({
|
||||
projectId: workspaceId,
|
||||
secretEncKey: workspaceKey
|
||||
});
|
||||
|
||||
const secretRecord: Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
|
||||
> = {};
|
||||
|
||||
// load up secrets in dashboard
|
||||
secrets?.forEach(({ key, value, comment }) => {
|
||||
secrets?.forEach(({ key, value, valueOverride, comment }) => {
|
||||
secPriority[key] = true;
|
||||
downloadedSecrets.push({ key, value, comment });
|
||||
downloadedSecrets.push({ key, value: valueOverride || value, comment });
|
||||
});
|
||||
// now load imported secrets with secPriority
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
importedSecrets[i].secrets.forEach(({ key, value, comment }) => {
|
||||
importedSecrets[i].secrets.forEach(({ key, value, valueOverride, comment }) => {
|
||||
if (secPriority?.[key]) return;
|
||||
downloadedSecrets.unshift({ key, value, comment });
|
||||
downloadedSecrets.unshift({ key, value: valueOverride || value, comment });
|
||||
secPriority[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
downloadedSecrets.forEach((secret) => {
|
||||
secretRecord[secret.key] = {
|
||||
value: secret.value,
|
||||
comment: secret.comment
|
||||
};
|
||||
});
|
||||
|
||||
await expandSecrets(secretRecord);
|
||||
|
||||
const file = downloadedSecrets
|
||||
.sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase()))
|
||||
.reduce(
|
||||
(prev, { key, value, comment }, index) =>
|
||||
(prev, { key, comment }, index) =>
|
||||
prev +
|
||||
(comment
|
||||
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${value}\n`
|
||||
: `${key}=${value}\n`),
|
||||
? `${index === 0 ? "#" : "\n#"} ${comment}\n${key}=${secretRecord[key].value}\n`
|
||||
: `${key}=${secretRecord[key].value}\n`),
|
||||
""
|
||||
);
|
||||
|
||||
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
|
||||
FileSaver.saveAs(blob, `${environment}.env`);
|
||||
};
|
||||
|
@@ -152,7 +152,7 @@ export const SecretDropzone = ({
|
||||
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setDragActive.off();
|
||||
parseFile(e.dataTransfer.files[0]);
|
||||
parseFile(e.dataTransfer.files[0], e.dataTransfer.files[0].type === "application/json");
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
Reference in New Issue
Block a user