mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-31 15:32:32 +00:00
Compare commits
56 Commits
sid/k8s-op
...
edit-acces
Author | SHA1 | Date | |
---|---|---|---|
|
4da24bfa39 | ||
|
dc48281e6a | ||
|
b3002d784e | ||
|
c782493704 | ||
|
5c632db282 | ||
|
817daecc6c | ||
|
461deef0d5 | ||
|
7748e03612 | ||
|
2389c64e69 | ||
|
de5ad47f77 | ||
|
e0161cd06f | ||
|
7c12fa3a4c | ||
|
0af53e82da | ||
|
f0c080187e | ||
|
47118bcf19 | ||
|
bb1975491f | ||
|
28cc919ff7 | ||
|
5c21ac3182 | ||
|
06de9d06c9 | ||
|
3cceec86c8 | ||
|
ff043f990f | ||
|
9e177c1e45 | ||
|
5aeb823c9e | ||
|
ef6f79f7a6 | ||
|
43752e1888 | ||
|
d587e779f5 | ||
|
bd72129d8c | ||
|
bf10b2f58a | ||
|
d24f5a57a8 | ||
|
166104e523 | ||
|
a7847f177c | ||
|
48e5f550e9 | ||
|
4a4a7fd325 | ||
|
91b8ed8015 | ||
|
6cf978b593 | ||
|
68fbb399fc | ||
|
97366f6e95 | ||
|
c83d4af7a3 | ||
|
c35c937c63 | ||
|
b10752acb5 | ||
|
eb9b75d930 | ||
|
f60dd528e8 | ||
|
09db98db50 | ||
|
a37f1eb1f8 | ||
|
2113abcfdc | ||
|
ea2707651c | ||
|
b986ff9a21 | ||
|
106833328b | ||
|
c2db2a0bc7 | ||
|
0473fb0ddb | ||
|
f77a53bd8e | ||
|
4bd61e5607 | ||
|
aa4dbfa073 | ||
|
18881749fd | ||
|
55607a4886 | ||
|
385c75c543 |
1
backend/src/@types/fastify.d.ts
vendored
1
backend/src/@types/fastify.d.ts
vendored
@@ -148,6 +148,7 @@ declare module "fastify" {
|
||||
interface Session {
|
||||
callbackPort: string;
|
||||
isAdminLogin: boolean;
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
|
@@ -84,6 +84,9 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
t.index("expiresAt");
|
||||
t.index("orgId");
|
||||
t.index("projectId");
|
||||
t.index("eventType");
|
||||
t.index("userAgentType");
|
||||
t.index("actor");
|
||||
});
|
||||
|
||||
console.log("Adding GIN indices...");
|
||||
@@ -119,8 +122,8 @@ const up = async (knex: Knex): Promise<void> => {
|
||||
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
|
||||
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
|
||||
|
||||
// create partitions 4 years ahead
|
||||
const partitionMonths = 4 * 12;
|
||||
// create partitions 20 years ahead
|
||||
const partitionMonths = 20 * 12;
|
||||
const partitionPromises: Promise<void>[] = [];
|
||||
for (let x = 1; x <= partitionMonths; x += 1) {
|
||||
partitionPromises.push(
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
|
||||
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (!hasGoogleSsoAuthEnforcedColumn)
|
||||
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
|
||||
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
|
||||
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
|
||||
});
|
||||
}
|
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -14,7 +14,7 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||
|
||||
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
||||
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
|
||||
pruneAuditLog: () => Promise<void>;
|
||||
find: (
|
||||
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string | undefined;
|
||||
@@ -41,6 +41,10 @@ type TFindQuery = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
@@ -151,20 +155,20 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// delete all audit log that have expired
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
||||
const runPrune = async (dbClient: knex.Knex) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async () => {
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
|
||||
const findExpiredLogSubQuery = trx(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
@@ -172,34 +176,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await dbClient(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
const results = await trx(TableName.AuditLog).whereIn("id", findExpiredLogSubQuery).del().returning("id");
|
||||
|
||||
if (tx) {
|
||||
await runPrune(tx);
|
||||
} else {
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
await runPrune(trx);
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
deletedAuditLogIds = [];
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
|
||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||
|
@@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
enforceGoogleSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
|
@@ -47,6 +47,7 @@ export type TFeatureSet = {
|
||||
auditLogStreamLimit: 3;
|
||||
githubOrgSync: false;
|
||||
samlSSO: false;
|
||||
enforceGoogleSSO: false;
|
||||
hsm: false;
|
||||
oidcSSO: false;
|
||||
secretAccessInsights: false;
|
||||
|
@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
|
||||
projectFavorites?: string[] | null | undefined;
|
||||
customRoleSlug?: string | null | undefined;
|
||||
orgAuthEnforced?: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
} & {
|
||||
groups: {
|
||||
id: string;
|
||||
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
|
||||
}[];
|
||||
orgId: string;
|
||||
orgAuthEnforced: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
orgRole: OrgMembershipRole;
|
||||
userId: string;
|
||||
projectId: string;
|
||||
@@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("groupId").withSchema("userGroups"),
|
||||
db.ref("groupOrgId").withSchema("userGroups"),
|
||||
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||
bypassOrgAuthEnabled: z.boolean(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean()
|
||||
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
orgId,
|
||||
username,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole: orgRole as OrgMembershipRole,
|
||||
userId,
|
||||
projectId,
|
||||
|
@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
function validateOrgSSO(
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
|
||||
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||
orgRole: OrgMembershipRole
|
||||
) {
|
||||
@@ -128,10 +129,16 @@ function validateOrgSSO(
|
||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||
}
|
||||
|
||||
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// case: google sso is enforced, but the actor is not using google sso
|
||||
if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) {
|
||||
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||
}
|
||||
|
||||
// case: SAML SSO is enforced, but the actor is not using SAML SSO
|
||||
if (
|
||||
isOrgSsoEnforced &&
|
||||
actorAuthMethod !== null &&
|
||||
|
@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
membership.orgAuthEnforced,
|
||||
membership.orgGoogleSsoAuthEnforced,
|
||||
membership.bypassOrgAuthEnabled,
|
||||
membership.role as OrgMembershipRole
|
||||
);
|
||||
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
userProjectPermission.orgAuthEnforced,
|
||||
userProjectPermission.orgGoogleSsoAuthEnforced,
|
||||
userProjectPermission.bypassOrgAuthEnabled,
|
||||
userProjectPermission.orgRole
|
||||
);
|
||||
|
@@ -2491,6 +2491,7 @@ export const SecretSyncs = {
|
||||
},
|
||||
RENDER: {
|
||||
serviceId: "The ID of the Render service to sync secrets to.",
|
||||
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
|
||||
scope: "The Render scope that secrets should be synced to.",
|
||||
type: "The Render resource type to sync secrets to."
|
||||
},
|
||||
|
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Safely retrieves a value from a nested object using dot notation path
|
||||
*/
|
||||
export const getStringValueByDot = (
|
||||
export const getValueByDot = (
|
||||
obj: Record<string, unknown> | null | undefined,
|
||||
path: string,
|
||||
defaultValue?: string
|
||||
): string | undefined => {
|
||||
defaultValue?: string | number | boolean
|
||||
): string | number | boolean | undefined => {
|
||||
// Handle null or undefined input
|
||||
if (!obj) {
|
||||
return defaultValue;
|
||||
@@ -26,7 +26,7 @@ export const getStringValueByDot = (
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
if (typeof current !== "string") {
|
||||
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
|
||||
return services;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/environment-groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -67,7 +67,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: () => ({ message: "Authenticated" as const })
|
||||
});
|
||||
|
||||
|
@@ -478,4 +478,30 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
return { identityMemberships };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/details",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
identityDetails: z.object({
|
||||
organization: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN], { requireOrg: false }),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.findIdentityOrganization(req.permission.id);
|
||||
return { identityDetails: { organization } };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
name: GenericResourceNameSchema.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
authEnforced: z.boolean().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().optional(),
|
||||
scimEnabled: z.boolean().optional(),
|
||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||
enforceMfa: z.boolean().optional(),
|
||||
|
@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
try {
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const orgSlug = req.session.get("orgSlug");
|
||||
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
if (!email)
|
||||
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort
|
||||
callbackPort,
|
||||
orgSlug
|
||||
});
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional(),
|
||||
org_slug: z.string().optional(),
|
||||
is_admin_login: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
preValidation: [
|
||||
async (req, res) => {
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query;
|
||||
// ensure fresh session state per login attempt
|
||||
await req.session.regenerate();
|
||||
if (callbackPort) {
|
||||
req.session.set("callbackPort", callbackPort);
|
||||
}
|
||||
if (orgSlug) {
|
||||
req.session.set("orgSlug", orgSlug);
|
||||
}
|
||||
if (isAdminLogin) {
|
||||
req.session.set("isAdminLogin", isAdminLogin);
|
||||
}
|
||||
|
@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { RenderConnectionMethod } from "./render-connection-enums";
|
||||
import {
|
||||
TRawRenderEnvironmentGroup,
|
||||
TRawRenderService,
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService
|
||||
} from "./render-connection-types";
|
||||
|
||||
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderService[] = (
|
||||
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
||||
params: new URLSearchParams({
|
||||
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return services;
|
||||
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listRenderEnvironmentGroups = async (
|
||||
appConnection: TRenderConnection
|
||||
): Promise<TRenderEnvironmentGroup[]> => {
|
||||
const {
|
||||
credentials: { apiKey }
|
||||
} = appConnection;
|
||||
|
||||
const groups: TRenderEnvironmentGroup[] = [];
|
||||
let hasMorePages = true;
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderEnvironmentGroup[] = (
|
||||
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
|
||||
params: new URLSearchParams({
|
||||
...(cursor ? { cursor: String(cursor) } : {}),
|
||||
limit: String(perPage)
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
res.forEach((item) => {
|
||||
groups.push({
|
||||
name: item.envGroup.name,
|
||||
id: item.envGroup.id
|
||||
});
|
||||
});
|
||||
|
||||
if (res.length < perPage) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listRenderServices } from "./render-connection-fns";
|
||||
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
|
||||
import { TRenderConnection } from "./render-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
||||
}
|
||||
};
|
||||
|
||||
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
|
||||
try {
|
||||
const groups = await listRenderEnvironmentGroups(appConnection);
|
||||
|
||||
return groups;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list environment groups for Render connection");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listServices
|
||||
listServices,
|
||||
listEnvironmentGroups
|
||||
};
|
||||
};
|
||||
|
@@ -33,3 +33,16 @@ export type TRawRenderService = {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TRawRenderEnvironmentGroup = {
|
||||
cursor: string;
|
||||
envGroup: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
@@ -448,15 +448,34 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrg = await orgDAL.findById(organizationId);
|
||||
|
||||
if (!hasOrganizationMembership) {
|
||||
if (!selectedOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
||||
|
||||
if (!canBypass) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
||||
error: "GoogleSsoEnforced"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
||||
await orgDAL.updateById(selectedOrg.id, {
|
||||
googleSsoAuthLastUsed: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
@@ -502,7 +521,8 @@ export const authLoginServiceFactory = ({
|
||||
selectedOrg.authEnforced &&
|
||||
selectedOrg.bypassOrgAuthEnabled &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC
|
||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||
decodedToken.authMethod !== AuthMethod.GOOGLE
|
||||
) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
@@ -705,7 +725,7 @@ export const authLoginServiceFactory = ({
|
||||
/*
|
||||
* OAuth2 login for google,github, and other oauth2 provider
|
||||
* */
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => {
|
||||
// akhilmhdh: case sensitive email resolution
|
||||
const usersByUsername = await userDAL.findUserByUsername(email);
|
||||
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||
@@ -759,6 +779,8 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
let orgId = "";
|
||||
let orgName: undefined | string;
|
||||
if (!user) {
|
||||
// Create a new user based on oAuth
|
||||
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
||||
@@ -784,7 +806,6 @@ export const authLoginServiceFactory = ({
|
||||
});
|
||||
|
||||
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||
let orgId = "";
|
||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||
if (!defaultOrg) {
|
||||
throw new BadRequestError({
|
||||
@@ -824,11 +845,39 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!orgId && orgSlug) {
|
||||
const org = await orgDAL.findOrgBySlug(orgSlug);
|
||||
|
||||
if (org) {
|
||||
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
|
||||
const orgMembership = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id,
|
||||
[`${TableName.OrgMembership}.isActive` as "isActive"]: true,
|
||||
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (orgMembership) {
|
||||
orgId = org.id;
|
||||
orgName = org.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUserCompleted = user.isAccepted;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
|
||||
...(orgId && orgSlug && orgName !== undefined
|
||||
? {
|
||||
organizationId: orgId,
|
||||
organizationName: orgName,
|
||||
organizationSlug: orgSlug
|
||||
}
|
||||
: {}),
|
||||
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
isEmailVerified: user.isEmailVerified,
|
||||
|
@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
|
||||
lastName?: string;
|
||||
authMethod: AuthMethod;
|
||||
callbackPort?: string;
|
||||
orgSlug?: string;
|
||||
};
|
||||
|
||||
export type TOauthTokenExchangeDTO = {
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -189,7 +189,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
@@ -198,9 +198,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
||||
});
|
||||
|
@@ -1,7 +1,16 @@
|
||||
import picomatch from "picomatch";
|
||||
|
||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
|
||||
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string | number | boolean, policyValue: string) => {
|
||||
if (typeof fieldValue === "boolean") {
|
||||
return fieldValue === (policyValue === "true");
|
||||
}
|
||||
|
||||
if (typeof fieldValue === "number") {
|
||||
return fieldValue === parseInt(policyValue, 10);
|
||||
}
|
||||
|
||||
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
};
|
||||
|
||||
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
|
||||
if (Array.isArray(fieldValue)) {
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -146,7 +146,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.boundClaims) {
|
||||
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
@@ -167,13 +167,13 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.claimMetadataMapping) {
|
||||
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
||||
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
filteredClaims[permissionKey] = value;
|
||||
filteredClaims[permissionKey] = value.toString();
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -630,6 +630,25 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findIdentityOrganization = async (
|
||||
identityId: string
|
||||
): Promise<{ id: string; name: string; slug: string; role: string }> => {
|
||||
try {
|
||||
const org = await db
|
||||
.replicaNode()(TableName.IdentityOrgMembership)
|
||||
.where({ identityId })
|
||||
.join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||
.select(db.ref("id").withSchema(TableName.Organization).as("id"))
|
||||
.select(db.ref("name").withSchema(TableName.Organization).as("name"))
|
||||
.select(db.ref("slug").withSchema(TableName.Organization).as("slug"))
|
||||
.select(db.ref("role").withSchema(TableName.IdentityOrgMembership).as("role"));
|
||||
|
||||
return org?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find identity organization" });
|
||||
}
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
...orgOrm,
|
||||
findOrgByProjectId,
|
||||
@@ -652,6 +671,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
deleteMembershipsById,
|
||||
updateMembership
|
||||
updateMembership,
|
||||
findIdentityOrganization
|
||||
});
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
authEnforced: true,
|
||||
googleSsoAuthEnforced: true,
|
||||
scimEnabled: true,
|
||||
kmsDefaultKeyId: true,
|
||||
defaultMembershipRole: true,
|
||||
|
@@ -198,6 +198,15 @@ export const orgServiceFactory = ({
|
||||
// Filter out orgs where the membership object is an invitation
|
||||
return orgs.filter((org) => org.userStatus !== "invited");
|
||||
};
|
||||
|
||||
/*
|
||||
* Get all organization an identity is part of
|
||||
* */
|
||||
const findIdentityOrganization = async (identityId: string) => {
|
||||
const org = await orgDAL.findIdentityOrganization(identityId);
|
||||
|
||||
return org;
|
||||
};
|
||||
/*
|
||||
* Get all workspace members
|
||||
* */
|
||||
@@ -355,6 +364,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
@@ -421,6 +431,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced !== undefined) {
|
||||
if (!plan.enforceGoogleSSO) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO."
|
||||
});
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
|
||||
if (authEnforced && googleSsoAuthEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time."
|
||||
});
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
const samlCfg = await samlConfigDAL.findOne({
|
||||
orgId,
|
||||
@@ -451,6 +476,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced) {
|
||||
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentOrg.googleSsoAuthLastUsed) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let defaultMembershipRole: string | undefined;
|
||||
if (defaultMembershipRoleSlug) {
|
||||
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
||||
@@ -465,6 +505,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRole,
|
||||
enforceMfa,
|
||||
@@ -1403,6 +1444,7 @@ export const orgServiceFactory = ({
|
||||
findOrganizationById,
|
||||
findAllOrgMembers,
|
||||
findAllOrganizationOfUser,
|
||||
findIdentityOrganization,
|
||||
inviteUserToOrganization,
|
||||
verifyUserToOrg,
|
||||
updateOrg,
|
||||
|
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
||||
name: string;
|
||||
slug: string;
|
||||
authEnforced: boolean;
|
||||
googleSsoAuthEnforced: boolean;
|
||||
scimEnabled: boolean;
|
||||
defaultMembershipRoleSlug: string;
|
||||
enforceMfa: boolean;
|
||||
|
@@ -177,6 +177,18 @@ export const projectEnvServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const envs = await projectEnvDAL.find({ projectId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (plan.environmentLimit !== null && envs.length > plan.environmentLimit) {
|
||||
// case: limit imposed on number of environments allowed
|
||||
// case: number of environments used exceeds the number of environments allowed
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to update environment due to environment limit exceeded. To update an environment, please upgrade your plan or remove unused environments."
|
||||
});
|
||||
}
|
||||
|
||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||
if (position) {
|
||||
const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import AWS, { AWSError } from "aws-sdk";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
@@ -34,18 +35,51 @@ const sleep = async () =>
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreRecord> => {
|
||||
const getFullPath = ({ path, keySchema, environment }: { path: string; keySchema?: string; environment: string }) => {
|
||||
if (!keySchema || !keySchema.includes("/")) return path;
|
||||
|
||||
if (keySchema.startsWith("/")) {
|
||||
throw new SecretSyncError({ message: `Key schema cannot contain leading '/'`, shouldRetry: false });
|
||||
}
|
||||
|
||||
const keySchemaSegments = handlebars
|
||||
.compile(keySchema)({
|
||||
environment,
|
||||
secretKey: "{{secretKey}}"
|
||||
})
|
||||
.split("/");
|
||||
|
||||
const pathSegments = keySchemaSegments.slice(0, keySchemaSegments.length - 1);
|
||||
|
||||
if (pathSegments.some((segment) => segment.includes("{{secretKey}}"))) {
|
||||
throw new SecretSyncError({
|
||||
message: "Key schema cannot contain '/' after {{secretKey}}",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
return `${path}${pathSegments.join("/")}/`;
|
||||
};
|
||||
|
||||
const getParametersByPath = async (
|
||||
ssm: AWS.SSM,
|
||||
path: string,
|
||||
keySchema: string | undefined,
|
||||
environment: string
|
||||
): Promise<TAWSParameterStoreRecord> => {
|
||||
const awsParameterStoreSecretsRecord: TAWSParameterStoreRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
const fullPath = getFullPath({ path, keySchema, environment });
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const parameters = await ssm
|
||||
.getParametersByPath({
|
||||
Path: path,
|
||||
Path: fullPath,
|
||||
Recursive: false,
|
||||
WithDecryption: true,
|
||||
MaxResults: BATCH_SIZE,
|
||||
@@ -59,7 +93,7 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
const secKey = fullPath.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreSecretsRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
@@ -83,12 +117,19 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
return awsParameterStoreSecretsRecord;
|
||||
};
|
||||
|
||||
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const getParameterMetadataByPath = async (
|
||||
ssm: AWS.SSM,
|
||||
path: string,
|
||||
keySchema: string | undefined,
|
||||
environment: string
|
||||
): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
const fullPath = getFullPath({ path, keySchema, environment });
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -100,7 +141,7 @@ const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<T
|
||||
{
|
||||
Key: "Path",
|
||||
Option: "OneLevel",
|
||||
Values: [path]
|
||||
Values: [fullPath]
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -112,7 +153,7 @@ const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<T
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
const secKey = fullPath.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
@@ -298,9 +339,19 @@ export const AwsParameterStoreSyncFns = {
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||
ssm,
|
||||
@@ -400,22 +451,32 @@ export const AwsParameterStoreSyncFns = {
|
||||
await deleteParametersBatch(ssm, parametersToDelete);
|
||||
},
|
||||
getSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }])
|
||||
);
|
||||
},
|
||||
removeSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(
|
||||
ssm,
|
||||
destinationConfig.path,
|
||||
syncOptions.keySchema,
|
||||
environment!.slug
|
||||
);
|
||||
|
||||
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { isAxiosError } from "axios";
|
||||
import { AxiosRequestConfig, isAxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { RenderSyncScope } from "./render-sync-enums";
|
||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
|
||||
}
|
||||
};
|
||||
|
||||
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.token}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (input.destination.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
req.url = `/services/${input.destination.serviceId}/env-vars`;
|
||||
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.request<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>({
|
||||
...req,
|
||||
params: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
}
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
req.url = `/env-groups/${input.destination.environmentGroupId}`;
|
||||
|
||||
const res = await makeRequestWithRetry(() =>
|
||||
request.request<{
|
||||
envVars: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>(req)
|
||||
);
|
||||
|
||||
return res.data.envVars.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
}));
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
}
|
||||
|
||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
const secrets = await getSecrets({
|
||||
destination: destinationConfig,
|
||||
token: apiKey
|
||||
});
|
||||
|
||||
do {
|
||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
||||
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.get<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const batchUpdateEnvironmentSecrets = async (
|
||||
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars`,
|
||||
data: envVars
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
for await (const variable of envVars) {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
|
||||
data: {
|
||||
value: variable.value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEnvironmentSecret = async (
|
||||
secretSync: TRenderSyncWithCredentials,
|
||||
envVar: { key: string; value: string }
|
||||
): Promise<void> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiKey }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.post(
|
||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
method: "POST",
|
||||
url: `/services/${destinationConfig.serviceId}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
|
||||
...req,
|
||||
method: "GET",
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}`
|
||||
});
|
||||
|
||||
for await (const link of data.serviceLinks) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${link.id}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
export const RenderSyncFns = {
|
||||
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
|
||||
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||
|
||||
for (const renderSecret of renderSecrets) {
|
||||
if (!(renderSecret.key in secretMap)) {
|
||||
if (renderSecret.key in secretMap) {
|
||||
finalEnvVars.push({
|
||||
key: renderSecret.key,
|
||||
value: renderSecret.value
|
||||
});
|
||||
}
|
||||
}
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
|
@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
environmentGroupId: z
|
||||
.string()
|
||||
.min(1, "Environment Group ID is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -310,7 +310,8 @@
|
||||
"self-hosting/guides/mongo-to-postgres",
|
||||
"self-hosting/guides/custom-certificates",
|
||||
"self-hosting/guides/automated-bootstrapping",
|
||||
"self-hosting/guides/production-hardening"
|
||||
"self-hosting/guides/production-hardening",
|
||||
"self-hosting/guides/monitoring-telemetry"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -416,6 +417,9 @@
|
||||
"pages": [
|
||||
"documentation/platform/secrets-mgmt/project",
|
||||
"documentation/platform/folder",
|
||||
"documentation/platform/secret-versioning",
|
||||
"documentation/platform/pit-recovery",
|
||||
"documentation/platform/secret-reference",
|
||||
{
|
||||
"group": "Secret Rotation",
|
||||
"pages": [
|
||||
@@ -459,7 +463,8 @@
|
||||
"documentation/platform/dynamic-secrets/kubernetes",
|
||||
"documentation/platform/dynamic-secrets/vertica"
|
||||
]
|
||||
}
|
||||
},
|
||||
"documentation/platform/webhooks"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -25,6 +25,11 @@ This functionality works in the following way:
|
||||
{/*  */}
|
||||

|
||||
|
||||
<Note>
|
||||
Optionally, approvers can edit the duration of an access request to reduce how long access will be granted by clicking the **Edit** icon next to the duration.
|
||||

|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
If the access request matches with a policy that allows break-glass approval
|
||||
bypasses, the requester may bypass the policy and get access to the resource
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Delivering Secrets"
|
||||
description: "Learn how to get secrets out of Infisical and into the systems, applications, and environments that need them."
|
||||
title: "Fetching Secrets"
|
||||
description: "Learn how to deliver secrets from Infisical into the systems, applications, and environments that need them."
|
||||
---
|
||||
|
||||
Once secrets are stored and scoped in Infisical, the next step is delivering them securely to the systems and applications that need them.
|
||||
|
BIN
docs/images/platform/access-controls/edit-access-request.png
Normal file
BIN
docs/images/platform/access-controls/edit-access-request.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 638 KiB |
@@ -27,7 +27,7 @@ $ ansible-galaxy collection install infisical.vault
|
||||
The python module dependencies are not installed by ansible-galaxy. They can be manually installed using pip:
|
||||
|
||||
```bash
|
||||
$ pip install infisical-python
|
||||
$ pip install infisicalsdk
|
||||
```
|
||||
|
||||
## Using this collection
|
||||
@@ -42,7 +42,7 @@ vars:
|
||||
# [{ "key": "HOST", "value": "google.com" }, { "key": "SMTP", "value": "gmail.smtp.edu" }]
|
||||
|
||||
read_secret_by_name_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', secret_name='HOST', url='https://spotify.infisical.com') }}"
|
||||
# [{ "key": "HOST", "value": "google.com" }]
|
||||
# { "key": "HOST", "value": "google.com" }
|
||||
```
|
||||
|
||||
|
||||
|
@@ -22,7 +22,7 @@ It can also automatically reload dependent Deployments resources whenever releva
|
||||
|
||||
## Install
|
||||
|
||||
The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
|
||||
The operator can be installed via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
|
||||
|
||||
**Install the latest Helm repository**
|
||||
```bash
|
||||
@@ -229,9 +229,9 @@ The managed secret created by the operator will not be deleted when the operator
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Helm">
|
||||
Install Infisical Helm repository
|
||||
Uninstall Infisical Helm repository
|
||||
```bash
|
||||
helm uninstall <release name>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
|
@@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical."
|
||||

|
||||
|
||||
- **Render Connection**: The Render Connection to authenticate with.
|
||||
- **Scope**: Select **Service**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Scope**: Select **Service** or **Environment Group**.
|
||||
- **Service**: Choose the Render service you want to sync secrets to.
|
||||
- **Environment Group**: Choose the Render environment group you want to sync secrets to.
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||

|
||||
|
440
docs/self-hosting/guides/monitoring-telemetry.mdx
Normal file
440
docs/self-hosting/guides/monitoring-telemetry.mdx
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
title: "Monitoring and Telemetry Setup"
|
||||
description: "Learn how to set up monitoring and telemetry for your self-hosted Infisical instance using Grafana, Prometheus, and OpenTelemetry."
|
||||
---
|
||||
|
||||
Infisical provides comprehensive monitoring and telemetry capabilities to help you monitor the health, performance, and usage of your self-hosted instance. This guide covers setting up monitoring using Grafana with two different telemetry collection approaches.
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical exports metrics in **OpenTelemetry (OTEL) format**, which provides maximum flexibility for your monitoring infrastructure. While this guide focuses on Grafana, the OTEL format means you can easily integrate with:
|
||||
|
||||
- **Cloud-native monitoring**: AWS CloudWatch, Google Cloud Monitoring, Azure Monitor
|
||||
- **Observability platforms**: Datadog, New Relic, Splunk, Dynatrace
|
||||
- **Custom backends**: Any system that supports OTEL ingestion
|
||||
- **Traditional monitoring**: Prometheus, Grafana (as covered in this guide)
|
||||
|
||||
Infisical supports two telemetry collection methods:
|
||||
|
||||
1. **Pull-based (Prometheus)**: Exposes metrics on a dedicated endpoint for Prometheus to scrape
|
||||
2. **Push-based (OTLP)**: Sends metrics to an OpenTelemetry Collector via OTLP protocol
|
||||
|
||||
Both approaches provide the same metrics data in OTEL format, so you can choose the one that best fits your infrastructure and monitoring strategy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Self-hosted Infisical instance running
|
||||
- Access to deploy monitoring services (Prometheus, Grafana, etc.)
|
||||
- Basic understanding of Prometheus and Grafana
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Configure the following environment variables in your Infisical backend:
|
||||
|
||||
```bash
|
||||
# Enable telemetry collection
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
|
||||
# Choose export type: "prometheus" or "otlp"
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
|
||||
# For OTLP push mode, also configure:
|
||||
# OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics
|
||||
# OTEL_COLLECTOR_BASIC_AUTH_USERNAME=your_collector_username
|
||||
# OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=your_collector_password
|
||||
# OTEL_OTLP_PUSH_INTERVAL=30000
|
||||
```
|
||||
|
||||
**Note**: The `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` values must match the credentials configured in your OpenTelemetry Collector's `basicauth/server` extension. These are not hardcoded values - you configure them in your collector configuration file.
|
||||
|
||||
## Option 1: Pull-based Monitoring (Prometheus)
|
||||
|
||||
This approach exposes metrics on port 9464 at the `/metrics` endpoint, allowing Prometheus to scrape the data. The metrics are exposed in Prometheus format but originate from OpenTelemetry instrumentation.
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Enable Prometheus export in Infisical**:
|
||||
|
||||
```bash
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
OTEL_EXPORT_TYPE=prometheus
|
||||
```
|
||||
|
||||
2. **Expose the metrics port** in your Infisical backend:
|
||||
|
||||
- **Docker**: Expose port 9464
|
||||
- **Kubernetes**: Create a service exposing port 9464
|
||||
- **Other**: Ensure port 9464 is accessible to your monitoring stack
|
||||
|
||||
3. **Create Prometheus configuration** (`prometheus.yml`):
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 30s
|
||||
evaluation_interval: 30s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "infisical"
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: ["infisical-backend:9464"] # Adjust hostname/port based on your deployment
|
||||
metrics_path: "/metrics"
|
||||
```
|
||||
|
||||
**Note**: Replace `infisical-backend:9464` with the actual hostname and port where your Infisical backend is running. This could be:
|
||||
|
||||
- **Docker Compose**: `infisical-backend:9464` (service name)
|
||||
- **Kubernetes**: `infisical-backend.default.svc.cluster.local:9464` (service name)
|
||||
- **Bare Metal**: `192.168.1.100:9464` (actual IP address)
|
||||
- **Cloud**: `your-infisical.example.com:9464` (domain name)
|
||||
|
||||
### Deployment Options
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
```yaml
|
||||
# prometheus-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: prometheus
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: prometheus
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: prometheus
|
||||
spec:
|
||||
containers:
|
||||
- name: prometheus
|
||||
image: prom/prometheus:latest
|
||||
ports:
|
||||
- containerPort: 9090
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/prometheus
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: prometheus-config
|
||||
|
||||
---
|
||||
# prometheus-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: prometheus
|
||||
spec:
|
||||
selector:
|
||||
app: prometheus
|
||||
ports:
|
||||
- port: 9090
|
||||
targetPort: 9090
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
#### Helm
|
||||
|
||||
```bash
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
|
||||
helm install prometheus prometheus-community/prometheus \
|
||||
--set server.config.global.scrape_interval=30s \
|
||||
--set server.config.scrape_configs[0].job_name=infisical \
|
||||
--set server.config.scrape_configs[0].static_configs[0].targets[0]=infisical-backend:9464
|
||||
```
|
||||
|
||||
## Option 2: Push-based Monitoring (OTLP)
|
||||
|
||||
This approach sends metrics directly to an OpenTelemetry Collector via the OTLP protocol. This gives you the most flexibility as you can configure the collector to export to multiple backends simultaneously.
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Enable OTLP export in Infisical**:
|
||||
|
||||
```bash
|
||||
OTEL_TELEMETRY_COLLECTION_ENABLED=true
|
||||
OTEL_EXPORT_TYPE=otlp
|
||||
OTEL_EXPORT_OTLP_ENDPOINT=http://otel-collector:4318/v1/metrics
|
||||
OTEL_COLLECTOR_BASIC_AUTH_USERNAME=infisical
|
||||
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD=infisical
|
||||
OTEL_OTLP_PUSH_INTERVAL=30000
|
||||
```
|
||||
|
||||
2. **Create OpenTelemetry Collector configuration** (`otel-collector-config.yaml`):
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
health_check:
|
||||
pprof:
|
||||
zpages:
|
||||
basicauth/server:
|
||||
htpasswd:
|
||||
inline: |
|
||||
your_username:your_password
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
auth:
|
||||
authenticator: basicauth/server
|
||||
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: otel-collector
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: [infisical-backend:9464]
|
||||
metric_relabel_configs:
|
||||
- action: labeldrop
|
||||
regex: "service_instance_id|service_name"
|
||||
|
||||
processors:
|
||||
batch:
|
||||
|
||||
exporters:
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
auth:
|
||||
authenticator: basicauth/server
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
|
||||
service:
|
||||
extensions: [basicauth/server, health_check, pprof, zpages]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [prometheus]
|
||||
```
|
||||
|
||||
**Important**: Replace `your_username:your_password` with your chosen credentials. These must match the values you set in Infisical's `OTEL_COLLECTOR_BASIC_AUTH_USERNAME` and `OTEL_COLLECTOR_BASIC_AUTH_PASSWORD` environment variables.
|
||||
|
||||
3. **Create Prometheus configuration** for the collector:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 30s
|
||||
evaluation_interval: 30s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"] # Adjust hostname/port based on your deployment
|
||||
metrics_path: "/metrics"
|
||||
```
|
||||
|
||||
**Note**: Replace `otel-collector:8889` with the actual hostname and port where your OpenTelemetry Collector is running. This could be:
|
||||
|
||||
- **Docker Compose**: `otel-collector:8889` (service name)
|
||||
- **Kubernetes**: `otel-collector.default.svc.cluster.local:8889` (service name)
|
||||
- **Bare Metal**: `192.168.1.100:8889` (actual IP address)
|
||||
- **Cloud**: `your-collector.example.com:8889` (domain name)
|
||||
|
||||
### Deployment Options
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
ports:
|
||||
- 4318:4318 # OTLP http receiver
|
||||
- 8889:8889 # Prometheus exporter metrics
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
|
||||
command:
|
||||
- "--config=/etc/otelcol-contrib/config.yaml"
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
```yaml
|
||||
# otel-collector-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: otel-collector
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: otel-collector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: otel-collector
|
||||
spec:
|
||||
containers:
|
||||
- name: otel-collector
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
ports:
|
||||
- containerPort: 4318
|
||||
- containerPort: 8889
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/otelcol-contrib
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: otel-collector-config
|
||||
```
|
||||
|
||||
#### Helm
|
||||
|
||||
```bash
|
||||
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
|
||||
helm install otel-collector open-telemetry/opentelemetry-collector \
|
||||
--set config.receivers.otlp.protocols.http.endpoint=0.0.0.0:4318 \
|
||||
--set config.exporters.prometheus.endpoint=0.0.0.0:8889
|
||||
```
|
||||
|
||||
## Alternative Backends
|
||||
|
||||
Since Infisical exports in OpenTelemetry format, you can easily configure the collector to send metrics to other backends instead of (or in addition to) Prometheus:
|
||||
|
||||
### Cloud-Native Examples
|
||||
|
||||
```yaml
|
||||
# Add to your otel-collector-config.yaml exporters section
|
||||
exporters:
|
||||
# AWS CloudWatch
|
||||
awsemf:
|
||||
region: us-west-2
|
||||
log_group_name: /aws/emf/infisical
|
||||
log_stream_name: metrics
|
||||
|
||||
# Google Cloud Monitoring
|
||||
googlecloud:
|
||||
project_id: your-project-id
|
||||
|
||||
# Azure Monitor
|
||||
azuremonitor:
|
||||
connection_string: "your-connection-string"
|
||||
|
||||
# Datadog
|
||||
datadog:
|
||||
api:
|
||||
key: "your-api-key"
|
||||
site: "datadoghq.com"
|
||||
|
||||
# New Relic
|
||||
newrelic:
|
||||
apikey: "your-api-key"
|
||||
host_override: "otlp.nr-data.net"
|
||||
```
|
||||
|
||||
### Multi-Backend Configuration
|
||||
|
||||
```yaml
|
||||
service:
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [prometheus, awsemf, datadog] # Send to multiple backends
|
||||
```
|
||||
|
||||
## Setting Up Grafana
|
||||
|
||||
1. **Access Grafana**: Navigate to your Grafana instance
|
||||
2. **Login**: Use your configured credentials
|
||||
3. **Add Prometheus Data Source**:
|
||||
- Go to Configuration → Data Sources
|
||||
- Click "Add data source"
|
||||
- Select "Prometheus"
|
||||
- Set URL to your Prometheus endpoint
|
||||
- Click "Save & Test"
|
||||
|
||||
## Available Metrics
|
||||
|
||||
Infisical exposes the following key metrics in OpenTelemetry format:
|
||||
|
||||
### API Performance Metrics
|
||||
|
||||
- `API_latency` - API request latency histogram in milliseconds
|
||||
|
||||
- **Labels**: `route`, `method`, `statusCode`
|
||||
- **Example**: Monitor response times for specific endpoints
|
||||
|
||||
- `API_errors` - API error count histogram
|
||||
- **Labels**: `route`, `method`, `type`, `name`
|
||||
- **Example**: Track error rates by endpoint and error type
|
||||
|
||||
### Integration & Secret Sync Metrics
|
||||
|
||||
- `integration_secret_sync_errors` - Integration secret sync error count
|
||||
|
||||
- **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId`
|
||||
- **Example**: Monitor integration sync failures across different services
|
||||
|
||||
- `secret_sync_sync_secrets_errors` - Secret sync operation error count
|
||||
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Track secret sync failures to external systems
|
||||
|
||||
- `secret_sync_import_secrets_errors` - Secret import operation error count
|
||||
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Monitor secret import failures
|
||||
|
||||
- `secret_sync_remove_secrets_errors` - Secret removal operation error count
|
||||
- **Labels**: `version`, `destination`, `syncId`, `projectId`, `type`, `status`, `name`
|
||||
- **Example**: Track secret removal operation failures
|
||||
|
||||
### System Metrics
|
||||
|
||||
These metrics are automatically collected by OpenTelemetry's HTTP instrumentation:
|
||||
|
||||
- `http_server_duration` - HTTP server request duration metrics (histogram buckets, count, sum)
|
||||
- `http_client_duration` - HTTP client request duration metrics (histogram buckets, count, sum)
|
||||
|
||||
### Custom Business Metrics
|
||||
|
||||
- `infisical_secret_operations_total` - Total secret operations
|
||||
- `infisical_secrets_processed_total` - Total secrets processed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Metrics not appearing**:
|
||||
|
||||
- Check if `OTEL_TELEMETRY_COLLECTION_ENABLED=true`
|
||||
- Verify the correct `OTEL_EXPORT_TYPE` is set
|
||||
- Check network connectivity between services
|
||||
|
||||
2. **Authentication errors**:
|
||||
|
||||
- Verify basic auth credentials in OTLP configuration
|
||||
- Check if credentials match between Infisical and collector
|
29
frontend/src/components/roles/RoleOption.tsx
Normal file
29
frontend/src/components/roles/RoleOption.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { components, OptionProps } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const RoleOption = ({
|
||||
isSelected,
|
||||
children,
|
||||
...props
|
||||
}: OptionProps<{ name: string; slug: string; description?: string | undefined }>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<p className="truncate">{children}</p>
|
||||
{props.data.description ? (
|
||||
<p className="truncate text-xs leading-4 text-mineshaft-400">
|
||||
{props.data.description}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs leading-4 text-mineshaft-400/50">No Description</p>
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
1
frontend/src/components/roles/index.tsx
Normal file
1
frontend/src/components/roles/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./RoleOption";
|
@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||
import {
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService,
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
|
||||
|
||||
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
|
||||
connectionId,
|
||||
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(connectionId, {
|
||||
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.serviceId", "");
|
||||
setValue("destinationConfig.environmentGroupId", "");
|
||||
setValue("destinationConfig.type", RenderSyncType.Env);
|
||||
setValue("destinationConfig.scope", RenderSyncScope.Service);
|
||||
}}
|
||||
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === RenderSyncScope.Service && (
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Service"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isServicesPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.serviceName",
|
||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedScope === RenderSyncScope.EnvironmentGroup && (
|
||||
<Controller
|
||||
name="destinationConfig.environmentGroupId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Environment Group"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isGroupsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
|
||||
setValue(
|
||||
"destinationConfig.environmentGroupName",
|
||||
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
|
||||
);
|
||||
}}
|
||||
options={groups}
|
||||
placeholder="Select an environment group..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
export const RenderSyncOptionsReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
|
||||
|
||||
export const RenderSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||
const serviceName = watch("destinationConfig.serviceName");
|
||||
const scope = watch("destinationConfig.scope");
|
||||
const config = watch("destinationConfig");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
|
||||
{config.scope === RenderSyncScope.Service ? (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.serviceName ?? config.serviceId}
|
||||
</GenericFieldLabel>
|
||||
) : (
|
||||
<GenericFieldLabel label="Service">
|
||||
{config.environmentGroupName ?? config.environmentGroupId}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
||||
serviceId: z.string().trim().min(1, "Service is required"),
|
||||
serviceName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup),
|
||||
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
|
||||
environmentGroupName: z.string().trim().optional(),
|
||||
type: z.nativeEnum(RenderSyncType)
|
||||
})
|
||||
])
|
||||
})
|
||||
|
@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
|
||||
[RenderSyncScope.Service]: {
|
||||
name: "Service",
|
||||
description: "Infisical will sync secrets to the specified Render service."
|
||||
},
|
||||
[RenderSyncScope.EnvironmentGroup]: {
|
||||
name: "EnvironmentGroup",
|
||||
description: "Infisical will sync secrets to the specified Render environment group."
|
||||
}
|
||||
};
|
||||
|
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TRenderService } from "./types";
|
||||
import { TRenderEnvironmentGroup, TRenderService } from "./types";
|
||||
|
||||
const renderConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "render"] as const,
|
||||
listServices: (connectionId: string) =>
|
||||
[...renderConnectionKeys.all, "services", connectionId] as const
|
||||
[...renderConnectionKeys.all, "services", connectionId] as const,
|
||||
listEnvironmentGroups: (connectionId: string) =>
|
||||
[...renderConnectionKeys.all, "environment-groups", connectionId] as const
|
||||
};
|
||||
|
||||
export const useRenderConnectionListServices = (
|
||||
@@ -35,3 +37,28 @@ export const useRenderConnectionListServices = (
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenderConnectionListEnvironmentGroups = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TRenderEnvironmentGroup[],
|
||||
unknown,
|
||||
TRenderEnvironmentGroup[],
|
||||
ReturnType<typeof renderConnectionKeys.listEnvironmentGroups>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TRenderEnvironmentGroup[]>(
|
||||
`/api/v1/app-connections/render/${connectionId}/environment-groups`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
@@ -2,3 +2,8 @@ export type TRenderService = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
@@ -104,6 +104,7 @@ export const useUpdateOrg = () => {
|
||||
mutationFn: ({
|
||||
name,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
slug,
|
||||
orgId,
|
||||
@@ -125,6 +126,7 @@ export const useUpdateOrg = () => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
slug,
|
||||
defaultMembershipRoleSlug,
|
||||
|
@@ -9,6 +9,7 @@ export type Organization = {
|
||||
createAt: string;
|
||||
updatedAt: string;
|
||||
authEnforced: boolean;
|
||||
googleSsoAuthEnforced: boolean;
|
||||
bypassOrgAuthEnabled: boolean;
|
||||
orgAuthMethod: string;
|
||||
scimEnabled: boolean;
|
||||
@@ -34,6 +35,7 @@ export type UpdateOrgDTO = {
|
||||
orgId: string;
|
||||
name?: string;
|
||||
authEnforced?: boolean;
|
||||
googleSsoAuthEnforced?: boolean;
|
||||
scimEnabled?: boolean;
|
||||
slug?: string;
|
||||
defaultMembershipRoleSlug?: string;
|
||||
|
@@ -4,12 +4,19 @@ import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/typ
|
||||
|
||||
export type TRenderSync = TRootSecretSync & {
|
||||
destination: SecretSync.Render;
|
||||
destinationConfig: {
|
||||
scope: RenderSyncScope.Service;
|
||||
type: RenderSyncType;
|
||||
serviceId: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
destinationConfig:
|
||||
| {
|
||||
type: RenderSyncType;
|
||||
scope: RenderSyncScope.Service;
|
||||
serviceId: string;
|
||||
serviceName?: string | undefined;
|
||||
}
|
||||
| {
|
||||
type: RenderSyncType;
|
||||
scope: RenderSyncScope.EnvironmentGroup;
|
||||
environmentGroupId: string;
|
||||
environmentGroupName?: string | undefined;
|
||||
};
|
||||
|
||||
connection: {
|
||||
app: AppConnection.Render;
|
||||
@@ -23,7 +30,8 @@ export type TRenderSync = TRootSecretSync & {
|
||||
};
|
||||
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
@@ -48,6 +48,7 @@ export type SubscriptionPlan = {
|
||||
externalKms: boolean;
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
enforceGoogleSSO: boolean;
|
||||
projectTemplates: boolean;
|
||||
kmip: boolean;
|
||||
secretScanning: boolean;
|
||||
|
@@ -240,6 +240,13 @@ export const Navbar = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (org.googleSsoAuthEnforced) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleOrgChange(org?.id);
|
||||
}}
|
||||
variant="plain"
|
||||
|
@@ -82,25 +82,40 @@ export const SelectOrganizationSection = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (organization.authEnforced && !canBypassOrgAuth) {
|
||||
if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) {
|
||||
const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod };
|
||||
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
await logout.mutateAsync();
|
||||
let url = "";
|
||||
if (organization.orgAuthMethod === AuthMethod.OIDC) {
|
||||
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
|
||||
callbackPort ? `&callbackPort=${callbackPort}` : ""
|
||||
}`;
|
||||
} else {
|
||||
} else if (organization.orgAuthMethod === AuthMethod.SAML) {
|
||||
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
|
||||
|
||||
if (callbackPort) {
|
||||
url += `?callback_port=${callbackPort}`;
|
||||
}
|
||||
} else if (
|
||||
organization.googleSsoAuthEnforced &&
|
||||
authToken.authMethod !== AuthMethod.GOOGLE
|
||||
) {
|
||||
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
|
||||
|
||||
if (callbackPort) {
|
||||
url += `&callback_port=${callbackPort}`;
|
||||
}
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
return;
|
||||
// we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO
|
||||
// see line 103-106
|
||||
if (url) {
|
||||
await logout.mutateAsync();
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||
|
@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { RoleOption } from "@app/components/roles";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
@@ -45,7 +46,11 @@ const addMemberFormSchema = z.object({
|
||||
)
|
||||
.default([]),
|
||||
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||
organizationRole: z.object({ name: z.string(), slug: z.string() })
|
||||
organizationRole: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string().optional()
|
||||
})
|
||||
});
|
||||
|
||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||
@@ -238,6 +243,7 @@ export const AddOrgMemberModal = ({
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
components={{ Option: RoleOption }}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
@@ -36,7 +36,8 @@ type GithubFormData = BaseFormData &
|
||||
type GithubRadarFormData = BaseFormData &
|
||||
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
|
||||
|
||||
type GitLabFormData = BaseFormData & Pick<TGitLabConnection, "name" | "method" | "description">;
|
||||
type GitLabFormData = BaseFormData &
|
||||
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
|
||||
|
||||
type AzureKeyVaultFormData = BaseFormData &
|
||||
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
|
||||
@@ -147,7 +148,7 @@ export const OAuthCallbackPage = () => {
|
||||
|
||||
clearState(AppConnection.GitLab);
|
||||
|
||||
const { connectionId, name, description, returnUrl, isUpdate } = formData;
|
||||
const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData;
|
||||
|
||||
try {
|
||||
if (isUpdate && connectionId) {
|
||||
@@ -155,7 +156,8 @@ export const OAuthCallbackPage = () => {
|
||||
app: AppConnection.GitLab,
|
||||
connectionId,
|
||||
credentials: {
|
||||
code: code as string
|
||||
code: code as string,
|
||||
instanceUrl: credentials.instanceUrl as string
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -165,7 +167,8 @@ export const OAuthCallbackPage = () => {
|
||||
description,
|
||||
method: GitLabConnectionMethod.OAuth,
|
||||
credentials: {
|
||||
code: code as string
|
||||
code: code as string,
|
||||
instanceUrl: credentials.instanceUrl as string
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -13,8 +13,23 @@ import {
|
||||
} from "@app/context";
|
||||
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const OrgGeneralAuthSection = () => {
|
||||
enum EnforceAuthType {
|
||||
SAML = "saml",
|
||||
GOOGLE = "google",
|
||||
OIDC = "oidc"
|
||||
}
|
||||
|
||||
export const OrgGeneralAuthSection = ({
|
||||
isSamlConfigured,
|
||||
isOidcConfigured,
|
||||
isGoogleConfigured
|
||||
}: {
|
||||
isSamlConfigured: boolean;
|
||||
isOidcConfigured: boolean;
|
||||
isGoogleConfigured: boolean;
|
||||
}) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
@@ -23,27 +38,61 @@ export const OrgGeneralAuthSection = () => {
|
||||
|
||||
const logout = useLogoutUser();
|
||||
|
||||
const handleEnforceOrgAuthToggle = async (value: boolean) => {
|
||||
const handleEnforceOrgAuthToggle = async (value: boolean, type: EnforceAuthType) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.samlSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
|
||||
if (type === EnforceAuthType.SAML) {
|
||||
if (!subscription?.samlSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
authEnforced: value
|
||||
});
|
||||
} else if (type === EnforceAuthType.GOOGLE) {
|
||||
if (!subscription?.enforceGoogleSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
googleSsoAuthEnforced: value
|
||||
});
|
||||
} else if (type === EnforceAuthType.OIDC) {
|
||||
if (!subscription?.oidcSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
authEnforced: value
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: `Invalid auth enforcement type ${type}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
authEnforced: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} org-level auth`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (value) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
|
||||
|
||||
if (type === EnforceAuthType.SAML) {
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
|
||||
} else if (type === EnforceAuthType.GOOGLE) {
|
||||
window.open(`/api/v1/sso/redirect/google?org_slug=${currentOrg.slug}`);
|
||||
}
|
||||
|
||||
window.close();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -78,45 +127,91 @@ export const OrgGeneralAuthSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="allow-org-invites"
|
||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
|
||||
</div> */}
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-org-auth"
|
||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce users to authenticate via SAML to access this organization
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-gray-200">SSO Enforcement</p>
|
||||
<p className="mb-2 mt-1 text-gray-400">
|
||||
Manage strict enforcement of specific authentication methods for your organization.
|
||||
</p>
|
||||
</div>
|
||||
{currentOrg?.authEnforced && (
|
||||
<div className="py-4">
|
||||
<div className="flex flex-col gap-2 py-4">
|
||||
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-saml-auth"
|
||||
onCheckedChange={(value) =>
|
||||
handleEnforceOrgAuthToggle(value, EnforceAuthType.SAML)
|
||||
}
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
isDisabled={!isAllowed || currentOrg?.googleSsoAuthEnforced}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce users to authenticate via SAML to access this organization.
|
||||
<br />
|
||||
When this is enabled your organization members will only be able to login with SAML.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-oidc-auth"
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
onCheckedChange={(value) =>
|
||||
handleEnforceOrgAuthToggle(value, EnforceAuthType.OIDC)
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce users to authenticate via OIDC to access this organization.
|
||||
<br />
|
||||
When this is enabled your organization members will only be able to login with OIDC.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce Google SSO</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-google-sso"
|
||||
onCheckedChange={(value) =>
|
||||
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
|
||||
}
|
||||
isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
|
||||
isDisabled={!isAllowed || currentOrg?.authEnforced}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Enforce users to authenticate via Google to access this organization.
|
||||
<br />
|
||||
When this is enabled your organization members will only be able to login with Google.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{(currentOrg?.authEnforced || currentOrg?.googleSsoAuthEnforced) && (
|
||||
<div className="mt-4 py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
|
||||
@@ -125,8 +220,8 @@ export const OrgGeneralAuthSection = () => {
|
||||
content={
|
||||
<div>
|
||||
<span>
|
||||
When this is enabled, we strongly recommend enforcing MFA at the organization
|
||||
level.
|
||||
When enabling admin SSO bypass, we highly recommend enabling MFA enforcement
|
||||
at the organization-level for security reasons.
|
||||
</span>
|
||||
<p className="mt-4">
|
||||
In case of a lockout, admins can use the{" "}
|
||||
@@ -182,6 +277,6 @@ export const OrgGeneralAuthSection = () => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -95,43 +95,25 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div className="mb-4">
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">LDAP</h2>
|
||||
<div className="flex">
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
|
||||
{(isAllowed) => (
|
||||
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-gray-200">LDAP</p>
|
||||
<p className="mb-2 text-gray-400">Manage LDAP authentication configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={openLDAPGroupMapModal}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Button onClick={addLDAPBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Manage how LDAP groups are mapped to internal groups in Infisical
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{data && (
|
||||
<div className="py-4">
|
||||
<div className="pt-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
|
||||
@@ -152,6 +134,27 @@ export const OrgLDAPSection = (): JSX.Element => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">LDAP Group Mappings</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={openLDAPGroupMapModal}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Manage how LDAP groups are mapped to internal groups in Infisical
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LDAPModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useGetOIDCConfig, useLogoutUser, useUpdateOrg } from "@app/hooks/api";
|
||||
import { useGetOIDCConfig } from "@app/hooks/api";
|
||||
import { useUpdateOIDCConfig } from "@app/hooks/api/oidcConfig/mutations";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -23,9 +23,7 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
|
||||
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
|
||||
const { mutateAsync } = useUpdateOIDCConfig();
|
||||
const { mutateAsync: updateOrg } = useUpdateOrg();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addOIDC",
|
||||
"upgradePlan"
|
||||
@@ -54,56 +52,6 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnforceOrgAuthToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.oidcSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOrg({
|
||||
orgId: currentOrg?.id,
|
||||
authEnforced: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (value) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/oidc/login?orgSlug=${currentOrg.slug}`);
|
||||
window.close();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnableBypassOrgAuthToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (!subscription?.oidcSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOrg({
|
||||
orgId: currentOrg?.id,
|
||||
bypassOrgAuthEnabled: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} admin bypassing of org-level auth`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOIDCGroupManagement = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
@@ -136,25 +84,22 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">OIDC</h2>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={addOidcButtonClick}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-gray-200">OIDC</p>
|
||||
<p className="mb-2 text-gray-400">Manage OIDC authentication configuration</p>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">Manage OIDC authentication configuration</p>
|
||||
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button onClick={addOidcButtonClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="py-4">
|
||||
@@ -178,88 +123,6 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-org-auth"
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
<span>Enforce users to authenticate via OIDC to access this organization.</span>
|
||||
</p>
|
||||
</div>
|
||||
{currentOrg?.authEnforced && (
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-md text-mineshaft-100">Enable Admin SSO Bypass</span>
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<span>
|
||||
When this is enabled, we strongly recommend enforcing MFA at the organization
|
||||
level.
|
||||
</span>
|
||||
<p className="mt-4">
|
||||
In case of a lockout, admins can use the{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-300"
|
||||
href="https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Admin Login Portal
|
||||
</a>{" "}
|
||||
at{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-300"
|
||||
href={`${window.location.origin}/login/admin`}
|
||||
>
|
||||
{window.location.origin}/login/admin
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
size="sm"
|
||||
className="mt-0.5 inline-block text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="allow-admin-bypass"
|
||||
isChecked={currentOrg?.bypassOrgAuthEnabled ?? false}
|
||||
onCheckedChange={(value) => handleEnableBypassOrgAuthToggle(value)}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
<span>
|
||||
Allow organization admins to bypass OIDC enforcement when SSO is unavailable,
|
||||
misconfigured, or inaccessible.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="text-md flex items-center text-mineshaft-100">
|
||||
|
@@ -79,25 +79,24 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr className="border-mineshaft-600" />
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-md text-mineshaft-100">SAML</h2>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-gray-200">SAML</p>
|
||||
<p className="mb-2 text-gray-400">Manage SAML authentication configuration</p>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button onClick={addSSOBtnClick} colorSchema="secondary" isDisabled={!isAllowed}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between pt-4">
|
||||
<h2 className="text-md text-mineshaft-100">Enable SAML</h2>
|
||||
{!isPending && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
@@ -126,6 +125,6 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -49,13 +49,19 @@ export const OrgSsoTab = withPermission(
|
||||
);
|
||||
const areConfigsLoading = isLoadingOidcConfig || isLoadingSamlConfig || isLoadingLdapConfig;
|
||||
|
||||
const shouldDisplaySection = (method: LoginMethod) =>
|
||||
!enabledLoginMethods || enabledLoginMethods.includes(method);
|
||||
const shouldDisplaySection = (method: LoginMethod[] | LoginMethod) => {
|
||||
if (Array.isArray(method)) {
|
||||
return method.some((m) => !enabledLoginMethods || enabledLoginMethods.includes(m));
|
||||
}
|
||||
|
||||
const isOidcConfigured = oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer);
|
||||
return !enabledLoginMethods || enabledLoginMethods.includes(method);
|
||||
};
|
||||
|
||||
const isOidcConfigured = Boolean(oidcConfig && (oidcConfig.discoveryURL || oidcConfig.issuer));
|
||||
const isSamlConfigured =
|
||||
samlConfig && (samlConfig.entryPoint || samlConfig.issuer || samlConfig.cert);
|
||||
const isLdapConfigured = ldapConfig && ldapConfig.url;
|
||||
const isGoogleConfigured = shouldDisplaySection(LoginMethod.GOOGLE);
|
||||
|
||||
const shouldShowCreateIdentityProviderView =
|
||||
!isOidcConfigured && !isSamlConfigured && !isLdapConfigured;
|
||||
@@ -65,11 +71,14 @@ export const OrgSsoTab = withPermission(
|
||||
shouldDisplaySection(LoginMethod.OIDC) ||
|
||||
shouldDisplaySection(LoginMethod.LDAP) ? (
|
||||
<>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
|
||||
<p className="mb-2 mt-1 text-gray-400">
|
||||
Connect your identity provider to simplify user management
|
||||
</p>
|
||||
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-gray-200">Connect an Identity Provider</p>
|
||||
<p className="mb-2 mt-1 text-gray-400">
|
||||
Connect your identity provider to simplify user management with options like SAML,
|
||||
OIDC, and LDAP.
|
||||
</p>
|
||||
</div>
|
||||
{shouldDisplaySection(LoginMethod.SAML) && (
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -169,20 +178,27 @@ export const OrgSsoTab = withPermission(
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
<>
|
||||
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<OrgGeneralAuthSection />
|
||||
<OrgSSOSection />
|
||||
<div className="space-y-4">
|
||||
{shouldDisplaySection([LoginMethod.SAML, LoginMethod.GOOGLE]) && (
|
||||
<OrgGeneralAuthSection
|
||||
isSamlConfigured={isSamlConfigured}
|
||||
isOidcConfigured={isOidcConfigured}
|
||||
isGoogleConfigured={isGoogleConfigured}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
<div className="mb-4 space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div>
|
||||
{isSamlConfigured && shouldDisplaySection(LoginMethod.SAML) && <OrgSSOSection />}
|
||||
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
|
||||
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
|
||||
</div>
|
||||
)}
|
||||
{isOidcConfigured && shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
|
||||
{isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
@@ -6,6 +6,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { RoleOption } from "@app/components/roles";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
@@ -320,6 +321,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
>
|
||||
<FilterableSelect
|
||||
options={roles}
|
||||
components={{ Option: RoleOption }}
|
||||
placeholder="Select roles..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
@@ -24,7 +24,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Displaying audit logs from the last {auditLogsRetentionDays} days
|
||||
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
||||
</p>
|
||||
</div>
|
||||
<LogsSection
|
||||
@@ -32,7 +32,9 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { integrationId: integration.id },
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||
startDate: new Date(
|
||||
new Date().setDate(new Date().getDate() - Math.min(auditLogsRetentionDays, 60))
|
||||
),
|
||||
eventType: INTEGRATION_EVENTS
|
||||
}}
|
||||
/>
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
import {
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
@@ -9,21 +12,59 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RenderSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
|
||||
|
||||
const { data: services = [], isPending } = useRenderConnectionListServices(
|
||||
secretSync.connectionId
|
||||
secretSync.connectionId,
|
||||
{
|
||||
enabled: isServiceScope
|
||||
}
|
||||
);
|
||||
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
serviceName: services.find((s) => s.id === secretSync.destinationConfig.serviceId)?.name
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
|
||||
|
||||
switch (secretSync.destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
const id = secretSync.destinationConfig.serviceId;
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
serviceName: services.find((s) => s.id === id)?.name
|
||||
}
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
}
|
||||
});
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const id = secretSync.destinationConfig.environmentGroupId;
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues({
|
||||
...secretSync,
|
||||
destinationConfig: {
|
||||
...secretSync.destinationConfig,
|
||||
environmentGroupName: groups.find((s) => s.id === id)?.name
|
||||
}
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <SecretSyncTableCell primaryText="Loading service info..." secondaryText="Service" />;
|
||||
if (isGroupsPending) {
|
||||
return (
|
||||
<SecretSyncTableCell
|
||||
primaryText="Loading environment group info..."
|
||||
secondaryText="Environment Group"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown render sync destination scope");
|
||||
}
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
} from "@app/hooks/api/secretSyncs/types/github-sync";
|
||||
import { GitLabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
|
||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
// This functional ensures parity across what is displayed in the destination column
|
||||
// and the values used when search filtering
|
||||
@@ -125,8 +126,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
secondaryText = destinationConfig.app;
|
||||
break;
|
||||
case SecretSync.Render:
|
||||
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
|
||||
secondaryText = "Service";
|
||||
if (destinationConfig.scope === RenderSyncScope.Service) {
|
||||
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
|
||||
secondaryText = "Service";
|
||||
} else {
|
||||
primaryText =
|
||||
destinationConfig.environmentGroupName ?? destinationConfig.environmentGroupId;
|
||||
secondaryText = "Environment Group";
|
||||
}
|
||||
|
||||
break;
|
||||
case SecretSync.Flyio:
|
||||
primaryText = destinationConfig.appId;
|
||||
|
@@ -72,7 +72,10 @@ import {
|
||||
useMoveSecrets,
|
||||
useUpdateSecretBatch
|
||||
} from "@app/hooks/api";
|
||||
import { dashboardKeys, fetchDashboardProjectSecretsByKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
@@ -96,7 +99,7 @@ import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||
import { FolderForm } from "./FolderForm";
|
||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||
|
||||
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
|
||||
type TParsedEnv = { value: string; comments: string[]; secretPath?: string; secretKey: string }[];
|
||||
type TParsedFolderEnv = Record<
|
||||
string,
|
||||
Record<string, { value: string; comments: string[]; secretPath?: string }>
|
||||
@@ -404,8 +407,8 @@ export const ActionBar = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const allUpdateSecrets: TParsedEnv = {};
|
||||
const allCreateSecrets: TParsedEnv = {};
|
||||
const allUpdateSecrets: TParsedEnv = [];
|
||||
const allCreateSecrets: TParsedEnv = [];
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(envByPath).map(async ([folderPath, secrets]) => {
|
||||
@@ -436,7 +439,7 @@ export const ActionBar = ({
|
||||
(_, i) => secretFolderKeys.slice(i * batchSize, (i + 1) * batchSize)
|
||||
);
|
||||
|
||||
const existingSecretLookup: Record<string, boolean> = {};
|
||||
const existingSecretLookup = new Set<string>();
|
||||
|
||||
const processBatches = async () => {
|
||||
await secretBatches.reduce(async (previous, batch) => {
|
||||
@@ -450,7 +453,7 @@ export const ActionBar = ({
|
||||
});
|
||||
|
||||
batchSecrets.forEach((secret) => {
|
||||
existingSecretLookup[secret.secretKey] = true;
|
||||
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
};
|
||||
@@ -464,18 +467,18 @@ export const ActionBar = ({
|
||||
// Store the path with the secret for later batch processing
|
||||
const secretWithPath = {
|
||||
...secretData,
|
||||
secretPath: normalizedPath
|
||||
secretPath: normalizedPath,
|
||||
secretKey
|
||||
};
|
||||
|
||||
if (existingSecretLookup[secretKey]) {
|
||||
allUpdateSecrets[secretKey] = secretWithPath;
|
||||
if (existingSecretLookup.has(`${normalizedPath}-${secretKey}`)) {
|
||||
allUpdateSecrets.push(secretWithPath);
|
||||
} else {
|
||||
allCreateSecrets[secretKey] = secretWithPath;
|
||||
allCreateSecrets.push(secretWithPath);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
handlePopUpOpen("confirmUpload", {
|
||||
update: allUpdateSecrets,
|
||||
create: allCreateSecrets
|
||||
@@ -518,7 +521,7 @@ export const ActionBar = ({
|
||||
const allPaths = new Set<string>();
|
||||
|
||||
// Add paths from create secrets
|
||||
Object.values(create || {}).forEach((secData) => {
|
||||
create.forEach((secData) => {
|
||||
if (secData.secretPath && secData.secretPath !== secretPath) {
|
||||
allPaths.add(secData.secretPath);
|
||||
}
|
||||
@@ -574,8 +577,8 @@ export const ActionBar = ({
|
||||
return Promise.resolve();
|
||||
}, Promise.resolve());
|
||||
|
||||
if (Object.keys(create || {}).length > 0) {
|
||||
Object.entries(create).forEach(([secretKey, secData]) => {
|
||||
if (create.length > 0) {
|
||||
create.forEach((secData) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
@@ -587,7 +590,7 @@ export const ActionBar = ({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
secretKey: secData.secretKey
|
||||
});
|
||||
});
|
||||
|
||||
@@ -603,8 +606,8 @@ export const ActionBar = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(update || {}).length > 0) {
|
||||
Object.entries(update).forEach(([secretKey, secData]) => {
|
||||
if (update.length > 0) {
|
||||
update.forEach((secData) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
@@ -616,7 +619,7 @@ export const ActionBar = ({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
secretKey: secData.secretKey
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1238,8 +1241,8 @@ export const ActionBar = ({
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||
?.map((key) => key)
|
||||
{(popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update
|
||||
?.map((sec) => sec.secretKey)
|
||||
.join(", ")}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
faSave
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
@@ -302,47 +303,61 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
<>
|
||||
{/* Floating Panel */}
|
||||
{!isModalOpen && (
|
||||
<div className="fixed bottom-4 left-1/2 z-40 w-full max-w-3xl -translate-x-1/2 self-center rounded-lg border border-yellow/30 bg-mineshaft-800 shadow-2xl lg:left-auto lg:translate-x-0">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500" />
|
||||
<span className="font-medium text-mineshaft-100">Pending Changes</span>
|
||||
<Badge variant="primary" className="text-xs">
|
||||
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
<div className="fixed bottom-4 left-1/2 z-40 w-full max-w-3xl -translate-x-1/2 self-center lg:left-auto lg:translate-x-0">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="commit-panel"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateY: 30 }}
|
||||
animate={{ opacity: 1, translateY: 0 }}
|
||||
exit={{ opacity: 0, translateY: -30 }}
|
||||
>
|
||||
<div className="rounded-lg border border-yellow/30 bg-mineshaft-800 shadow-2xl">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
{/* Left Content */}
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500" />
|
||||
<span className="font-medium text-mineshaft-100">Pending Changes</span>
|
||||
<Badge variant="primary" className="text-xs">
|
||||
{totalChangesCount} Change{totalChangesCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm leading-5 text-mineshaft-400">
|
||||
Review pending changes and commit them to apply the updates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right Buttons */}
|
||||
<div className="ml-6 mt-0.5 flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
clearAllPendingChanges({ workspaceId, environment, secretPath })
|
||||
}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
variant="outline_bg"
|
||||
className="px-4 hover:border-red/40 hover:bg-red/[0.1]"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
className="px-6"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm leading-5 text-mineshaft-400">
|
||||
Review pending changes and commit them to apply the updates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right Buttons */}
|
||||
<div className="ml-6 mt-0.5 flex items-center gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => clearAllPendingChanges({ workspaceId, environment, secretPath })}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
variant="outline_bg"
|
||||
className="px-4 hover:border-red/40 hover:bg-red/[0.1]"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
isDisabled={totalChangesCount === 0}
|
||||
className="px-6"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@@ -28,7 +28,7 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Logs</h3>
|
||||
{subscription.auditLogs && (
|
||||
<p className="text-xs text-bunker-300">
|
||||
Displaying audit logs from the last {auditLogsRetentionDays} days
|
||||
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -38,7 +38,9 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { syncId: secretSync.id },
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||
startDate: new Date(
|
||||
new Date().setDate(new Date().getDate() - Math.min(auditLogsRetentionDays, 60))
|
||||
),
|
||||
eventType: INTEGRATION_EVENTS
|
||||
}}
|
||||
/>
|
||||
|
@@ -1,23 +1,50 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
import {
|
||||
useRenderConnectionListEnvironmentGroups,
|
||||
useRenderConnectionListServices
|
||||
} from "@app/hooks/api/appConnections/render";
|
||||
import { RenderSyncScope, TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TRenderSync;
|
||||
};
|
||||
|
||||
export const RenderSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const isServiceScope = secretSync.destinationConfig.scope === RenderSyncScope.Service;
|
||||
|
||||
const { data: services = [], isPending } = useRenderConnectionListServices(
|
||||
secretSync.connectionId
|
||||
secretSync.connectionId,
|
||||
{
|
||||
enabled: isServiceScope
|
||||
}
|
||||
);
|
||||
const {
|
||||
destinationConfig: { serviceId }
|
||||
} = secretSync;
|
||||
|
||||
if (isPending) {
|
||||
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
|
||||
const { data: groups = [], isPending: isGroupsPending } =
|
||||
useRenderConnectionListEnvironmentGroups(secretSync.connectionId, { enabled: !isServiceScope });
|
||||
|
||||
switch (secretSync.destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
const id = secretSync.destinationConfig.serviceId;
|
||||
|
||||
if (isPending) {
|
||||
return <GenericFieldLabel label="Service">Loading...</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
const serviceName = services.find((service) => service.id === id)?.name;
|
||||
return <GenericFieldLabel label="Service">{serviceName ?? id}</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const id = secretSync.destinationConfig.environmentGroupId;
|
||||
|
||||
if (isGroupsPending) {
|
||||
return <GenericFieldLabel label="Environment Group">Loading...</GenericFieldLabel>;
|
||||
}
|
||||
|
||||
const envName = groups.find((g) => g.id === id)?.name;
|
||||
return <GenericFieldLabel label="Environment Group">{envName ?? id}</GenericFieldLabel>;
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown render sync destination scope");
|
||||
}
|
||||
|
||||
const serviceName = services.find((service) => service.id === serviceId)?.name;
|
||||
return <GenericFieldLabel label="Service">{serviceName ?? serviceId}</GenericFieldLabel>;
|
||||
};
|
||||
|
@@ -12,9 +12,15 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useUpdateWsEnvironment } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -35,6 +41,7 @@ type Props = {
|
||||
|
||||
export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const updateEnvironment = useUpdateWsEnvironment();
|
||||
|
||||
@@ -61,6 +68,16 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isMoreEnvironmentsAllowed =
|
||||
subscription?.environmentLimit && currentWorkspace?.environments
|
||||
? currentWorkspace.environments.length <= subscription.environmentLimit
|
||||
: true;
|
||||
|
||||
const environmentsOverPlanLimit =
|
||||
subscription?.environmentLimit && currentWorkspace?.environments
|
||||
? Math.max(0, currentWorkspace.environments.length - subscription.environmentLimit)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -122,18 +139,26 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug, id });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<Tooltip
|
||||
content={
|
||||
isMoreEnvironmentsAllowed
|
||||
? ""
|
||||
: `You have exceeded the number of environments allowed by your plan. To edit an existing environment, either upgrade your plan or remove at least ${environmentsOverPlanLimit} environment${environmentsOverPlanLimit === 1 ? "" : "s"}.`
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug, id });
|
||||
}}
|
||||
isDisabled={!isAllowed || !isMoreEnvironmentsAllowed}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
|
@@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.10.1
|
||||
version: v0.10.2
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.10.1"
|
||||
appVersion: "v0.10.2"
|
||||
|
@@ -316,6 +316,8 @@ spec:
|
||||
hostAPI:
|
||||
description: Infisical host to pull secrets from
|
||||
type: string
|
||||
instantUpdates:
|
||||
type: boolean
|
||||
managedKubeConfigMapReferences:
|
||||
items:
|
||||
properties:
|
||||
|
@@ -12,7 +12,7 @@ controllerManager:
|
||||
readOnlyRootFilesystem: true
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.10.1
|
||||
tag: v0.10.2
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
@@ -161,7 +161,7 @@ type InfisicalSecretSpec struct {
|
||||
// +kubebuilder:validation:Optional
|
||||
TLS TLSConfig `json:"tls"`
|
||||
|
||||
// +kubebuilder:default:=false
|
||||
// +kubebuilder:validation:Optional
|
||||
InstantUpdates bool `json:"instantUpdates"`
|
||||
}
|
||||
|
||||
|
@@ -315,7 +315,6 @@ spec:
|
||||
description: Infisical host to pull secrets from
|
||||
type: string
|
||||
instantUpdates:
|
||||
default: false
|
||||
type: boolean
|
||||
managedKubeConfigMapReferences:
|
||||
items:
|
||||
@@ -472,7 +471,6 @@ spec:
|
||||
- secretNamespace
|
||||
type: object
|
||||
required:
|
||||
- instantUpdates
|
||||
- resyncInterval
|
||||
type: object
|
||||
status:
|
||||
|
Reference in New Issue
Block a user