Merge branch 'main' into feat/azureClientSecretsRotation

This commit is contained in:
carlosmonastyrski
2025-04-21 10:27:56 -03:00
175 changed files with 2608 additions and 249 deletions
@@ -145,3 +145,9 @@ jobs:
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }} INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- name: Invalidate Cloudfront cache
run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}
+1 -1
View File
@@ -136,7 +136,7 @@ declare module "fastify" {
rateLimits: RateLimitConfiguration; rateLimits: RateLimitConfiguration;
// passport data // passport data
passportUser: { passportUser: {
isUserCompleted: string; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;
}; };
kmipUser: { kmipUser: {
@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
if (!hasCol) {
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
t.boolean("expiryNotificationSent").defaultTo(false);
});
// Update only tokens where expiresAt is before current time
await knex(TableName.ServiceToken)
.whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`)
.whereNotNull("expiresAt")
.update({ expiryNotificationSent: true });
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
if (hasCol) {
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
t.dropColumn("expiryNotificationSent");
});
}
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
if (!hasCol) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.boolean("hasDeleteProtection").defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
if (hasCol) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("hasDeleteProtection");
});
}
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) {
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
t.dropColumn("jwtSignatureAlgorithm");
});
}
}
@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled"))) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.boolean("bypassOrgAuthEnabled").defaultTo(false).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled")) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.dropColumn("bypassOrgAuthEnabled");
});
}
}
+3 -2
View File
@@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
lastUsed: z.date().nullable().optional(), lastUsed: z.date().nullable().optional(),
manageGroupMemberships: z.boolean().default(false),
encryptedOidcClientId: zodBuffer, encryptedOidcClientId: zodBuffer,
encryptedOidcClientSecret: zodBuffer encryptedOidcClientSecret: zodBuffer,
manageGroupMemberships: z.boolean().default(false),
jwtSignatureAlgorithm: z.string().default("RS256")
}); });
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>; export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
+2 -1
View File
@@ -26,7 +26,8 @@ export const OrganizationsSchema = z.object({
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(), allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true), shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional() privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false)
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+2 -1
View File
@@ -26,7 +26,8 @@ export const ProjectsSchema = z.object({
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string(), type: z.string(),
enforceCapitalization: z.boolean().default(false) enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(true).nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
+2 -1
View File
@@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
createdBy: z.string(), createdBy: z.string(),
projectId: z.string() projectId: z.string(),
expiryNotificationSent: z.boolean().default(false).nullable().optional()
}); });
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>; export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;
+12 -5
View File
@@ -12,7 +12,7 @@ import RedisStore from "connect-redis";
import { z } from "zod"; import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({
orgId: true, orgId: true,
isActive: true, isActive: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}); });
export const registerOidcRouter = async (server: FastifyZodProvider) => { export const registerOidcRouter = async (server: FastifyZodProvider) => {
@@ -170,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
isActive: true, isActive: true,
orgId: true, orgId: true,
allowedEmailDomains: true, allowedEmailDomains: true,
manageGroupMemberships: true manageGroupMemberships: true,
jwtSignatureAlgorithm: true
}).extend({ }).extend({
clientId: z.string(), clientId: z.string(),
clientSecret: z.string() clientSecret: z.string()
@@ -225,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientId: z.string().trim(), clientId: z.string().trim(),
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
manageGroupMemberships: z.boolean().optional() manageGroupMemberships: z.boolean().optional(),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ orgSlug: z.string() })),
@@ -292,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
clientSecret: z.string().trim(), clientSecret: z.string().trim(),
isActive: z.boolean(), isActive: z.boolean(),
orgSlug: z.string().trim(), orgSlug: z.string().trim(),
manageGroupMemberships: z.boolean().optional().default(false) manageGroupMemberships: z.boolean().optional().default(false),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.default(OIDCJWTSignatureAlgorithm.RS256)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {
+12 -6
View File
@@ -223,12 +223,18 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
samlConfigId: z.string().trim() samlConfigId: z.string().trim()
}) })
}, },
preValidation: passport.authenticate("saml", { preValidation: passport.authenticate(
session: false, "saml",
failureFlash: true, {
failureRedirect: "/login/provider/error" session: false
// this is due to zod type difference },
}) as any, async (req, res, err, user) => {
if (err) {
throw new BadRequestError({ message: `Saml authentication failed. ${err?.message}`, error: err });
}
req.passportUser = user as { isUserCompleted: boolean; providerAuthToken: string };
}
) as any, // this is due to zod type difference
handler: (req, res) => { handler: (req, res) => {
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
@@ -1,6 +1,5 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>; export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
@@ -8,22 +7,5 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
export const oidcConfigDALFactory = (db: TDbClient) => { export const oidcConfigDALFactory = (db: TDbClient) => {
const oidcCfgOrm = ormify(db, TableName.OidcConfig); const oidcCfgOrm = ormify(db, TableName.OidcConfig);
const findEnforceableOidcCfg = async (orgId: string) => { return oidcCfgOrm;
try {
const oidcCfg = await db
.replicaNode()(TableName.OidcConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return oidcCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return { ...oidcCfgOrm, findEnforceableOidcCfg };
}; };
@@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({
allowedEmailDomains: oidcCfg.allowedEmailDomains, allowedEmailDomains: oidcCfg.allowedEmailDomains,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships,
jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm
}; };
}; };
@@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({
jwksUri, jwksUri,
isActive, isActive,
lastUsed: null, lastUsed: null,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}; };
if (clientId !== undefined) { if (clientId !== undefined) {
@@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
clientSecret, clientSecret,
manageGroupMemberships manageGroupMemberships,
jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({
slug: orgSlug slug: orgSlug
@@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({
userinfoEndpoint, userinfoEndpoint,
orgId: org.id, orgId: org.id,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm,
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
}); });
@@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({
const client = new issuer.Client({ const client = new issuer.Client({
client_id: oidcCfg.clientId, client_id: oidcCfg.clientId,
client_secret: oidcCfg.clientSecret, client_secret: oidcCfg.clientSecret,
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`],
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
}); });
const strategy = new OpenIdStrategy( const strategy = new OpenIdStrategy(
@@ -5,6 +5,12 @@ export enum OIDCConfigurationType {
DISCOVERY_URL = "discoveryURL" DISCOVERY_URL = "discoveryURL"
} }
export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256",
HS256 = "HS256",
RS512 = "RS512"
}
export type TOidcLoginDTO = { export type TOidcLoginDTO = {
externalId: string; externalId: string;
email: string; email: string;
@@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = {
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
export type TUpdateOidcCfgDTO = Partial<{ export type TUpdateOidcCfgDTO = Partial<{
@@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{
isActive: boolean; isActive: boolean;
orgSlug: string; orgSlug: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &
TGenericPermission; TGenericPermission;
@@ -3,6 +3,7 @@ import { z } from "zod";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import {
IdentityProjectMembershipRoleSchema, IdentityProjectMembershipRoleSchema,
OrgMembershipRole,
OrgMembershipsSchema, OrgMembershipsSchema,
TableName, TableName,
TProjectRoles, TProjectRoles,
@@ -53,6 +54,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("groupId").withSchema("userGroups"), db.ref("groupId").withSchema("userGroups"),
db.ref("groupOrgId").withSchema("userGroups"), db.ref("groupOrgId").withSchema("userGroups"),
db.ref("groupName").withSchema("userGroups"), db.ref("groupName").withSchema("userGroups"),
@@ -71,6 +73,7 @@ export const permissionDALFactory = (db: TDbClient) => {
OrgMembershipsSchema.extend({ OrgMembershipsSchema.extend({
permissions: z.unknown(), permissions: z.unknown(),
orgAuthEnforced: z.boolean().optional().nullable(), orgAuthEnforced: z.boolean().optional().nullable(),
bypassOrgAuthEnabled: z.boolean(),
customRoleSlug: z.string().optional().nullable(), customRoleSlug: z.string().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean() shouldUseNewPrivilegeSystem: z.boolean()
}).parse(el), }).parse(el),
@@ -571,6 +574,11 @@ export const permissionDALFactory = (db: TDbClient) => {
}) })
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.join(TableName.OrgMembership, (qb) => {
void qb
.on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`);
})
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
@@ -670,6 +678,8 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId"), db.ref("id").withSchema(TableName.Project).as("projectId"),
@@ -683,6 +693,7 @@ export const permissionDALFactory = (db: TDbClient) => {
orgId, orgId,
username, username,
orgAuthEnforced, orgAuthEnforced,
orgRole,
membershipId, membershipId,
groupMembershipId, groupMembershipId,
membershipCreatedAt, membershipCreatedAt,
@@ -690,10 +701,12 @@ export const permissionDALFactory = (db: TDbClient) => {
groupMembershipUpdatedAt, groupMembershipUpdatedAt,
membershipUpdatedAt, membershipUpdatedAt,
projectType, projectType,
shouldUseNewPrivilegeSystem shouldUseNewPrivilegeSystem,
bypassOrgAuthEnabled
}) => ({ }) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
orgRole: orgRole as OrgMembershipRole,
userId, userId,
projectId, projectId,
username, username,
@@ -701,7 +714,8 @@ export const permissionDALFactory = (db: TDbClient) => {
id: membershipId || groupMembershipId, id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
shouldUseNewPrivilegeSystem shouldUseNewPrivilegeSystem,
bypassOrgAuthEnabled
}), }),
childrenMapper: [ childrenMapper: [
{ {
@@ -2,7 +2,7 @@
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { TOrganizations } from "@app/db/schemas"; import { OrgMembershipRole, TOrganizations } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
@@ -118,11 +118,20 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
].includes(actorAuthMethod); ].includes(actorAuthMethod);
} }
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { function validateOrgSSO(
actorAuthMethod: ActorAuthMethod,
isOrgSsoEnforced: TOrganizations["authEnforced"],
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
orgRole: OrgMembershipRole
) {
if (actorAuthMethod === undefined) { if (actorAuthMethod === undefined) {
throw new UnauthorizedError({ name: "No auth method defined" }); throw new UnauthorizedError({ name: "No auth method defined" });
} }
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
return;
}
if ( if (
isOrgSsoEnforced && isOrgSsoEnforced &&
actorAuthMethod !== null && actorAuthMethod !== null &&
@@ -139,7 +139,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSSO(authMethod, membership.orgAuthEnforced); validateOrgSSO(
authMethod,
membership.orgAuthEnforced,
membership.bypassOrgAuthEnabled,
membership.role as OrgMembershipRole
);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
membership?.groups?.map(({ role, customRolePermission }) => ({ membership?.groups?.map(({ role, customRolePermission }) => ({
@@ -226,7 +231,12 @@ export const permissionServiceFactory = ({
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); validateOrgSSO(
authMethod,
userProjectPermission.orgAuthEnforced,
userProjectPermission.bypassOrgAuthEnabled,
userProjectPermission.orgRole
);
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
throw new BadRequestError({ throw new BadRequestError({
@@ -1,6 +1,5 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>; export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
@@ -8,25 +7,5 @@ export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
export const samlConfigDALFactory = (db: TDbClient) => { export const samlConfigDALFactory = (db: TDbClient) => {
const samlCfgOrm = ormify(db, TableName.SamlConfig); const samlCfgOrm = ormify(db, TableName.SamlConfig);
const findEnforceableSamlCfg = async (orgId: string) => { return samlCfgOrm;
try {
const samlCfg = await db
.replicaNode()(TableName.SamlConfig)
.where({
orgId,
isActive: true
})
.whereNotNull("lastUsed")
.first();
return samlCfg;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
return {
...samlCfgOrm,
findEnforceableSamlCfg
};
}; };
+10 -1
View File
@@ -478,7 +478,8 @@ export const PROJECTS = {
name: "The new name of the project.", name: "The new name of the project.",
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)" slug: "An optional slug for the project. (must be unique within the organization)",
hasDeleteProtection: "Enable or disable delete protection for the project."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -1807,6 +1808,10 @@ export const AppConnections = {
CAMUNDA: { CAMUNDA: {
clientId: "The client ID used to authenticate with Camunda.", clientId: "The client ID used to authenticate with Camunda.",
clientSecret: "The client secret used to authenticate with Camunda." clientSecret: "The client secret used to authenticate with Camunda."
},
WINDMILL: {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
} }
} }
}; };
@@ -1942,6 +1947,10 @@ export const SecretSyncs = {
env: "The ID of the Vercel environment to sync secrets to.", env: "The ID of the Vercel environment to sync secrets to.",
branch: "The branch to sync preview secrets to.", branch: "The branch to sync preview secrets to.",
teamId: "The ID of the Vercel team to sync secrets to." teamId: "The ID of the Vercel team to sync secrets to."
},
WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to."
} }
} }
}; };
+4 -2
View File
@@ -1255,7 +1255,8 @@ export const registerRoutes = async (
userDAL, userDAL,
permissionService, permissionService,
projectDAL, projectDAL,
accessTokenQueue accessTokenQueue,
smtpService
}); });
const identityService = identityServiceFactory({ const identityService = identityServiceFactory({
@@ -1416,7 +1417,8 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL, secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
serviceTokenService
}); });
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({ const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
@@ -260,7 +260,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
upgradeStatus: true, upgradeStatus: true,
pitVersionLimit: true, pitVersionLimit: true,
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true auditLogsRetentionDays: true,
hasDeleteProtection: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({ export const SanitizedTagSchema = SecretTagsSchema.pick({
@@ -41,6 +41,10 @@ import {
TerraformCloudConnectionListItemSchema TerraformCloudConnectionListItemSchema
} from "@app/services/app-connection/terraform-cloud"; } from "@app/services/app-connection/terraform-cloud";
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel"; import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
import {
SanitizedWindmillConnectionSchema,
WindmillConnectionListItemSchema
} from "@app/services/app-connection/windmill";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps // can't use discriminated due to multiple schemas for certain apps
@@ -58,7 +62,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMsSqlConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options, ...SanitizedAuth0ConnectionSchema.options,
...SanitizedAzureClientSecretsConnectionSchema.options ...SanitizedAzureClientSecretsConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -75,7 +80,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MsSqlConnectionListItemSchema, MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema, CamundaConnectionListItemSchema,
Auth0ConnectionListItemSchema, Auth0ConnectionListItemSchema,
AzureClientSecretsConnectionListItemSchema AzureClientSecretsConnectionListItemSchema,
WindmillConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -14,6 +14,7 @@ import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router";
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
export * from "./app-connection-router"; export * from "./app-connection-router";
@@ -32,5 +33,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.MsSql]: registerMsSqlConnectionRouter, [AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter, [AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter, [AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter [AppConnection.Auth0]: registerAuth0ConnectionRouter
}; };
@@ -0,0 +1,53 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateWindmillConnectionSchema,
SanitizedWindmillConnectionSchema,
UpdateWindmillConnectionSchema
} from "@app/services/app-connection/windmill";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerWindmillConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Windmill,
server,
sanitizedResponseSchema: SanitizedWindmillConnectionSchema,
createSchema: CreateWindmillConnectionSchema,
updateSchema: UpdateWindmillConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/workspaces`,
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 workspaces = await server.services.appConnection.windmill.listWorkspaces(connectionId, req.permission);
return workspaces;
}
});
};
@@ -31,7 +31,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
organizations: sanitizedOrganizationSchema organizations: sanitizedOrganizationSchema
.extend({ .extend({
orgAuthMethod: z.string() orgAuthMethod: z.string(),
userRole: z.string()
}) })
.array() .array()
}) })
@@ -259,7 +260,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(), defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
enforceMfa: z.boolean().optional(), enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(), selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
allowSecretSharingOutsideOrganization: z.boolean().optional() allowSecretSharingOutsideOrganization: z.boolean().optional(),
bypassOrgAuthEnabled: z.boolean().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -312,6 +312,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.optional() .optional()
.describe(PROJECTS.UPDATE.projectDescription), .describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization), autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection),
slug: z slug: z
.string() .string()
.trim() .trim()
@@ -340,6 +341,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization, autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug slug: req.body.slug
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -390,6 +392,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/:workspaceId/delete-protection",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
hasDeleteProtection: z.boolean()
}),
response: {
200: z.object({
message: z.string(),
workspace: SanitizedProjectSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.toggleDeleteProtection({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
hasDeleteProtection: req.body.hasDeleteProtection
});
return {
message: "Successfully changed workspace settings",
workspace
};
}
});
server.route({ server.route({
method: "PUT", method: "PUT",
url: "/:workspaceSlug/version-limit", url: "/:workspaceSlug/version-limit",
@@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path), .describe(FOLDERS.CREATE.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory), .describe(FOLDERS.CREATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
}), }),
response: { response: {
@@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.createFolder({ const folder = await server.services.folder.createFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path), .describe(FOLDERS.UPDATE.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory), .describe(FOLDERS.UPDATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}), }),
response: { response: {
@@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const { folder, old } = await server.services.folder.updateFolder({ const { folder, old } = await server.services.folder.updateFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path), .describe(FOLDERS.DELETE.path)
.optional(),
// keep this here as cli need directory // keep this here as cli need directory
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .default("/")
.transform(prefixWithSlash) .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.directory) .describe(FOLDERS.DELETE.directory)
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory; const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.deleteFolder({ const folder = await server.services.folder.deleteFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
path: z path: z
.string() .string()
.trim() .trim()
.default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path), .describe(FOLDERS.LIST.path)
.optional(),
// backward compatiability with cli // backward compatiability with cli
directory: z directory: z
.string() .string()
.trim() .trim()
.default("/") .transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(prefixWithSlash)
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory), .describe(FOLDERS.LIST.directory)
.optional(),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}), }),
response: { response: {
@@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.query.path || req.query.directory; const path = req.query.path || req.query.directory || "/";
const folders = await server.services.folder.getFolders({ const folders = await server.services.folder.getFolders({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -11,6 +11,7 @@ import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
import { registerWindmillSyncRouter } from "./windmill-sync-router";
export * from "./secret-sync-router"; export * from "./secret-sync-router";
@@ -25,5 +26,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Humanitec]: registerHumanitecSyncRouter, [SecretSync.Humanitec]: registerHumanitecSyncRouter,
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter, [SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
[SecretSync.Camunda]: registerCamundaSyncRouter, [SecretSync.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter [SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter
}; };
@@ -25,6 +25,7 @@ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
const SecretSyncSchema = z.discriminatedUnion("destination", [ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema, AwsParameterStoreSyncSchema,
@@ -37,7 +38,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
HumanitecSyncSchema, HumanitecSyncSchema,
TerraformCloudSyncSchema, TerraformCloudSyncSchema,
CamundaSyncSchema, CamundaSyncSchema,
VercelSyncSchema VercelSyncSchema,
WindmillSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -51,7 +53,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
HumanitecSyncListItemSchema, HumanitecSyncListItemSchema,
TerraformCloudSyncListItemSchema, TerraformCloudSyncListItemSchema,
CamundaSyncListItemSchema, CamundaSyncListItemSchema,
VercelSyncListItemSchema VercelSyncListItemSchema,
WindmillSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,17 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
CreateWindmillSyncSchema,
UpdateWindmillSyncSchema,
WindmillSyncSchema
} from "@app/services/secret-sync/windmill";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerWindmillSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Windmill,
server,
responseSchema: WindmillSyncSchema,
createSchema: CreateWindmillSyncSchema,
updateSchema: UpdateWindmillSyncSchema
});
@@ -303,7 +303,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription), description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection)
}), }),
response: { response: {
200: SanitizedProjectSchema 200: SanitizedProjectSchema
@@ -321,7 +322,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: { update: {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection
}, },
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -12,6 +12,7 @@ export enum AppConnection {
Postgres = "postgres", Postgres = "postgres",
MsSql = "mssql", MsSql = "mssql",
Camunda = "camunda", Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0" Auth0 = "auth0"
} }
@@ -55,6 +55,11 @@ import {
} from "./terraform-cloud"; } from "./terraform-cloud";
import { VercelConnectionMethod } from "./vercel"; import { VercelConnectionMethod } from "./vercel";
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns"; import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
import {
getWindmillConnectionListItem,
validateWindmillConnectionCredentials,
WindmillConnectionMethod
} from "./windmill";
export const listAppConnectionOptions = () => { export const listAppConnectionOptions = () => {
return [ return [
@@ -71,6 +76,7 @@ export const listAppConnectionOptions = () => {
getMsSqlConnectionListItem(), getMsSqlConnectionListItem(),
getCamundaConnectionListItem(), getCamundaConnectionListItem(),
getAzureClientSecretsConnectionListItem(), getAzureClientSecretsConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem() getAuth0ConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -136,7 +142,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureClientSecrets]: [AppConnection.AzureClientSecrets]:
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -168,6 +175,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password"; return "Username & Password";
case WindmillConnectionMethod.AccessToken:
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials: case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials"; return "Client Credentials";
default: default:
@@ -214,5 +223,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported, [AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported [AppConnection.Auth0]: platformManagedCredentialsNotSupported
}; };
@@ -14,5 +14,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Postgres]: "PostgreSQL", [AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda", [AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0" [AppConnection.Auth0]: "Auth0"
}; };
@@ -51,6 +51,8 @@ import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-c
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service"; import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
import { ValidateVercelConnectionCredentialsSchema } from "./vercel"; import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
import { vercelConnectionService } from "./vercel/vercel-connection-service"; import { vercelConnectionService } from "./vercel/vercel-connection-service";
import { ValidateWindmillConnectionCredentialsSchema } from "./windmill";
import { windmillConnectionService } from "./windmill/windmill-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
@@ -74,6 +76,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema, [AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema, [AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema, [AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema [AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
}; };
@@ -450,6 +453,7 @@ export const appConnectionServiceFactory = ({
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById), vercel: vercelConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
windmill: windmillConnectionService(connectAppConnectionById)
}; };
}; };
@@ -81,6 +81,12 @@ import {
TVercelConnectionConfig, TVercelConnectionConfig,
TVercelConnectionInput TVercelConnectionInput
} from "./vercel"; } from "./vercel";
import {
TValidateWindmillConnectionCredentialsSchema,
TWindmillConnection,
TWindmillConnectionConfig,
TWindmillConnectionInput
} from "./windmill";
export type TAppConnection = { id: string } & ( export type TAppConnection = { id: string } & (
| TAwsConnection | TAwsConnection
@@ -96,6 +102,7 @@ export type TAppConnection = { id: string } & (
| TMsSqlConnection | TMsSqlConnection
| TCamundaConnection | TCamundaConnection
| TAzureClientSecretsConnection | TAzureClientSecretsConnection
| TWindmillConnection
| TAuth0Connection | TAuth0Connection
); );
@@ -117,6 +124,7 @@ export type TAppConnectionInput = { id: string } & (
| TMsSqlConnectionInput | TMsSqlConnectionInput
| TCamundaConnectionInput | TCamundaConnectionInput
| TAzureClientSecretsConnectionInput | TAzureClientSecretsConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput | TAuth0ConnectionInput
); );
@@ -144,6 +152,7 @@ export type TAppConnectionConfig =
| TCamundaConnectionConfig | TCamundaConnectionConfig
| TAzureClientSecretsConnectionConfig | TAzureClientSecretsConnectionConfig
| TVercelConnectionConfig | TVercelConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig; | TAuth0ConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
@@ -160,6 +169,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateVercelConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema | TValidateTerraformCloudConnectionCredentialsSchema
| TValidateAzureClientSecretsConnectionCredentialsSchema | TValidateAzureClientSecretsConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema; | TValidateAuth0ConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
@@ -44,7 +44,7 @@ export const validateTerraformCloudConnectionCredentials = async (config: TTerra
}); });
} }
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to validate connection - verify credentials" message: "Unable to validate connection: verify credentials"
}); });
} }
@@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { AxiosError, AxiosResponse } from "axios"; import { AxiosError } from "axios";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -27,10 +27,8 @@ export const getVercelConnectionListItem = () => {
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => { export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
const { credentials: inputCredentials } = config; const { credentials: inputCredentials } = config;
let response: AxiosResponse<VercelApp[]> | null = null;
try { try {
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, { await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
headers: { headers: {
Authorization: `Bearer ${inputCredentials.apiToken}` Authorization: `Bearer ${inputCredentials.apiToken}`
} }
@@ -38,17 +36,14 @@ export const validateVercelConnectionCredentials = async (config: TVercelConnect
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}` // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to validate credentials: ${
error.response?.data ? JSON.stringify(error.response?.data) : error.message || "Unknown error"
}`
}); });
} }
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to validate connection - verify credentials" message: `Unable to validate connection: ${(error as Error).message || "Verify credentials"}`
});
}
if (!response?.data) {
throw new InternalServerError({
message: "Failed to get organizations: Response was empty"
}); });
} }
@@ -0,0 +1,4 @@
export * from "./windmill-connection-enums";
export * from "./windmill-connection-fns";
export * from "./windmill-connection-schemas";
export * from "./windmill-connection-types";
@@ -0,0 +1,3 @@
export enum WindmillConnectionMethod {
AccessToken = "access-token"
}
@@ -0,0 +1,65 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { WindmillConnectionMethod } from "./windmill-connection-enums";
import { TWindmillConnection, TWindmillConnectionConfig, TWindmillWorkspace } from "./windmill-connection-types";
export const getWindmillInstanceUrl = async (config: TWindmillConnectionConfig) => {
const instanceUrl = config.credentials.instanceUrl
? removeTrailingSlash(config.credentials.instanceUrl)
: "https://app.windmill.dev";
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export const getWindmillConnectionListItem = () => {
return {
name: "Windmill" as const,
app: AppConnection.Windmill as const,
methods: Object.values(WindmillConnectionMethod) as [WindmillConnectionMethod.AccessToken]
};
};
export const validateWindmillConnectionCredentials = async (config: TWindmillConnectionConfig) => {
const instanceUrl = await getWindmillInstanceUrl(config);
const { accessToken } = config.credentials;
try {
await request.get(`${instanceUrl}/api/workspaces/list`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
export const listWindmillWorkspaces = async (appConnection: TWindmillConnection) => {
const instanceUrl = await getWindmillInstanceUrl(appConnection);
const { accessToken } = appConnection.credentials;
const resp = await request.get<TWindmillWorkspace[]>(`${instanceUrl}/api/workspaces/list`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return resp.data.filter((workspace) => !workspace.deleted);
};
@@ -0,0 +1,70 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { WindmillConnectionMethod } from "./windmill-connection-enums";
export const WindmillConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.describe(AppConnections.CREDENTIALS.WINDMILL.accessToken),
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.optional()
.describe(AppConnections.CREDENTIALS.WINDMILL.instanceUrl)
});
const BaseWindmillConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Windmill) });
export const WindmillConnectionSchema = BaseWindmillConnectionSchema.extend({
method: z.literal(WindmillConnectionMethod.AccessToken),
credentials: WindmillConnectionAccessTokenCredentialsSchema
});
export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method", [
BaseWindmillConnectionSchema.extend({
method: z.literal(WindmillConnectionMethod.AccessToken),
credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(WindmillConnectionMethod.AccessToken)
.describe(AppConnections.CREATE(AppConnection.Windmill).method),
credentials: WindmillConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Windmill).credentials
)
})
]);
export const CreateWindmillConnectionSchema = ValidateWindmillConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Windmill)
);
export const UpdateWindmillConnectionSchema = z
.object({
credentials: WindmillConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Windmill).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill));
export const WindmillConnectionListItemSchema = z.object({
name: z.literal("Windmill"),
app: z.literal(AppConnection.Windmill),
methods: z.nativeEnum(WindmillConnectionMethod).array()
});
@@ -0,0 +1,28 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listWindmillWorkspaces } from "./windmill-connection-fns";
import { TWindmillConnection } from "./windmill-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TWindmillConnection>;
export const windmillConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Windmill, connectionId, actor);
try {
const workspaces = await listWindmillWorkspaces(appConnection);
return workspaces;
} catch (error) {
return [];
}
};
return {
listWorkspaces
};
};
@@ -0,0 +1,27 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateWindmillConnectionSchema,
ValidateWindmillConnectionCredentialsSchema,
WindmillConnectionSchema
} from "./windmill-connection-schemas";
export type TWindmillConnection = z.infer<typeof WindmillConnectionSchema>;
export type TWindmillConnectionInput = z.infer<typeof CreateWindmillConnectionSchema> & {
app: AppConnection.Windmill;
};
export type TValidateWindmillConnectionCredentialsSchema = typeof ValidateWindmillConnectionCredentialsSchema;
export type TWindmillConnectionConfig = DiscriminativePick<
TWindmillConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TWindmillWorkspace = { id: string; name: string; deleted: boolean };
+37 -20
View File
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; import { Knex } from "knex";
import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
@@ -174,20 +174,25 @@ export const authLoginServiceFactory = ({
const userEnc = await userDAL.findUserEncKeyByUsername({ const userEnc = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if ( if (
serverCfg.enabledLoginMethods && serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
!providerAuthToken !providerAuthToken
) { ) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with email is disabled by administrator." const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId);
}); if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
} throw new BadRequestError({
message: "Login with email is disabled by administrator."
if (!userEnc || (userEnc && !userEnc.isAccepted)) { });
throw new Error("Failed to find user"); }
} }
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
@@ -573,28 +578,40 @@ export const authLoginServiceFactory = ({
switch (authMethod) { switch (authMethod) {
case AuthMethod.GITHUB: { case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Github is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Github is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }
case AuthMethod.GOOGLE: { case AuthMethod.GOOGLE: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Google is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Google is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }
case AuthMethod.GITLAB: { case AuthMethod.GITLAB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
throw new BadRequestError({ // bypass server configuration when user is an organization admin - this is to prevent lockout
message: "Login with Gitlab is disabled by administrator.", const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
name: "Oauth 2 login" if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
}); throw new BadRequestError({
message: "Login with Gitlab is disabled by administrator.",
name: "Oauth 2 login"
});
}
} }
break; break;
} }
+5 -2
View File
@@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { const findAllOrgsByUserId = async (
userId: string
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => {
try { try {
const org = (await db const org = (await db
.replicaNode()(TableName.OrgMembership) .replicaNode()(TableName.OrgMembership)
@@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
); );
}) })
.select(selectAllTableCols(TableName.Organization)) .select(selectAllTableCols(TableName.Organization))
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
.select( .select(
db.raw(` db.raw(`
CASE CASE
@@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => {
ELSE '' ELSE ''
END as "orgAuthMethod" END as "orgAuthMethod"
`) `)
)) as (TOrganizations & { orgAuthMethod: string })[]; )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[];
return org; return org;
} catch (error) { } catch (error) {
+2 -1
View File
@@ -16,5 +16,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
allowSecretSharingOutsideOrganization: true, allowSecretSharingOutsideOrganization: true,
shouldUseNewPrivilegeSystem: true, shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedByUsername: true, privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true
}); });
+28 -6
View File
@@ -110,8 +110,8 @@ type TOrgServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">; oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
smtpService: TSmtpService; smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
@@ -349,7 +349,8 @@ export const orgServiceFactory = ({
defaultMembershipRoleSlug, defaultMembershipRoleSlug,
enforceMfa, enforceMfa,
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
} }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -402,13 +403,33 @@ export const orgServiceFactory = ({
} }
if (authEnforced) { if (authEnforced) {
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); const samlCfg = await samlConfigDAL.findOne({
const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId); orgId,
isActive: true
});
const oidcCfg = await oidcConfigDAL.findOne({
orgId,
isActive: true
});
if (!samlCfg && !oidcCfg) if (!samlCfg && !oidcCfg)
throw new NotFoundError({ throw new NotFoundError({
message: `SAML or OIDC configuration for organization with ID '${orgId}' not found` message: `SAML or OIDC configuration for organization with ID '${orgId}' not found`
}); });
if (samlCfg && !samlCfg.lastUsed) {
throw new BadRequestError({
message:
"To apply the new SAML auth enforcement, please log in via SAML at least once. This step is required to enforce SAML-based authentication."
});
}
if (oidcCfg && !oidcCfg.lastUsed) {
throw new BadRequestError({
message:
"To apply the new OIDC auth enforcement, please log in via OIDC at least once. This step is required to enforce OIDC-based authentication."
});
}
} }
let defaultMembershipRole: string | undefined; let defaultMembershipRole: string | undefined;
@@ -429,7 +450,8 @@ export const orgServiceFactory = ({
defaultMembershipRole, defaultMembershipRole,
enforceMfa, enforceMfa,
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;
+1
View File
@@ -73,6 +73,7 @@ export type TUpdateOrgDTO = {
enforceMfa: boolean; enforceMfa: boolean;
selectedMfaMethod: MfaMethod; selectedMfaMethod: MfaMethod;
allowSecretSharingOutsideOrganization: boolean; allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean;
}>; }>;
} & TOrgPermission; } & TOrgPermission;
@@ -86,6 +86,7 @@ import {
TProjectAccessRequestDTO, TProjectAccessRequestDTO,
TSearchProjectsDTO, TSearchProjectsDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TToggleProjectDeleteProtectionDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO, TUpdateProjectKmsDTO,
@@ -482,6 +483,12 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
if (project.hasDeleteProtection) {
throw new ForbiddenRequestError({
message: "Project delete protection is enabled"
});
}
const deletedProject = await projectDAL.transaction(async (tx) => { const deletedProject = await projectDAL.transaction(async (tx) => {
// delete these so that project custom roles can be deleted in cascade effect // delete these so that project custom roles can be deleted in cascade effect
// direct deletion of project without these will cause fk error // direct deletion of project without these will cause fk error
@@ -616,6 +623,7 @@ export const projectServiceFactory = ({
description: update.description, description: update.description,
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization, enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug slug: update.slug
}); });
@@ -648,6 +656,29 @@ export const projectServiceFactory = ({
return updatedProject; return updatedProject;
}; };
const toggleDeleteProtection = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
hasDeleteProtection
}: TToggleProjectDeleteProtectionDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection });
return updatedProject;
};
const updateVersionLimit = async ({ const updateVersionLimit = async ({
actor, actor,
actorId, actorId,
@@ -1499,6 +1530,7 @@ export const projectServiceFactory = ({
getProjectUpgradeStatus, getProjectUpgradeStatus,
getAProject, getAProject,
toggleAutoCapitalization, toggleAutoCapitalization,
toggleDeleteProtection,
updateName, updateName,
upgradeProject, upgradeProject,
listProjectCas, listProjectCas,
@@ -66,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = {
autoCapitalization: boolean; autoCapitalization: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TToggleProjectDeleteProtectionDTO = {
hasDeleteProtection: boolean;
} & TProjectPermission;
export type TUpdateProjectVersionLimitDTO = { export type TUpdateProjectVersionLimitDTO = {
pitVersionLimit: number; pitVersionLimit: number;
workspaceSlug: string; workspaceSlug: string;
@@ -86,6 +90,7 @@ export type TUpdateProjectDTO = {
name?: string; name?: string;
description?: string; description?: string;
autoCapitalization?: boolean; autoCapitalization?: boolean;
hasDeleteProtection?: boolean;
slug?: string; slug?: string;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TServiceTokenServiceFactory } from "../service-token/service-token-service";
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
@@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
}; };
@@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL, secretVersionV2DAL,
identityUniversalAuthClientSecretDAL identityUniversalAuthClientSecretDAL,
serviceTokenService
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
@@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -819,10 +819,14 @@ export const secretImportServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( if (
ProjectPermissionActions.Read, permission.cannot(
subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ProjectPermissionActions.Read,
); subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
)
) {
return [];
}
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return []; if (!folder) return [];
@@ -9,7 +9,8 @@ export enum SecretSync {
Humanitec = "humanitec", Humanitec = "humanitec",
TerraformCloud = "terraform-cloud", TerraformCloud = "terraform-cloud",
Camunda = "camunda", Camunda = "camunda",
Vercel = "vercel" Vercel = "vercel",
Windmill = "windmill"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {
@@ -29,6 +29,7 @@ import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = { const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -41,7 +42,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION
}; };
export const listSecretSyncOptions = () => { export const listSecretSyncOptions = () => {
@@ -136,6 +138,8 @@ export const SecretSyncFns = {
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, secretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap); return VercelSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -192,6 +196,9 @@ export const SecretSyncFns = {
case SecretSync.Vercel: case SecretSync.Vercel:
secretMap = await VercelSyncFns.getSecrets(secretSync); secretMap = await VercelSyncFns.getSecrets(secretSync);
break; break;
case SecretSync.Windmill:
secretMap = await WindmillSyncFns.getSecrets(secretSync);
break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -243,6 +250,8 @@ export const SecretSyncFns = {
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, secretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap); return VercelSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -12,7 +12,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Humanitec]: "Humanitec", [SecretSync.Humanitec]: "Humanitec",
[SecretSync.TerraformCloud]: "Terraform Cloud", [SecretSync.TerraformCloud]: "Terraform Cloud",
[SecretSync.Camunda]: "Camunda", [SecretSync.Camunda]: "Camunda",
[SecretSync.Vercel]: "Vercel" [SecretSync.Vercel]: "Vercel",
[SecretSync.Windmill]: "Windmill"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -26,5 +27,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Humanitec]: AppConnection.Humanitec, [SecretSync.Humanitec]: AppConnection.Humanitec,
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
[SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel [SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill
}; };
@@ -76,6 +76,7 @@ type TSecretSyncQueueFactoryDep = {
| "findBySecretKeys" | "findBySecretKeys"
| "bulkUpdate" | "bulkUpdate"
| "deleteMany" | "deleteMany"
| "invalidateSecretCacheByProjectId"
>; >;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">; secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
@@ -382,6 +383,9 @@ export const secretSyncQueueFactory = ({
}); });
} }
if (secretsToUpdate.length || secretsToCreate.length)
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
return importedSecretMap; return importedSecretMap;
}; };
@@ -29,6 +29,12 @@ import {
} from "@app/services/secret-sync/github"; } from "@app/services/secret-sync/github";
import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal"; import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
import {
TWindmillSync,
TWindmillSyncInput,
TWindmillSyncListItem,
TWindmillSyncWithCredentials
} from "@app/services/secret-sync/windmill";
import { import {
TAwsParameterStoreSync, TAwsParameterStoreSync,
@@ -74,7 +80,8 @@ export type TSecretSync =
| THumanitecSync | THumanitecSync
| TTerraformCloudSync | TTerraformCloudSync
| TCamundaSync | TCamundaSync
| TVercelSync; | TVercelSync
| TWindmillSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -87,7 +94,8 @@ export type TSecretSyncWithCredentials =
| THumanitecSyncWithCredentials | THumanitecSyncWithCredentials
| TTerraformCloudSyncWithCredentials | TTerraformCloudSyncWithCredentials
| TCamundaSyncWithCredentials | TCamundaSyncWithCredentials
| TVercelSyncWithCredentials; | TVercelSyncWithCredentials
| TWindmillSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -100,7 +108,8 @@ export type TSecretSyncInput =
| THumanitecSyncInput | THumanitecSyncInput
| TTerraformCloudSyncInput | TTerraformCloudSyncInput
| TCamundaSyncInput | TCamundaSyncInput
| TVercelSyncInput; | TVercelSyncInput
| TWindmillSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -113,7 +122,8 @@ export type TSecretSyncListItem =
| THumanitecSyncListItem | THumanitecSyncListItem
| TTerraformCloudSyncListItem | TTerraformCloudSyncListItem
| TCamundaSyncListItem | TCamundaSyncListItem
| TVercelSyncListItem; | TVercelSyncListItem
| TWindmillSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;
@@ -0,0 +1,4 @@
export * from "./windmill-sync-constants";
export * from "./windmill-sync-fns";
export * from "./windmill-sync-schemas";
export * from "./windmill-sync-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const WINDMILL_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Windmill",
destination: SecretSync.Windmill,
connection: AppConnection.Windmill,
canImportSecrets: true
};
@@ -0,0 +1,241 @@
import { request } from "@app/lib/config/request";
import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import {
TDeleteWindmillVariable,
TPostWindmillVariable,
TWindmillListVariables,
TWindmillListVariablesResponse,
TWindmillSyncWithCredentials,
TWindmillVariable
} from "@app/services/secret-sync/windmill/windmill-sync-types";
import { TSecretMap } from "../secret-sync-types";
const PAGE_LIMIT = 100;
const listWindmillVariables = async ({ instanceUrl, workspace, accessToken, path }: TWindmillListVariables) => {
const variables: Record<string, TWindmillVariable> = {};
// windmill paginates but doesn't return if there's more pages so we need to check if page size full
let page: number | null = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
const { data: variablesPage } = await request.get<TWindmillListVariablesResponse>(
`${instanceUrl}/api/w/${workspace}/variables/list`,
{
headers: {
Authorization: `Bearer ${accessToken}`
},
params: {
page,
limit: PAGE_LIMIT,
path_start: path
}
}
);
for (const variable of variablesPage) {
const variableName = variable.path.replace(path, "");
if (variable.is_secret) {
// eslint-disable-next-line no-await-in-loop
const { data: variableValue } = await request.get<string>(
`${instanceUrl}/api/w/${workspace}/variables/get_value/${variable.path}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
variables[variableName] = {
...variable,
value: variableValue
};
} else {
variables[variableName] = variable;
}
}
if (variablesPage.length >= PAGE_LIMIT) {
page += 1;
} else {
page = null;
}
}
return variables;
};
const createWindmillVariable = async ({
path,
value,
instanceUrl,
accessToken,
workspace,
description
}: TPostWindmillVariable) =>
request.post(
`${instanceUrl}/api/w/${workspace}/variables/create`,
{
path,
value,
is_secret: true,
description
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
const updateWindmillVariable = async ({
path,
value,
instanceUrl,
accessToken,
workspace,
description
}: TPostWindmillVariable) =>
request.post(
`${instanceUrl}/api/w/${workspace}/variables/update/${path}`,
{
value,
is_secret: true,
description
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
const deleteWindmillVariable = async ({ path, instanceUrl, accessToken, workspace }: TDeleteWindmillVariable) =>
request.delete(`${instanceUrl}/api/w/${workspace}/variables/delete/${path}`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
export const WindmillSyncFns = {
syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { path },
syncOptions: { disableSecretDeletion }
} = secretSync;
// url needs to be lowercase
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
const instanceUrl = await getWindmillInstanceUrl(connection);
const { accessToken } = connection.credentials;
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
for await (const entry of Object.entries(secretMap)) {
const [key, { value, comment = "" }] = entry;
try {
const payload = {
instanceUrl,
workspace,
path: path + key,
value,
accessToken,
description: comment
};
if (key in variables) {
if (variables[key].value !== value || variables[key].description !== comment)
await updateWindmillVariable(payload);
} else {
await createWindmillVariable(payload);
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(variables)) {
if (!(key in secretMap)) {
try {
await deleteWindmillVariable({
instanceUrl,
workspace,
path: variable.path,
accessToken
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
removeSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { path }
} = secretSync;
// url needs to be lowercase
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
const instanceUrl = await getWindmillInstanceUrl(connection);
const { accessToken } = connection.credentials;
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
for await (const [key, variable] of Object.entries(variables)) {
if (key in secretMap) {
try {
await deleteWindmillVariable({
path: variable.path,
instanceUrl,
workspace,
accessToken
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
getSecrets: async (secretSync: TWindmillSyncWithCredentials) => {
const {
connection,
destinationConfig: { path }
} = secretSync;
// url needs to be lowercase
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
const instanceUrl = await getWindmillInstanceUrl(connection);
const { accessToken } = connection.credentials;
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
return Object.fromEntries(
Object.entries(variables).map(([key, variable]) => [key, { value: variable.value ?? "" }])
);
}
};
@@ -0,0 +1,66 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const pathCharacterValidator = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Underscore,
CharacterType.Hyphen
]);
const WindmillSyncDestinationConfigSchema = z.object({
workspace: z.string().trim().min(1, "Workspace required").describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.workspace),
path: z
.string()
.trim()
.min(1, "Path required")
.refine(
(val) =>
(val.startsWith("u/") || val.startsWith("f/")) &&
val.endsWith("/") &&
val.split("/").length >= 3 &&
val
.split("/")
.slice(0, -1) // Remove last empty segment from trailing slash
.every((segment) => segment && pathCharacterValidator(segment)),
'Invalid path - must follow Windmill path format. ex: "f/folder/path/"'
)
.describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.path)
});
const WindmillSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const WindmillSyncSchema = BaseSecretSyncSchema(SecretSync.Windmill, WindmillSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Windmill),
destinationConfig: WindmillSyncDestinationConfigSchema
});
export const CreateWindmillSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Windmill,
WindmillSyncOptionsConfig
).extend({
destinationConfig: WindmillSyncDestinationConfigSchema
});
export const UpdateWindmillSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Windmill,
WindmillSyncOptionsConfig
).extend({
destinationConfig: WindmillSyncDestinationConfigSchema.optional()
});
export const WindmillSyncListItemSchema = z.object({
name: z.literal("Windmill"),
connection: z.literal(AppConnection.Windmill),
destination: z.literal(SecretSync.Windmill),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,39 @@
import { z } from "zod";
import { TWindmillConnection } from "@app/services/app-connection/windmill";
import { CreateWindmillSyncSchema, WindmillSyncListItemSchema, WindmillSyncSchema } from "./windmill-sync-schemas";
export type TWindmillSync = z.infer<typeof WindmillSyncSchema>;
export type TWindmillSyncInput = z.infer<typeof CreateWindmillSyncSchema>;
export type TWindmillSyncListItem = z.infer<typeof WindmillSyncListItemSchema>;
export type TWindmillSyncWithCredentials = TWindmillSync & {
connection: TWindmillConnection;
};
export type TWindmillVariable = {
path: string;
value: string;
is_secret: boolean;
is_oauth: boolean;
description: string;
};
export type TWindmillListVariablesResponse = TWindmillVariable[];
export type TWindmillListVariables = {
accessToken: string;
instanceUrl: string;
path: string;
workspace: string;
description?: string;
};
export type TPostWindmillVariable = TWindmillListVariables & {
value: string;
};
export type TDeleteWindmillVariable = TWindmillListVariables;
@@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
} }
}; };
return { ...stOrm, findById }; const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
try {
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
await (tx || db.replicaNode())(TableName.ServiceToken)
.leftJoin<TUsers>(
TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName")
)
.limit(batchSize)
.offset(offset);
return batch;
} catch (err) {
throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
}
};
return { ...stOrm, findById, findExpiringTokens };
}; };
@@ -12,11 +12,13 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TServiceTokenDALFactory } from "./service-token-dal"; import { TServiceTokenDALFactory } from "./service-token-dal";
import { import {
@@ -33,6 +35,7 @@ type TServiceTokenServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">; accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>; export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
@@ -43,7 +46,8 @@ export const serviceTokenServiceFactory = ({
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL,
accessTokenQueue accessTokenQueue,
smtpService
}: TServiceTokenServiceFactoryDep) => { }: TServiceTokenServiceFactoryDep) => {
const createServiceToken = async ({ const createServiceToken = async ({
iv, iv,
@@ -185,11 +189,56 @@ export const serviceTokenServiceFactory = ({
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
}; };
const notifyExpiringTokens = async () => {
const appCfg = getConfig();
let processedCount = 0;
let hasMoreRecords = true;
let offset = 0;
const batchSize = 500;
while (hasMoreRecords) {
// eslint-disable-next-line no-await-in-loop
const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset);
if (expiringTokens.length === 0) {
hasMoreRecords = false;
break;
}
// eslint-disable-next-line no-await-in-loop
await Promise.all(
expiringTokens.map(async (token) => {
try {
await smtpService.sendMail({
recipients: [token.createdByEmail],
subjectLine: "Service Token Expiry Notice",
template: SmtpTemplates.ServiceTokenExpired,
substitutions: {
tokenName: token.name,
projectName: token.projectName,
url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens`
}
});
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
} catch (error) {
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
}
})
);
processedCount += expiringTokens.length;
offset += batchSize;
}
return processedCount;
};
return { return {
createServiceToken, createServiceToken,
deleteServiceToken, deleteServiceToken,
getServiceToken, getServiceToken,
getProjectServiceTokens, getProjectServiceTokens,
fnValidateServiceToken fnValidateServiceToken,
notifyExpiringTokens
}; };
}; };
+2 -1
View File
@@ -43,7 +43,8 @@ export enum SmtpTemplates {
SecretRequestCompleted = "secretRequestCompleted.handlebars", SecretRequestCompleted = "secretRequestCompleted.handlebars",
SecretRotationFailed = "secretRotationFailed.handlebars", SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars", ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars" OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
} }
export enum SmtpHost { export enum SmtpHost {
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Service Token Expiring Soon</title>
</head>
<body>
<h2>Service Token Expiry Notice</h2>
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
<p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
<a href="{{url}}">Create New Token</a>
{{emailFooter}}
</body>
</html>
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/windmill/available"
---
@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/windmill"
---
<Note>
Check out the configuration docs for [Windmill Connections](/integrations/app-connections/windmill) to learn how to obtain
the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/windmill/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/windmill/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/windmill/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/windmill"
---
@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/windmill/{connectionId}"
---
<Note>
Check out the configuration docs for [Windmill Connections](/integrations/app-connections/windmill) to learn how to obtain
the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/windmill"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/windmill/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/windmill/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/windmill/sync-name/{syncName}"
---
@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/import-secrets"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/windmill"
---
@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/remove-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/windmill/{syncId}/sync-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/windmill/{syncId}"
---
@@ -25,7 +25,7 @@ By default, every user in a project is either a **viewer**, **developer**, or an
As such: As such:
- **Admin**: This role enables identities to have access to all environments, folders, secrets, and actions within the project. - **Admin**: This role enables identities to have access to all environments, folders, secrets, and actions within the project.
- **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles/members, and more. - **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles, editing and removing project members, and more.
- **Viewer**: The most limiting bulit-in role on the project level – it forbids user and machine identities to perform any action and rather shows them in the read-only mode. - **Viewer**: The most limiting bulit-in role on the project level – it forbids user and machine identities to perform any action and rather shows them in the read-only mode.
![Project member role](/images/platform/access-controls/rbac.png) ![Project member role](/images/platform/access-controls/rbac.png)
@@ -42,7 +42,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**.
![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png)
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png)
Once you've done that, press **Update** to complete the required configuration. Once you've done that, press **Update** to complete the required configuration.
@@ -65,7 +65,9 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
We recommend ensuring that your account is provisioned using the application in Auth0 We recommend ensuring that your account is provisioned using the application in Auth0
prior to enforcing OIDC SSO to prevent any unintended issues. prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -72,6 +72,10 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO."
To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical; To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Auth0 user with Infisical;
Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -106,6 +106,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Azure We recommend ensuring that your account is provisioned the application in Azure
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -66,6 +66,9 @@ Prerequisites:
<Warning> <Warning>
We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues. We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -81,6 +81,9 @@ description: "Learn how to configure Google SAML for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Google We recommend ensuring that your account is provisioned the application in Google
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -86,6 +86,9 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO."
We recommend ensuring that your account is provisioned the application in JumpCloud We recommend ensuring that your account is provisioned the application in JumpCloud
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -69,7 +69,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png)
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**.
![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png)
Once you've done that, press **Update** to complete the required configuration. Once you've done that, press **Update** to complete the required configuration.
@@ -92,7 +92,9 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
We recommend ensuring that your account is provisioned using the application in Keycloak We recommend ensuring that your account is provisioned using the application in Keycloak
prior to enforcing OIDC SSO to prevent any unintended issues. prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
@@ -127,6 +127,9 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Keycloak We recommend ensuring that your account is provisioned the application in Keycloak
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
+3
View File
@@ -94,6 +94,9 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Okta We recommend ensuring that your account is provisioned the application in Okta
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
<Info>
In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step> </Step>
</Steps> </Steps>
Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Some files were not shown because too many files have changed in this diff Show More