mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 17:29:14 +00:00
Merge branch 'main' into feat/azureClientSecretsRotation
This commit is contained in:
@@ -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 }}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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,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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
+1
-1
@@ -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 };
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

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

|

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

|

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

|

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

|

|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user