Compare commits

...

34 Commits

Author SHA1 Message Date
Sheen Capadngan
8964218516 misc: added error prompt for insufficent access 2024-05-27 18:56:07 +08:00
Sheen Capadngan
0e16ea7703 misc: improved integration rbac control 2024-05-27 17:51:37 +08:00
BlackMagiq
b9782c1a85 Merge pull request #1833 from Infisical/azure-auth
Azure Native Authentication Method
2024-05-27 00:37:09 -07:00
Vladyslav Matsiiako
a0be2985dd Added money page 2024-05-26 22:19:42 -07:00
vmatsiiako
86d16c5b9f Merge pull request #1877 from Infisical/sheensantoscapadngan-patch-1
Update onboarding.mdx
2024-05-26 22:05:12 -07:00
Sheen Capadngan
c1c1471439 Update onboarding.mdx 2024-05-27 12:28:14 +08:00
vmatsiiako
527e1d6b79 Merge pull request #1876 from Infisical/aws-integration-patch
added company handbook
2024-05-26 16:16:18 -07:00
Vladyslav Matsiiako
3e32915a82 added company handbook 2024-05-26 16:14:37 -07:00
Maidul Islam
4faa9ced04 Merge pull request #1837 from akhilmhdh/feat/resource-daily-prune
Daily cron for cleaning up expired tokens from db
2024-05-24 12:53:26 -04:00
Maidul Islam
b6ff07b605 revert repete cron 2024-05-24 12:45:19 -04:00
Maidul Islam
1753cd76be update delete access token logic 2024-05-24 12:43:14 -04:00
Sheen Capadngan
f75fc54e10 Merge pull request #1870 from Infisical/doc/updated-gcp-secrets-manager-doc-reminder
doc: added reminder for GCP oauth user permissions
2024-05-25 00:00:15 +08:00
Sheen Capadngan
c782df1176 Merge pull request #1872 from Infisical/fix/resolve-cloudflare-pages-integration
fix: resolved cloudflare pages integration
2024-05-24 23:50:57 +08:00
Sheen Capadngan
e9c5b7f846 Merge pull request #1871 from Infisical/fix/address-json-drop-behavior
fix: address json drag behavior
2024-05-24 21:46:33 +08:00
Sheen Capadngan
008b37c0f4 fix: resolved cloudflare pages integration 2024-05-24 19:45:20 +08:00
Sheen Capadngan
c9b234dbea fix: address json drag behavior 2024-05-24 17:42:38 +08:00
Sheen Capadngan
049df6abec Merge pull request #1869 from Infisical/misc/made-aws-sm-mapping-plaintext-one-to-one
misc: made aws sm mapping one to one plaintext
2024-05-24 02:15:04 +08:00
Sheen Capadngan
e7c5645aa9 misc: made aws sm mapping one to one plaintext 2024-05-24 00:35:55 +08:00
Tuan Dang
4e06fa3a0c Move azure auth migration file to front 2024-05-20 21:15:42 -07:00
Tuan Dang
0f827fc31a Merge remote-tracking branch 'origin' into azure-auth 2024-05-20 21:14:30 -07:00
Tuan Dang
7189544705 Merge branch 'azure-auth' of https://github.com/Infisical/infisical into azure-auth 2024-05-20 08:41:19 -07:00
Tuan Dang
a724ab101c Fix identities docs markings 2024-05-20 08:39:10 -07:00
Tuan Dang
dea67e3cb0 Update azure auth based on review 2024-05-19 22:24:26 -07:00
Tuan Dang
ce66cccd8b Fix merge conflicts 2024-05-19 22:19:49 -07:00
Daniel Hougaard
91eda2419a Update machine-identities.mdx 2024-05-20 00:32:10 +02:00
Tuan Dang
b350eef2b9 Add access token trusted ip support for azure auth 2024-05-17 15:43:12 -07:00
Tuan Dang
85725215f2 Merge remote-tracking branch 'origin' into azure-auth 2024-05-17 15:41:58 -07:00
=
76c9d642a9 fix: resolved identity check failing due to comma seperated header in ip 2024-05-16 15:46:19 +05:30
=
3ed5dd6109 feat: removed audit log queue and switched to resource clean up queue 2024-05-16 15:46:19 +05:30
=
08e7815ec1 feat: added increment and decrement ops in update knex orm 2024-05-16 15:46:19 +05:30
=
04d961b832 feat: added dal to remove expired token for queue and fixed token validation check missing num uses increment and maxTTL failed check 2024-05-16 15:46:18 +05:30
Tuan Dang
9c0a1b7089 Merge remote-tracking branch 'origin' into azure-auth 2024-05-15 23:23:50 -07:00
Tuan Dang
9352e8bca0 Add docs for Azure auth 2024-05-15 23:13:19 -07:00
Tuan Dang
265932df20 Finish preliminary azure auth method 2024-05-15 16:30:42 -07:00
73 changed files with 2790 additions and 278 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);
}

View 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>>;

View File

@@ -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";

View File

@@ -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"
}

View File

@@ -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
};
};

View File

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

View File

@@ -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" });
}

View File

@@ -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) => {

View File

@@ -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;
}

View File

@@ -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,

View 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 };
}
});
};

View File

@@ -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()

View File

@@ -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" }
);

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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
};
};

View File

@@ -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;
};
};

View File

@@ -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(", ");
});

View File

@@ -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)
}
}
};

View File

@@ -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" });

View File

@@ -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
};
};

View 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&apos;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.

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

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

View File

@@ -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"
]
}
],

View File

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

View 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**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Azure Auth**.
![identities create azure auth method](/images/platform/identities/identities-org-create-azure-auth-method.png)
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.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you 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>

View File

@@ -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.
![organization identities](/images/platform/organization/organization-machine-identities.png)
![Organization Identities](/images/platform/organization/organization-machine-identities.png)
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

View File

@@ -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",
{

View File

@@ -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"
};

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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 }) => {

View File

@@ -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),

View File

@@ -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;

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}>

View File

@@ -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}`}

View File

@@ -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}`}

View File

@@ -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}`}

View File

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

View File

@@ -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>
);
};

View File

@@ -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>) => {