mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-31 15:32:32 +00:00
Compare commits
34 Commits
doc/update
...
misc/impro
Author | SHA1 | Date | |
---|---|---|---|
|
8964218516 | ||
|
0e16ea7703 | ||
|
b9782c1a85 | ||
|
a0be2985dd | ||
|
86d16c5b9f | ||
|
c1c1471439 | ||
|
527e1d6b79 | ||
|
3e32915a82 | ||
|
4faa9ced04 | ||
|
b6ff07b605 | ||
|
1753cd76be | ||
|
f75fc54e10 | ||
|
c782df1176 | ||
|
e9c5b7f846 | ||
|
008b37c0f4 | ||
|
c9b234dbea | ||
|
049df6abec | ||
|
e7c5645aa9 | ||
|
4e06fa3a0c | ||
|
0f827fc31a | ||
|
7189544705 | ||
|
a724ab101c | ||
|
dea67e3cb0 | ||
|
ce66cccd8b | ||
|
91eda2419a | ||
|
b350eef2b9 | ||
|
85725215f2 | ||
|
76c9d642a9 | ||
|
3ed5dd6109 | ||
|
08e7815ec1 | ||
|
04d961b832 | ||
|
9c0a1b7089 | ||
|
9352e8bca0 | ||
|
265932df20 |
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,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";
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -79,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",
|
||||
@@ -572,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: {
|
||||
@@ -839,6 +885,10 @@ export type Event =
|
||||
| AddIdentityAwsAuthEvent
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| LoginIdentityAzureAuthEvent
|
||||
| AddIdentityAzureAuthEvent
|
||||
| UpdateIdentityAzureAuthEvent
|
||||
| GetIdentityAzureAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
|
@@ -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" });
|
||||
}
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -80,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";
|
||||
@@ -115,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";
|
||||
@@ -212,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);
|
||||
@@ -742,6 +745,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityAzureAuthService = identityAzureAuthServiceFactory({
|
||||
identityAzureAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
@@ -769,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", {
|
||||
@@ -813,6 +830,7 @@ export const registerRoutes = async (
|
||||
identityKubernetesAuth: identityKubernetesAuthService,
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
secretApprovalPolicy: sapService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
|
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" }
|
||||
);
|
||||
|
@@ -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(", ");
|
||||
});
|
@@ -587,7 +587,10 @@ const syncSecretsAWSSecretManager = async ({
|
||||
}
|
||||
});
|
||||
|
||||
const processAwsSecret = async (secretId: string, keyValuePairs: Record<string, string | null | undefined>) => {
|
||||
const processAwsSecret = async (
|
||||
secretId: string,
|
||||
secretValue: Record<string, string | null | undefined> | string
|
||||
) => {
|
||||
try {
|
||||
const awsSecretManagerSecret = await secretsManager.send(
|
||||
new GetSecretValueCommand({
|
||||
@@ -595,17 +598,20 @@ const syncSecretsAWSSecretManager = async ({
|
||||
})
|
||||
);
|
||||
|
||||
let awsSecretManagerSecretObj: { [key: string]: AWS.SecretsManager } = {};
|
||||
|
||||
let secretToCompare;
|
||||
if (awsSecretManagerSecret?.SecretString) {
|
||||
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
if (typeof secretValue === "string") {
|
||||
secretToCompare = awsSecretManagerSecret.SecretString;
|
||||
} else {
|
||||
secretToCompare = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEqual(awsSecretManagerSecretObj, keyValuePairs)) {
|
||||
if (!isEqual(secretToCompare, secretValue)) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
SecretId: secretId,
|
||||
SecretString: JSON.stringify(keyValuePairs)
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -695,7 +701,7 @@ const syncSecretsAWSSecretManager = async ({
|
||||
await secretsManager.send(
|
||||
new CreateSecretCommand({
|
||||
Name: secretId,
|
||||
SecretString: JSON.stringify(keyValuePairs),
|
||||
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 }))
|
||||
@@ -708,9 +714,7 @@ const syncSecretsAWSSecretManager = async ({
|
||||
|
||||
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
|
||||
for await (const [key, value] of Object.entries(secrets)) {
|
||||
await processAwsSecret(key, {
|
||||
[key]: value.value
|
||||
});
|
||||
await processAwsSecret(key, value.value);
|
||||
}
|
||||
} else {
|
||||
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
|
||||
@@ -2692,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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -66,6 +66,10 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath })
|
||||
);
|
||||
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
@@ -123,6 +127,11 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@@ -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.
After Width: | Height: | Size: 513 KiB |
@@ -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",
|
||||
{
|
||||
|
@@ -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,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -13,6 +14,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
|
||||
@@ -94,12 +97,35 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
setSelectedAWSRegion(awsRegions[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
||||
useGetIntegrationAuthAwsKmsKeys({
|
||||
@@ -218,7 +244,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`flyio-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -13,6 +14,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
@@ -104,6 +107,18 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
// const [path, setPath] = useState('');
|
||||
// const [pathErrorText, setPathErrorText] = useState('');
|
||||
@@ -118,11 +133,20 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
setSelectedAWSRegion(awsRegions[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
// const isValidAWSPath = (path: string) => {
|
||||
// const pattern = /^\/[\w./]+\/$/;
|
||||
@@ -238,7 +262,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`flyio-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -32,12 +35,33 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState("");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
@@ -79,7 +103,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -42,11 +45,33 @@ export default function BitBucketCreateIntegrationPage() {
|
||||
workspaceSlug: targetEnvironmentId
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -116,7 +141,7 @@ export default function BitBucketCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -62,11 +65,33 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
accountId: targetAppId
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -176,7 +201,7 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -50,12 +53,33 @@ export default function CircleCICreateIntegrationPage() {
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -137,7 +161,7 @@ export default function CircleCICreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -36,11 +39,33 @@ export default function Cloud66CreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -91,7 +116,7 @@ export default function Cloud66CreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
|
||||
@@ -37,11 +39,33 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -112,7 +136,7 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
|
||||
@@ -32,11 +34,33 @@ export default function CloudflareWorkersIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -103,7 +127,7 @@ export default function CloudflareWorkersIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -36,11 +39,33 @@ export default function CodefreshCreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -91,7 +116,7 @@ export default function CodefreshCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -36,11 +39,33 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -91,7 +116,7 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -52,11 +55,33 @@ export default function FlyioCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: handle case where apps can be empty
|
||||
@@ -136,7 +161,7 @@ export default function FlyioCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@@ -11,7 +12,9 @@ import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
@@ -100,11 +103,33 @@ export default function GCPSecretManagerCreateIntegrationPage() {
|
||||
setValue("labelValue", "");
|
||||
}, [shouldLabel]);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setValue("selectedSourceEnvironment", availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -237,7 +262,7 @@ export default function GCPSecretManagerCreateIntegrationPage() {
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowUpRightFromSquare,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
useCreateIntegration,
|
||||
useGetIntegrationAuthApps,
|
||||
@@ -98,7 +100,6 @@ type FormData = yup.InferType<typeof schema>;
|
||||
export default function GitHubCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
|
||||
const integrationAuthId =
|
||||
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
|
||||
@@ -138,11 +139,33 @@ export default function GitHubCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setValue("selectedSourceEnvironment", availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) {
|
||||
@@ -303,7 +326,7 @@ export default function GitHubCreateIntegrationPage() {
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@@ -11,7 +12,9 @@ import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
@@ -100,11 +103,33 @@ export default function GitLabCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setValue("selectedSourceEnvironment", availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -247,7 +272,7 @@ export default function GitLabCreateIntegrationPage() {
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -47,6 +49,19 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
const isValidVaultPath = (vaultPath: string) => {
|
||||
return !(vaultPath.length === 0 || vaultPath.startsWith("/") || vaultPath.endsWith("/"));
|
||||
};
|
||||
@@ -129,7 +144,7 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`vault-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@@ -11,6 +13,7 @@ import * as yup from "yup";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
@@ -72,6 +75,19 @@ export default function HasuraCloudCreateIntegrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
return integrationAuth && workspace && integrationAuthApps ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<Head>
|
||||
@@ -125,7 +141,7 @@ export default function HasuraCloudCreateIntegrationPage() {
|
||||
field.onChange(val);
|
||||
}}
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -17,7 +18,9 @@ import queryString from "query-string";
|
||||
// import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
// import { RadioGroup } from "@app/components/v2/RadioGroup";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
@@ -92,11 +95,33 @@ export default function HerokuCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setValue("selectedSourceEnvironment", availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (integrationAuthPipelineCouplings) {
|
||||
@@ -255,7 +280,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -36,11 +39,33 @@ export default function LaravelForgeCreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -92,7 +117,7 @@ export default function LaravelForgeCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -44,12 +47,34 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
setTargetEnvironment(netlifyEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -101,7 +126,7 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -43,11 +46,33 @@ export default function NorthflankCreateIntegrationPage() {
|
||||
appId: targetAppId
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -113,7 +138,7 @@ export default function NorthflankCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
TabPanel,
|
||||
Tabs
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthQoveryEnvironments,
|
||||
@@ -94,11 +97,33 @@ export default function QoveryCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -239,7 +264,7 @@ export default function QoveryCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -48,11 +51,33 @@ export default function RailwayCreateIntegrationPage() {
|
||||
appId: targetAppId
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -133,7 +158,7 @@ export default function RailwayCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -15,7 +16,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import queryString from "query-string";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -69,11 +72,33 @@ export default function RenderCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setValue("selectedSourceEnvironment", availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -167,7 +192,7 @@ export default function RenderCreateIntegrationPage() {
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -37,11 +40,33 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -92,7 +117,7 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -51,11 +54,33 @@ export default function TeamCityCreateIntegrationPage() {
|
||||
appId: targetAppId
|
||||
});
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -149,7 +174,7 @@ export default function TeamCityCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
@@ -30,8 +33,14 @@ const initialSyncBehaviors = [
|
||||
label: "No Import - Overwrite all values in Terraform Cloud",
|
||||
value: IntegrationSyncBehavior.OVERWRITE_TARGET
|
||||
},
|
||||
{ label: "Import non-sensitive - Prefer values from Terraform Cloud", value: IntegrationSyncBehavior.PREFER_TARGET },
|
||||
{ label: "Import non-sensitive - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
|
||||
{
|
||||
label: "Import non-sensitive - Prefer values from Terraform Cloud",
|
||||
value: IntegrationSyncBehavior.PREFER_TARGET
|
||||
},
|
||||
{
|
||||
label: "Import non-sensitive - Prefer values from Infisical",
|
||||
value: IntegrationSyncBehavior.PREFER_SOURCE
|
||||
}
|
||||
];
|
||||
|
||||
const variableTypes = [{ name: "env" }, { name: "terraform" }];
|
||||
@@ -54,14 +63,36 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
const [variableType, setVariableType] = useState("");
|
||||
const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source")
|
||||
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source");
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
setVariableType(variableTypes[0].name);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -153,7 +184,7 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
@@ -212,7 +243,11 @@ export default function TerraformCloudCreateIntegrationPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Initial Sync Behavior" className="px-6">
|
||||
<Select value={initialSyncBehavior} onValueChange={(e) => setInitialSyncBehavior(e)} className="w-full border border-mineshaft-600">
|
||||
<Select
|
||||
value={initialSyncBehavior}
|
||||
onValueChange={(e) => setInitialSyncBehavior(e)}
|
||||
className="w-full border border-mineshaft-600"
|
||||
>
|
||||
{initialSyncBehaviors.map((b) => {
|
||||
return (
|
||||
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -36,11 +39,33 @@ export default function TravisCICreateIntegrationPage() {
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -91,7 +116,7 @@ export default function TravisCICreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -63,11 +66,33 @@ export default function VercelCreateIntegrationPage() {
|
||||
|
||||
const filteredBranches = branches?.filter((branchName) => branchName !== "main").concat();
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -160,7 +185,7 @@ export default function VercelCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { subject } from "@casl/ability";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
@@ -37,11 +40,33 @@ export default function WindmillCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const availableEnvironments = useMemo(
|
||||
() =>
|
||||
workspace?.environments.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath: "/" })
|
||||
)
|
||||
),
|
||||
[workspace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
if (workspace && availableEnvironments) {
|
||||
if (!availableEnvironments.length) {
|
||||
createNotification({
|
||||
title: "Insufficient Access",
|
||||
text: "You do not have read access to any environment",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedSourceEnvironment(availableEnvironments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
}, [workspace, availableEnvironments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
@@ -92,7 +117,7 @@ export default function WindmillCreateIntegrationPage() {
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
{availableEnvironments?.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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