mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 09:26:37 +00:00
Merge remote-tracking branch 'origin/main' into feat/azurePkiConnector
This commit is contained in:
Vendored
+1
@@ -148,6 +148,7 @@ declare module "fastify" {
|
|||||||
interface Session {
|
interface Session {
|
||||||
callbackPort: string;
|
callbackPort: string;
|
||||||
isAdminLogin: boolean;
|
isAdminLogin: boolean;
|
||||||
|
orgSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ const up = async (knex: Knex): Promise<void> => {
|
|||||||
t.index("expiresAt");
|
t.index("expiresAt");
|
||||||
t.index("orgId");
|
t.index("orgId");
|
||||||
t.index("projectId");
|
t.index("projectId");
|
||||||
|
t.index("eventType");
|
||||||
|
t.index("userAgentType");
|
||||||
|
t.index("actor");
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Adding GIN indices...");
|
console.log("Adding GIN indices...");
|
||||||
@@ -119,8 +122,8 @@ const up = async (knex: Knex): Promise<void> => {
|
|||||||
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
|
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
|
||||||
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
|
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
|
||||||
|
|
||||||
// create partitions 4 years ahead
|
// create partitions 20 years ahead
|
||||||
const partitionMonths = 4 * 12;
|
const partitionMonths = 20 * 12;
|
||||||
const partitionPromises: Promise<void>[] = [];
|
const partitionPromises: Promise<void>[] = [];
|
||||||
for (let x = 1; x <= partitionMonths; x += 1) {
|
for (let x = 1; x <= partitionMonths; x += 1) {
|
||||||
partitionPromises.push(
|
partitionPromises.push(
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified"))) {
|
||||||
|
// Add the column
|
||||||
|
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||||
|
t.boolean("isEmailVerified").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliasesToUpdate: { aliasId: string; isEmailVerified: boolean }[] = await knex(TableName.UserAliases)
|
||||||
|
.join(TableName.Users, `${TableName.UserAliases}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select([`${TableName.UserAliases}.id as aliasId`, `${TableName.Users}.isEmailVerified`]);
|
||||||
|
|
||||||
|
for (let i = 0; i < aliasesToUpdate.length; i += BATCH_SIZE) {
|
||||||
|
const batch = aliasesToUpdate.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
|
const trueIds = batch.filter((row) => row.isEmailVerified).map((row) => row.aliasId);
|
||||||
|
|
||||||
|
if (trueIds.length > 0) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.UserAliases).whereIn("id", trueIds).update({ isEmailVerified: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.AuthTokens, "aliasId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||||
|
t.string("aliasId").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified")) {
|
||||||
|
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||||
|
t.dropColumn("isEmailVerified");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
|
||||||
|
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||||
|
t.dropColumn("aliasId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
|
||||||
|
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||||
|
);
|
||||||
|
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||||
|
if (!hasGoogleSsoAuthEnforcedColumn)
|
||||||
|
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
|
||||||
|
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||||
|
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
|
||||||
|
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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.SecretApprovalPolicy, "shouldCheckSecretPermission"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.boolean("shouldCheckSecretPermission").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
t.dropColumn("shouldCheckSecretPermission");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||||
|
// find all existing SecretApprovalPolicy rows to backfill shouldCheckSecretPermission flag
|
||||||
|
const rows = await knex(TableName.SecretApprovalPolicy).select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||||
|
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.SecretApprovalPolicy)
|
||||||
|
.whereIn(
|
||||||
|
"id",
|
||||||
|
batch.map((row) => row.id)
|
||||||
|
)
|
||||||
|
.update({ shouldCheckSecretPermission: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
||||||
@@ -17,7 +17,8 @@ export const AuthTokensSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional()
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
|
aliasId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
|||||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||||
|
googleSsoAuthEnforced: z.boolean().default(false),
|
||||||
|
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard"),
|
enforcementLevel: z.string().default("hard"),
|
||||||
deletedAt: z.date().nullable().optional(),
|
deletedAt: z.date().nullable().optional(),
|
||||||
allowedSelfApprovals: z.boolean().default(true)
|
allowedSelfApprovals: z.boolean().default(true),
|
||||||
|
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const UserAliasesSchema = z.object({
|
|||||||
emails: z.string().array().nullable().optional(),
|
emails: z.string().array().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z
|
approvers: z
|
||||||
.object({
|
.object({
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||||
userId: z.string().nullable().optional(),
|
userId: z.string().nullable().optional(),
|
||||||
sequence: z.number().nullable().optional(),
|
sequence: z.number().nullable().optional(),
|
||||||
approvalsRequired: z.number().nullable().optional(),
|
approvalsRequired: z.number().nullable().optional(),
|
||||||
@@ -150,6 +151,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
}),
|
}),
|
||||||
reviewers: z
|
reviewers: z
|
||||||
.object({
|
.object({
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
status: z.string()
|
status: z.string()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -294,22 +294,30 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approval: SecretApprovalRequestsSchema.merge(
|
approval: SecretApprovalRequestsSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
// secretPath: z.string(),
|
|
||||||
policy: z.object({
|
policy: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: approvalRequestUser.array(),
|
approvers: approvalRequestUser
|
||||||
|
.extend({ isOrgMembershipActive: z.boolean().nullable().optional() })
|
||||||
|
.array(),
|
||||||
bypassers: approvalRequestUser.array(),
|
bypassers: approvalRequestUser.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
enforcementLevel: z.string(),
|
enforcementLevel: z.string(),
|
||||||
deletedAt: z.date().nullish(),
|
deletedAt: z.date().nullish(),
|
||||||
allowedSelfApprovals: z.boolean()
|
allowedSelfApprovals: z.boolean(),
|
||||||
|
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||||
}),
|
}),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
statusChangedByUser: approvalRequestUser.optional(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
committerUser: approvalRequestUser.nullish(),
|
committerUser: approvalRequestUser.nullish(),
|
||||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
reviewers: approvalRequestUser
|
||||||
|
.extend({
|
||||||
|
status: z.string(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
isOrgMembershipActive: z.boolean().nullable().optional()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: secretRawSchema
|
commits: secretRawSchema
|
||||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AccessApprovalRequestsSchema,
|
AccessApprovalRequestsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
TAccessApprovalRequests,
|
TAccessApprovalRequests,
|
||||||
|
TOrgMemberships,
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUsers
|
TUsers
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@@ -144,6 +145,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -151,6 +153,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -202,6 +205,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
approvers: (
|
approvers: (
|
||||||
| {
|
| {
|
||||||
@@ -210,6 +214,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -217,6 +222,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -288,6 +294,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
`requestedByUser.id`
|
`requestedByUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
|
`approverOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||||
|
`${TableName.Users}.id`,
|
||||||
|
`approverGroupOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`reviewerOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
|
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
@@ -300,6 +324,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||||
|
|
||||||
|
db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||||
|
db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||||
|
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||||
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||||
)
|
)
|
||||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
@@ -396,17 +424,26 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
{
|
{
|
||||||
key: "reviewerUserId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
mapper: ({ reviewerUserId: userId, reviewerStatus: status, reviewerIsOrgMembershipActive }) =>
|
||||||
|
userId ? { userId, status, isOrgMembershipActive: reviewerIsOrgMembershipActive } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
|
mapper: ({
|
||||||
|
approverUserId,
|
||||||
|
approverSequence,
|
||||||
|
approvalsRequired,
|
||||||
|
approverUsername,
|
||||||
|
approverEmail,
|
||||||
|
approverIsOrgMembershipActive
|
||||||
|
}) => ({
|
||||||
userId: approverUserId,
|
userId: approverUserId,
|
||||||
sequence: approverSequence,
|
sequence: approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
email: approverEmail,
|
email: approverEmail,
|
||||||
username: approverUsername
|
username: approverUsername,
|
||||||
|
isOrgMembershipActive: approverIsOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -417,13 +454,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
approverSequence,
|
approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
approverGroupEmail,
|
approverGroupEmail,
|
||||||
approverGroupUsername
|
approverGroupUsername,
|
||||||
|
approverGroupIsOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId: approverGroupUserId,
|
userId: approverGroupUserId,
|
||||||
sequence: approverSequence,
|
sequence: approverSequence,
|
||||||
approvalsRequired,
|
approvalsRequired,
|
||||||
email: approverGroupEmail,
|
email: approverGroupEmail,
|
||||||
username: approverGroupUsername
|
username: approverGroupUsername,
|
||||||
|
isOrgMembershipActive: approverGroupIsOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -94,6 +95,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
@@ -145,6 +147,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
reviewers: {
|
reviewers: {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
approvers: (
|
approvers: (
|
||||||
| {
|
| {
|
||||||
@@ -153,6 +156,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -160,6 +164,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
|||||||
approvalsRequired: number | null | undefined;
|
approvalsRequired: number | null | undefined;
|
||||||
email: string | null | undefined;
|
email: string | null | undefined;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
bypassers: string[];
|
bypassers: string[];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ActorType } from "@app/services/auth/auth-type";
|
|||||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||||
|
|
||||||
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
||||||
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
|
pruneAuditLog: () => Promise<void>;
|
||||||
find: (
|
find: (
|
||||||
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||||
actorId?: string | undefined;
|
actorId?: string | undefined;
|
||||||
@@ -41,6 +41,10 @@ type TFindQuery = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
|
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||||
|
const MAX_RETRY_ON_FAILURE = 3;
|
||||||
|
|
||||||
export const auditLogDALFactory = (db: TDbClient) => {
|
export const auditLogDALFactory = (db: TDbClient) => {
|
||||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||||
|
|
||||||
@@ -151,20 +155,20 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// delete all audit log that have expired
|
// delete all audit log that have expired
|
||||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async () => {
|
||||||
const runPrune = async (dbClient: knex.Knex) => {
|
const today = new Date();
|
||||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
let deletedAuditLogIds: { id: string }[] = [];
|
||||||
const MAX_RETRY_ON_FAILURE = 3;
|
let numberOfRetryOnFailure = 0;
|
||||||
|
let isRetrying = false;
|
||||||
|
|
||||||
const today = new Date();
|
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||||
let deletedAuditLogIds: { id: string }[] = [];
|
do {
|
||||||
let numberOfRetryOnFailure = 0;
|
try {
|
||||||
let isRetrying = false;
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
deletedAuditLogIds = await db.transaction(async (trx) => {
|
||||||
|
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||||
|
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
const findExpiredLogSubQuery = trx(TableName.AuditLog)
|
||||||
do {
|
|
||||||
try {
|
|
||||||
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
|
|
||||||
.where("expiresAt", "<", today)
|
.where("expiresAt", "<", today)
|
||||||
.where("createdAt", "<", today) // to use audit log partition
|
.where("createdAt", "<", today) // to use audit log partition
|
||||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||||
@@ -172,35 +176,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
deletedAuditLogIds = await dbClient(TableName.AuditLog)
|
const results = await trx(TableName.AuditLog).whereIn("id", findExpiredLogSubQuery).del().returning("id");
|
||||||
.whereIn("id", findExpiredLogSubQuery)
|
|
||||||
.del()
|
|
||||||
.returning("id");
|
|
||||||
numberOfRetryOnFailure = 0; // reset
|
|
||||||
} catch (error) {
|
|
||||||
numberOfRetryOnFailure += 1;
|
|
||||||
deletedAuditLogIds = [];
|
|
||||||
logger.error(error, "Failed to delete audit log on pruning");
|
|
||||||
} finally {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, 10); // time to breathe for db
|
|
||||||
});
|
|
||||||
}
|
|
||||||
isRetrying = numberOfRetryOnFailure > 0;
|
|
||||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tx) {
|
return results;
|
||||||
await runPrune(tx);
|
});
|
||||||
} else {
|
|
||||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
numberOfRetryOnFailure = 0; // reset
|
||||||
await db.transaction(async (trx) => {
|
} catch (error) {
|
||||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
numberOfRetryOnFailure += 1;
|
||||||
await runPrune(trx);
|
deletedAuditLogIds = [];
|
||||||
});
|
logger.error(error, "Failed to delete audit log on pruning");
|
||||||
}
|
} finally {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 10); // time to breathe for db
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isRetrying = numberOfRetryOnFailure > 0;
|
||||||
|
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
|||||||
|
|
||||||
await redis.set(key, "1", "EX", 60);
|
await redis.set(key, "1", "EX", 60);
|
||||||
|
|
||||||
stream.push("1");
|
send({ type: "ping" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
|||||||
@@ -400,15 +400,13 @@ export const ldapConfigServiceFactory = ({
|
|||||||
|
|
||||||
userAlias = await userDAL.transaction(async (tx) => {
|
userAlias = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
if (serverCfg.trustLdapEmails) {
|
newUser = await userDAL.findOne(
|
||||||
newUser = await userDAL.findOne(
|
{
|
||||||
{
|
email: email.toLowerCase(),
|
||||||
email: email.toLowerCase(),
|
isEmailVerified: true
|
||||||
isEmailVerified: true
|
},
|
||||||
},
|
tx
|
||||||
tx
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||||
@@ -433,7 +431,8 @@ export const ldapConfigServiceFactory = ({
|
|||||||
aliasType: UserAliasType.LDAP,
|
aliasType: UserAliasType.LDAP,
|
||||||
externalId,
|
externalId,
|
||||||
emails: [email],
|
emails: [email],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustLdapEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -556,15 +555,14 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
|
||||||
|
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
@@ -572,6 +570,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: AuthMethod.LDAP,
|
authMethod: AuthMethod.LDAP,
|
||||||
authType: UserAliasType.LDAP,
|
authType: UserAliasType.LDAP,
|
||||||
|
aliasId: userAlias.id,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
? {
|
? {
|
||||||
@@ -585,10 +584,11 @@ export const ldapConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !userAlias.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
|
enforceGoogleSSO: false,
|
||||||
hsm: false,
|
hsm: false,
|
||||||
oidcSSO: false,
|
oidcSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type TFeatureSet = {
|
|||||||
auditLogStreamLimit: 3;
|
auditLogStreamLimit: 3;
|
||||||
githubOrgSync: false;
|
githubOrgSync: false;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
|
enforceGoogleSSO: false;
|
||||||
hsm: false;
|
hsm: false;
|
||||||
oidcSSO: false;
|
oidcSSO: false;
|
||||||
secretAccessInsights: false;
|
secretAccessInsights: false;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const userAlias = await userAliasDAL.findOne({
|
let userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.OIDC
|
aliasType: UserAliasType.OIDC
|
||||||
@@ -231,32 +231,29 @@ export const oidcConfigServiceFactory = ({
|
|||||||
} else {
|
} else {
|
||||||
user = await userDAL.transaction(async (tx) => {
|
user = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
|
// we prioritize getting the most complete user to create the new alias under
|
||||||
|
newUser = await userDAL.findOne(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
isEmailVerified: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
if (serverCfg.trustOidcEmails) {
|
if (!newUser) {
|
||||||
// we prioritize getting the most complete user to create the new alias under
|
// this fetches user entries created via invites
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.findOne(
|
||||||
{
|
{
|
||||||
email,
|
username: email
|
||||||
isEmailVerified: true
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newUser) {
|
if (newUser && !newUser.isEmailVerified) {
|
||||||
// this fetches user entries created via invites
|
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.updateById(newUser.id, {
|
||||||
{
|
isEmailVerified: serverCfg.trustOidcEmails
|
||||||
username: email
|
});
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newUser && !newUser.isEmailVerified) {
|
|
||||||
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
|
||||||
newUser = await userDAL.updateById(newUser.id, {
|
|
||||||
isEmailVerified: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +273,14 @@ export const oidcConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await userAliasDAL.create(
|
userAlias = await userAliasDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
aliasType: UserAliasType.OIDC,
|
aliasType: UserAliasType.OIDC,
|
||||||
externalId,
|
externalId,
|
||||||
emails: email ? [email] : [],
|
emails: email ? [email] : [],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustOidcEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -404,19 +402,20 @@ export const oidcConfigServiceFactory = ({
|
|||||||
|
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
|
aliasId: userAlias.id,
|
||||||
authMethod: AuthMethod.OIDC,
|
authMethod: AuthMethod.OIDC,
|
||||||
authType: UserAliasType.OIDC,
|
authType: UserAliasType.OIDC,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
@@ -430,10 +429,11 @@ export const oidcConfigServiceFactory = ({
|
|||||||
|
|
||||||
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !userAlias.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService
|
await smtpService
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionPkiTemplateActions,
|
ProjectPermissionPkiTemplateActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSecretEventActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSecretScanningConfigActions,
|
ProjectPermissionSecretScanningConfigActions,
|
||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
@@ -252,6 +253,16 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SecretScanningConfigs
|
ProjectPermissionSub.SecretScanningConfigs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SecretEvents
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -455,6 +466,16 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SecretEvents
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -505,6 +526,16 @@ const buildViewerPermissionRules = () => {
|
|||||||
|
|
||||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||||
|
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SecretEvents
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
|
|||||||
projectFavorites?: string[] | null | undefined;
|
projectFavorites?: string[] | null | undefined;
|
||||||
customRoleSlug?: string | null | undefined;
|
customRoleSlug?: string | null | undefined;
|
||||||
orgAuthEnforced?: boolean | null | undefined;
|
orgAuthEnforced?: boolean | null | undefined;
|
||||||
|
orgGoogleSsoAuthEnforced: boolean;
|
||||||
} & {
|
} & {
|
||||||
groups: {
|
groups: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
|
|||||||
}[];
|
}[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
orgAuthEnforced: boolean | null | undefined;
|
orgAuthEnforced: boolean | null | undefined;
|
||||||
|
orgGoogleSsoAuthEnforced: boolean;
|
||||||
orgRole: OrgMembershipRole;
|
orgRole: OrgMembershipRole;
|
||||||
userId: string;
|
userId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
db.ref("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("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
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"),
|
||||||
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
OrgMembershipsSchema.extend({
|
OrgMembershipsSchema.extend({
|
||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
|
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||||
bypassOrgAuthEnabled: z.boolean(),
|
bypassOrgAuthEnabled: z.boolean(),
|
||||||
customRoleSlug: z.string().optional().nullable(),
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean()
|
shouldUseNewPrivilegeSystem: z.boolean()
|
||||||
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
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("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||||
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
orgId,
|
orgId,
|
||||||
username,
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgGoogleSsoAuthEnforced,
|
||||||
orgRole,
|
orgRole,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
}) => ({
|
}) => ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgGoogleSsoAuthEnforced,
|
||||||
orgRole: orgRole as OrgMembershipRole,
|
orgRole: orgRole as OrgMembershipRole,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
function validateOrgSSO(
|
function validateOrgSSO(
|
||||||
actorAuthMethod: ActorAuthMethod,
|
actorAuthMethod: ActorAuthMethod,
|
||||||
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||||
|
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
|
||||||
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||||
orgRole: OrgMembershipRole
|
orgRole: OrgMembershipRole
|
||||||
) {
|
) {
|
||||||
@@ -128,10 +129,16 @@ function validateOrgSSO(
|
|||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// case: google sso is enforced, but the actor is not using google sso
|
||||||
|
if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) {
|
||||||
|
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: SAML SSO is enforced, but the actor is not using SAML SSO
|
||||||
if (
|
if (
|
||||||
isOrgSsoEnforced &&
|
isOrgSsoEnforced &&
|
||||||
actorAuthMethod !== null &&
|
actorAuthMethod !== null &&
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
|
|||||||
validateOrgSSO(
|
validateOrgSSO(
|
||||||
authMethod,
|
authMethod,
|
||||||
membership.orgAuthEnforced,
|
membership.orgAuthEnforced,
|
||||||
|
membership.orgGoogleSsoAuthEnforced,
|
||||||
membership.bypassOrgAuthEnabled,
|
membership.bypassOrgAuthEnabled,
|
||||||
membership.role as OrgMembershipRole
|
membership.role as OrgMembershipRole
|
||||||
);
|
);
|
||||||
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
|
|||||||
validateOrgSSO(
|
validateOrgSSO(
|
||||||
authMethod,
|
authMethod,
|
||||||
userProjectPermission.orgAuthEnforced,
|
userProjectPermission.orgAuthEnforced,
|
||||||
|
userProjectPermission.orgGoogleSsoAuthEnforced,
|
||||||
userProjectPermission.bypassOrgAuthEnabled,
|
userProjectPermission.bypassOrgAuthEnabled,
|
||||||
userProjectPermission.orgRole
|
userProjectPermission.orgRole
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAlias = await userAliasDAL.findOne({
|
let userAlias = await userAliasDAL.findOne({
|
||||||
externalId,
|
externalId,
|
||||||
orgId,
|
orgId,
|
||||||
aliasType: UserAliasType.SAML
|
aliasType: UserAliasType.SAML
|
||||||
@@ -320,15 +320,13 @@ export const samlConfigServiceFactory = ({
|
|||||||
|
|
||||||
user = await userDAL.transaction(async (tx) => {
|
user = await userDAL.transaction(async (tx) => {
|
||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
if (serverCfg.trustSamlEmails) {
|
newUser = await userDAL.findOne(
|
||||||
newUser = await userDAL.findOne(
|
{
|
||||||
{
|
email,
|
||||||
email,
|
isEmailVerified: true
|
||||||
isEmailVerified: true
|
},
|
||||||
},
|
tx
|
||||||
tx
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
||||||
@@ -346,13 +344,14 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await userAliasDAL.create(
|
userAlias = await userAliasDAL.create(
|
||||||
{
|
{
|
||||||
userId: newUser.id,
|
userId: newUser.id,
|
||||||
aliasType: UserAliasType.SAML,
|
aliasType: UserAliasType.SAML,
|
||||||
externalId,
|
externalId,
|
||||||
emails: email ? [email] : [],
|
emails: email ? [email] : [],
|
||||||
orgId
|
orgId,
|
||||||
|
isEmailVerified: serverCfg.trustSamlEmails
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -410,13 +409,13 @@ export const samlConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
|
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified && userAlias.isEmailVerified);
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
@@ -424,6 +423,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
organizationSlug: organization.slug,
|
organizationSlug: organization.slug,
|
||||||
authMethod: authProvider,
|
authMethod: authProvider,
|
||||||
hasExchangedPrivateKey: true,
|
hasExchangedPrivateKey: true,
|
||||||
|
aliasId: userAlias.id,
|
||||||
authType: UserAliasType.SAML,
|
authType: UserAliasType.SAML,
|
||||||
isUserCompleted,
|
isUserCompleted,
|
||||||
...(relayState
|
...(relayState
|
||||||
@@ -440,10 +440,11 @@ export const samlConfigServiceFactory = ({
|
|||||||
|
|
||||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !userAlias.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
aliasId: userAlias.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
|||||||
import {
|
import {
|
||||||
SecretApprovalRequestsSchema,
|
SecretApprovalRequestsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
|
TOrgMemberships,
|
||||||
TSecretApprovalRequests,
|
TSecretApprovalRequests,
|
||||||
TSecretApprovalRequestsSecrets,
|
TSecretApprovalRequestsSecrets,
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
@@ -107,11 +108,32 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||||
`secretApprovalReviewerUser.id`
|
`secretApprovalReviewerUser.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||||
|
`approverOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||||
|
`secretApprovalPolicyGroupApproverUser.id`,
|
||||||
|
`approverGroupOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
|
.leftJoin<TOrgMemberships>(
|
||||||
|
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||||
|
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`reviewerOrgMembership.userId`
|
||||||
|
)
|
||||||
|
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
|
tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
|
||||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||||
|
tx.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||||
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||||
@@ -148,6 +170,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
||||||
|
tx.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||||
tx.ref("projectId").withSchema(TableName.Environment),
|
tx.ref("projectId").withSchema(TableName.Environment),
|
||||||
@@ -157,7 +180,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
|
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt"),
|
||||||
|
tx
|
||||||
|
.ref("shouldCheckSecretPermission")
|
||||||
|
.withSchema(TableName.SecretApprovalPolicy)
|
||||||
|
.as("policySecretReadAccessCompat")
|
||||||
);
|
);
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
@@ -197,7 +224,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
enforcementLevel: el.policyEnforcementLevel,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
envId: el.policyEnvId,
|
envId: el.policyEnvId,
|
||||||
deletedAt: el.policyDeletedAt,
|
deletedAt: el.policyDeletedAt,
|
||||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
allowedSelfApprovals: el.policyAllowedSelfApprovals,
|
||||||
|
shouldCheckSecretPermission: el.policySecretReadAccessCompat
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
@@ -211,9 +239,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
reviewerLastName: lastName,
|
reviewerLastName: lastName,
|
||||||
reviewerUsername: username,
|
reviewerUsername: username,
|
||||||
reviewerFirstName: firstName,
|
reviewerFirstName: firstName,
|
||||||
reviewerComment: comment
|
reviewerComment: comment,
|
||||||
|
reviewerIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) =>
|
}) =>
|
||||||
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
userId
|
||||||
|
? {
|
||||||
|
userId,
|
||||||
|
status,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username,
|
||||||
|
comment: comment ?? "",
|
||||||
|
isOrgMembershipActive
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
@@ -223,13 +263,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverEmail: email,
|
approverEmail: email,
|
||||||
approverUsername: username,
|
approverUsername: username,
|
||||||
approverLastName: lastName,
|
approverLastName: lastName,
|
||||||
approverFirstName: firstName
|
approverFirstName: firstName,
|
||||||
|
approverIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
isOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,13 +282,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approverGroupEmail: email,
|
approverGroupEmail: email,
|
||||||
approverGroupUsername: username,
|
approverGroupUsername: username,
|
||||||
approverGroupLastName: lastName,
|
approverGroupLastName: lastName,
|
||||||
approverGroupFirstName: firstName
|
approverGroupFirstName: firstName,
|
||||||
|
approverGroupIsOrgMembershipActive: isOrgMembershipActive
|
||||||
}) => ({
|
}) => ({
|
||||||
userId,
|
userId,
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
username
|
username,
|
||||||
|
isOrgMembershipActive
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -653,14 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
|
||||||
.as("inner");
|
.as("inner");
|
||||||
|
|
||||||
const query = (tx || db)
|
const countQuery = (await (tx || db)
|
||||||
.select("*")
|
|
||||||
.select(db.raw("count(*) OVER() as total_count"))
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
.from(innerQuery)
|
.from(innerQuery.clone().distinctOn(`${TableName.SecretApprovalRequest}.id`))) as Array<{
|
||||||
.orderBy("createdAt", "desc") as typeof innerQuery;
|
total_count: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
void query.where((qb) => {
|
void query.where((qb) => {
|
||||||
@@ -686,8 +731,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.where("w.rank", ">=", rankOffset)
|
.where("w.rank", ">=", rankOffset)
|
||||||
.andWhere("w.rank", "<", rankOffset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
|
||||||
// @ts-expect-error knex does not infer
|
const totalCount = Number(countQuery[0]?.total_count || 0);
|
||||||
const totalCount = Number(docs[0]?.total_count || 0);
|
|
||||||
|
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
|
|||||||
+59
-13
@@ -258,6 +258,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||||
|
|
||||||
if (!secretApprovalRequest)
|
if (!secretApprovalRequest)
|
||||||
throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` });
|
throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` });
|
||||||
|
|
||||||
@@ -280,13 +281,22 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
) {
|
) {
|
||||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
}
|
}
|
||||||
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
|
const getHasSecretReadAccess = (
|
||||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
shouldCheckSecretPermission: boolean | null | undefined,
|
||||||
environment,
|
environment: string,
|
||||||
secretPath: secretPath || "/",
|
tags: { slug: string }[],
|
||||||
secretTags: tags.map((i) => i.slug)
|
secretPath?: string
|
||||||
});
|
) => {
|
||||||
return canRead;
|
if (shouldCheckSecretPermission) {
|
||||||
|
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
|
environment,
|
||||||
|
secretPath: secretPath || "/",
|
||||||
|
secretTags: tags.map((i) => i.slug)
|
||||||
|
});
|
||||||
|
return canRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
let secrets;
|
let secrets;
|
||||||
@@ -308,8 +318,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
version: el.version,
|
version: el.version,
|
||||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
),
|
||||||
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secret && el.secret.isRotatedSecret
|
: el.secret && el.secret.isRotatedSecret
|
||||||
? undefined
|
? undefined
|
||||||
@@ -325,11 +345,17 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
id: el.secret.id,
|
id: el.secret.id,
|
||||||
version: el.secret.version,
|
version: el.secret.version,
|
||||||
secretValueHidden: !getHasSecretReadAccess(
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
secretApprovalRequest.environment,
|
secretApprovalRequest.environment,
|
||||||
el.tags,
|
el.tags,
|
||||||
secretPath?.[0]?.path
|
secretPath?.[0]?.path
|
||||||
),
|
),
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secret.encryptedValue
|
: el.secret.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||||
@@ -345,11 +371,17 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
id: el.secretVersion.id,
|
id: el.secretVersion.id,
|
||||||
version: el.secretVersion.version,
|
version: el.secretVersion.version,
|
||||||
secretValueHidden: !getHasSecretReadAccess(
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
secretApprovalRequest.environment,
|
secretApprovalRequest.environment,
|
||||||
el.tags,
|
el.tags,
|
||||||
secretPath?.[0]?.path
|
secretPath?.[0]?.path
|
||||||
),
|
),
|
||||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
secretValue: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
)
|
||||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||||
: el.secretVersion.encryptedValue
|
: el.secretVersion.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||||
@@ -367,7 +399,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||||
secrets = encryptedSecrets.map((el) => ({
|
secrets = encryptedSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
secretValueHidden: !getHasSecretReadAccess(
|
||||||
|
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||||
|
secretApprovalRequest.environment,
|
||||||
|
el.tags,
|
||||||
|
secretPath?.[0]?.path
|
||||||
|
),
|
||||||
...decryptSecretWithBot(el, botKey),
|
...decryptSecretWithBot(el, botKey),
|
||||||
secret: el.secret
|
secret: el.secret
|
||||||
? {
|
? {
|
||||||
@@ -1447,6 +1484,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
|
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
|
||||||
const commitTagIds: Record<string, string[]> = {};
|
const commitTagIds: Record<string, string[]> = {};
|
||||||
|
const existingTagIds: Record<string, string[]> = {};
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -1512,6 +1550,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
secretsToUpdateStoredInDB.forEach((el) => {
|
||||||
|
if (el.tags?.length) existingTagIds[el.key] = el.tags.map((i) => i.id);
|
||||||
|
});
|
||||||
|
|
||||||
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
|
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||||
@@ -1555,7 +1598,10 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretMetadata
|
secretMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||||
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
if (tagIds?.length || existingTagIds[secretKey]?.length) {
|
||||||
|
commitTagIds[newSecretName ?? secretKey] = tagIds || existingTagIds[secretKey];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...latestSecretVersions[secretId],
|
...latestSecretVersions[secretId],
|
||||||
secretMetadata,
|
secretMetadata,
|
||||||
|
|||||||
@@ -2500,6 +2500,7 @@ export const SecretSyncs = {
|
|||||||
},
|
},
|
||||||
RENDER: {
|
RENDER: {
|
||||||
serviceId: "The ID of the Render service to sync secrets to.",
|
serviceId: "The ID of the Render service to sync secrets to.",
|
||||||
|
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
|
||||||
scope: "The Render scope that secrets should be synced to.",
|
scope: "The Render scope that secrets should be synced to.",
|
||||||
type: "The Render resource type to sync secrets to."
|
type: "The Render resource type to sync secrets to."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Safely retrieves a value from a nested object using dot notation path
|
* Safely retrieves a value from a nested object using dot notation path
|
||||||
*/
|
*/
|
||||||
export const getStringValueByDot = (
|
export const getValueByDot = (
|
||||||
obj: Record<string, unknown> | null | undefined,
|
obj: Record<string, unknown> | null | undefined,
|
||||||
path: string,
|
path: string,
|
||||||
defaultValue?: string
|
defaultValue?: string | number | boolean
|
||||||
): string | undefined => {
|
): string | number | boolean | undefined => {
|
||||||
// Handle null or undefined input
|
// Handle null or undefined input
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
@@ -26,7 +26,7 @@ export const getStringValueByDot = (
|
|||||||
current = (current as Record<string, unknown>)[part];
|
current = (current as Record<string, unknown>)[part];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof current !== "string") {
|
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -726,7 +726,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectMembershipDAL
|
projectMembershipDAL,
|
||||||
|
userAliasDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const totpService = totpServiceFactory({
|
const totpService = totpServiceFactory({
|
||||||
|
|||||||
@@ -53,4 +53,36 @@ export const registerChecklyConnectionRouter = async (server: FastifyZodProvider
|
|||||||
return { accounts };
|
return { accounts };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/accounts/:accountId/groups`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid(),
|
||||||
|
accountId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId, accountId } = req.params;
|
||||||
|
|
||||||
|
const groups = await server.services.appConnection.checkly.listGroups(connectionId, accountId, req.permission);
|
||||||
|
|
||||||
|
return { groups };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/environment-groups`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: GenericResourceNameSchema.optional(),
|
name: GenericResourceNameSchema.optional(),
|
||||||
slug: slugSchema({ max: 64 }).optional(),
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
authEnforced: z.boolean().optional(),
|
authEnforced: z.boolean().optional(),
|
||||||
|
googleSsoAuthEnforced: z.boolean().optional(),
|
||||||
scimEnabled: z.boolean().optional(),
|
scimEnabled: z.boolean().optional(),
|
||||||
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(),
|
||||||
|
|||||||
@@ -108,7 +108,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
firstName: true,
|
firstName: true,
|
||||||
lastName: true,
|
lastName: true,
|
||||||
id: true
|
id: true
|
||||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
})
|
||||||
|
.merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||||
|
.extend({
|
||||||
|
isOrgMembershipActive: z.boolean()
|
||||||
|
}),
|
||||||
project: SanitizedProjectSchema.pick({ name: true, id: true }),
|
project: SanitizedProjectSchema.pick({ name: true, id: true }),
|
||||||
roles: z.array(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
try {
|
try {
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
const callbackPort = req.session.get("callbackPort");
|
const callbackPort = req.session.get("callbackPort");
|
||||||
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
|
const orgSlug = req.session.get("orgSlug");
|
||||||
|
|
||||||
const email = profile?.emails?.[0]?.value;
|
const email = profile?.emails?.[0]?.value;
|
||||||
if (!email)
|
if (!email)
|
||||||
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
firstName: profile?.name?.givenName || "",
|
firstName: profile?.name?.givenName || "",
|
||||||
lastName: profile?.name?.familyName || "",
|
lastName: profile?.name?.familyName || "",
|
||||||
authMethod: AuthMethod.GOOGLE,
|
authMethod: AuthMethod.GOOGLE,
|
||||||
callbackPort
|
callbackPort,
|
||||||
|
orgSlug
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
callback_port: z.string().optional(),
|
callback_port: z.string().optional(),
|
||||||
|
org_slug: z.string().optional(),
|
||||||
is_admin_login: z
|
is_admin_login: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
preValidation: [
|
preValidation: [
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
|
const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query;
|
||||||
// ensure fresh session state per login attempt
|
// ensure fresh session state per login attempt
|
||||||
await req.session.regenerate();
|
await req.session.regenerate();
|
||||||
if (callbackPort) {
|
if (callbackPort) {
|
||||||
req.session.set("callbackPort", callbackPort);
|
req.session.set("callbackPort", callbackPort);
|
||||||
}
|
}
|
||||||
|
if (orgSlug) {
|
||||||
|
req.session.set("orgSlug", orgSlug);
|
||||||
|
}
|
||||||
if (isAdminLogin) {
|
if (isAdminLogin) {
|
||||||
req.session.set("isAdminLogin", isAdminLogin);
|
req.session.set("isAdminLogin", isAdminLogin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
username: z.string().trim()
|
token: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.object({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.user.sendEmailVerificationCode(req.body.username);
|
await server.services.user.sendEmailVerificationCode(req.body.token);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxi
|
|||||||
|
|
||||||
import { createRequestClient } from "@app/lib/config/request";
|
import { createRequestClient } from "@app/lib/config/request";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
|
|
||||||
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
|
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
|
||||||
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
|
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
|
||||||
@@ -181,6 +182,122 @@ class ChecklyPublicClient {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCheckGroups(connection: TChecklyConnectionConfig, accountId: string, limit = 50, page = 1) {
|
||||||
|
const res = await this.send<{ id: number; name: string }[]>(connection, {
|
||||||
|
accountId,
|
||||||
|
method: "GET",
|
||||||
|
url: `/v1/check-groups`,
|
||||||
|
params: { limit, page }
|
||||||
|
});
|
||||||
|
|
||||||
|
return res?.map((group) => ({
|
||||||
|
id: group.id.toString(),
|
||||||
|
name: group.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCheckGroup(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||||
|
try {
|
||||||
|
type ChecklyGroupResponse = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
environmentVariables: Array<{
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
locked: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await this.send<ChecklyGroupResponse>(connection, {
|
||||||
|
accountId,
|
||||||
|
method: "GET",
|
||||||
|
url: `/v1/check-groups/${groupId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.id.toString(),
|
||||||
|
name: res.name,
|
||||||
|
environmentVariables: res.environmentVariables
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCheckGroupEnvironmentVariables(
|
||||||
|
connection: TChecklyConnectionConfig,
|
||||||
|
accountId: string,
|
||||||
|
groupId: string,
|
||||||
|
environmentVariables: Array<{ key: string; value: string; locked?: boolean }>
|
||||||
|
) {
|
||||||
|
if (environmentVariables.length > 50) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
message: "Checkly does not support syncing more than 50 variables to Check Group",
|
||||||
|
shouldRetry: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiVariables = environmentVariables.map((v) => ({
|
||||||
|
key: v.key,
|
||||||
|
value: v.value,
|
||||||
|
locked: v.locked ?? false,
|
||||||
|
secret: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||||
|
|
||||||
|
await this.send(connection, {
|
||||||
|
accountId,
|
||||||
|
method: "PUT",
|
||||||
|
url: `/v2/check-groups/${groupId}`,
|
||||||
|
data: { name: group?.name, environmentVariables: apiVariables }
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getCheckGroup(connection, accountId, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCheckGroupEnvironmentVariables(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||||
|
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||||
|
return group?.environmentVariables || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertCheckGroupEnvironmentVariables(
|
||||||
|
connection: TChecklyConnectionConfig,
|
||||||
|
accountId: string,
|
||||||
|
groupId: string,
|
||||||
|
variables: Array<{ key: string; value: string; locked?: boolean }>
|
||||||
|
) {
|
||||||
|
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||||
|
const varMap = new Map(existingVars.map((v) => [v.key, v]));
|
||||||
|
|
||||||
|
for (const newVar of variables) {
|
||||||
|
varMap.set(newVar.key, {
|
||||||
|
key: newVar.key,
|
||||||
|
value: newVar.value,
|
||||||
|
locked: newVar.locked ?? false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, Array.from(varMap.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCheckGroupEnvironmentVariable(
|
||||||
|
connection: TChecklyConnectionConfig,
|
||||||
|
accountId: string,
|
||||||
|
groupId: string,
|
||||||
|
variableKey: string
|
||||||
|
) {
|
||||||
|
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||||
|
const filteredVars = existingVars.filter((v) => v.key !== variableKey);
|
||||||
|
|
||||||
|
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, filteredVars);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChecklyPublicAPI = new ChecklyPublicClient();
|
export const ChecklyPublicAPI = new ChecklyPublicClient();
|
||||||
|
|||||||
@@ -24,7 +24,19 @@ export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listGroups = async (connectionId: string, accountId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor);
|
||||||
|
try {
|
||||||
|
const groups = await ChecklyPublicAPI.getCheckGroups(appConnection, accountId);
|
||||||
|
return groups!;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to list accounts on Checkly");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listAccounts
|
listAccounts,
|
||||||
|
listGroups
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,3 +33,15 @@ export type TChecklyAccount = {
|
|||||||
name: string;
|
name: string;
|
||||||
runtimeId: string;
|
runtimeId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TChecklyGroupEnvironmentVariable = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
locked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TChecklyGroup = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
environmentVariables?: TChecklyGroupEnvironmentVariable[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { createAppAuth } from "@octokit/auth-app";
|
|
||||||
import { request } from "@octokit/request";
|
|
||||||
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
@@ -8,6 +6,7 @@ import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic
|
|||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request as httpRequest } from "@app/lib/config/request";
|
import { request as httpRequest } from "@app/lib/config/request";
|
||||||
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -114,10 +113,13 @@ export const requestWithGitHubGateway = async <T>(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => {
|
export const getGitHubAppAuthToken = async (
|
||||||
|
appConnection: TGitHubConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||||
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
let appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||||
|
|
||||||
if (!appId || !appPrivateKey) {
|
if (!appId || !appPrivateKey) {
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
@@ -125,21 +127,42 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appPrivateKey = appPrivateKey
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
if (appConnection.method !== GitHubConnectionMethod.App) {
|
if (appConnection.method !== GitHubConnectionMethod.App) {
|
||||||
throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
|
throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const appAuth = createAppAuth({
|
const now = Math.floor(Date.now() / 1000);
|
||||||
appId,
|
const payload = {
|
||||||
privateKey: appPrivateKey,
|
iat: now,
|
||||||
installationId: appConnection.credentials.installationId,
|
exp: now + 5 * 60,
|
||||||
request: request.defaults({
|
iss: appId
|
||||||
baseUrl: `https://${await getGitHubInstanceApiUrl(appConnection)}`
|
};
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const { token } = await appAuth({ type: "installation" });
|
const appJwt = crypto.jwt().sign(payload, appPrivateKey, { algorithm: "RS256" });
|
||||||
return token;
|
|
||||||
|
const apiBaseUrl = await getGitHubInstanceApiUrl(appConnection);
|
||||||
|
const { installationId } = appConnection.credentials;
|
||||||
|
|
||||||
|
const response = await requestWithGitHubGateway<{ token: string; expires_at: string }>(
|
||||||
|
appConnection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `https://${apiBaseUrl}/app/installations/${installationId}/access_tokens`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${appJwt}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseGitHubLinkHeader = (linkHeader: string | undefined): Record<string, string> => {
|
const parseGitHubLinkHeader = (linkHeader: string | undefined): Record<string, string> => {
|
||||||
@@ -174,7 +197,9 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
|||||||
const { credentials, method } = appConnection;
|
const { credentials, method } = appConnection;
|
||||||
|
|
||||||
const token =
|
const token =
|
||||||
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
|
method === GitHubConnectionMethod.OAuth
|
||||||
|
? credentials.accessToken
|
||||||
|
: await getGitHubAppAuthToken(appConnection, gatewayService);
|
||||||
|
|
||||||
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||||
const initialUrlObj = new URL(baseUrl);
|
const initialUrlObj = new URL(baseUrl);
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
|
|||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { RenderConnectionMethod } from "./render-connection-enums";
|
import { RenderConnectionMethod } from "./render-connection-enums";
|
||||||
import {
|
import {
|
||||||
|
TRawRenderEnvironmentGroup,
|
||||||
TRawRenderService,
|
TRawRenderService,
|
||||||
TRenderConnection,
|
TRenderConnection,
|
||||||
TRenderConnectionConfig,
|
TRenderConnectionConfig,
|
||||||
|
TRenderEnvironmentGroup,
|
||||||
TRenderService
|
TRenderService
|
||||||
} from "./render-connection-types";
|
} from "./render-connection-types";
|
||||||
|
|
||||||
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
|||||||
const perPage = 100;
|
const perPage = 100;
|
||||||
let cursor;
|
let cursor;
|
||||||
|
|
||||||
|
let maxIterations = 10;
|
||||||
|
|
||||||
while (hasMorePages) {
|
while (hasMorePages) {
|
||||||
|
if (maxIterations <= 0) break;
|
||||||
|
|
||||||
const res: TRawRenderService[] = (
|
const res: TRawRenderService[] = (
|
||||||
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
||||||
params: new URLSearchParams({
|
params: new URLSearchParams({
|
||||||
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
|||||||
} else {
|
} else {
|
||||||
cursor = res[res.length - 1].cursor;
|
cursor = res[res.length - 1].cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxIterations -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
|
|||||||
|
|
||||||
return inputCredentials;
|
return inputCredentials;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listRenderEnvironmentGroups = async (
|
||||||
|
appConnection: TRenderConnection
|
||||||
|
): Promise<TRenderEnvironmentGroup[]> => {
|
||||||
|
const {
|
||||||
|
credentials: { apiKey }
|
||||||
|
} = appConnection;
|
||||||
|
|
||||||
|
const groups: TRenderEnvironmentGroup[] = [];
|
||||||
|
let hasMorePages = true;
|
||||||
|
const perPage = 100;
|
||||||
|
let cursor;
|
||||||
|
let maxIterations = 10;
|
||||||
|
|
||||||
|
while (hasMorePages) {
|
||||||
|
if (maxIterations <= 0) break;
|
||||||
|
|
||||||
|
const res: TRawRenderEnvironmentGroup[] = (
|
||||||
|
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
|
||||||
|
params: new URLSearchParams({
|
||||||
|
...(cursor ? { cursor: String(cursor) } : {}),
|
||||||
|
limit: String(perPage)
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
res.forEach((item) => {
|
||||||
|
groups.push({
|
||||||
|
name: item.envGroup.name,
|
||||||
|
id: item.envGroup.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.length < perPage) {
|
||||||
|
hasMorePages = false;
|
||||||
|
} else {
|
||||||
|
cursor = res[res.length - 1].cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxIterations -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { listRenderServices } from "./render-connection-fns";
|
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
|
||||||
import { TRenderConnection } from "./render-connection-types";
|
import { TRenderConnection } from "./render-connection-types";
|
||||||
|
|
||||||
type TGetAppConnectionFunc = (
|
type TGetAppConnectionFunc = (
|
||||||
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
|
||||||
|
try {
|
||||||
|
const groups = await listRenderEnvironmentGroups(appConnection);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to list environment groups for Render connection");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listServices
|
listServices,
|
||||||
|
listEnvironmentGroups
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,3 +33,16 @@ export type TRawRenderService = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TRenderEnvironmentGroup = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRawRenderEnvironmentGroup = {
|
||||||
|
cursor: string;
|
||||||
|
envGroup: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
||||||
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => {
|
||||||
const { token, ...tkCfg } = getTokenConfig(type);
|
const { token, ...tkCfg } = getTokenConfig(type);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
||||||
@@ -88,7 +88,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
type,
|
type,
|
||||||
userId,
|
userId,
|
||||||
orgId,
|
orgId,
|
||||||
triesLeft: tkCfg?.triesLeft
|
triesLeft: tkCfg?.triesLeft,
|
||||||
|
aliasId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type TCreateTokenForUserDTO = {
|
|||||||
type: TokenType;
|
type: TokenType;
|
||||||
userId: string;
|
userId: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
aliasId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateOrgInviteTokenDTO = {
|
export type TCreateOrgInviteTokenDTO = {
|
||||||
|
|||||||
@@ -448,15 +448,41 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
// Check if the user actually has access to the specified organization.
|
// Check if the user actually has access to the specified organization.
|
||||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
|
|
||||||
|
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||||
|
|
||||||
const selectedOrg = await orgDAL.findById(organizationId);
|
const selectedOrg = await orgDAL.findById(organizationId);
|
||||||
|
|
||||||
if (!hasOrganizationMembership) {
|
// Check if authEnforced is true, if that's the case, throw an error
|
||||||
|
if (selectedOrg.authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Authentication is required by your organization before you can log in."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedOrgMembership) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||||
|
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
||||||
|
|
||||||
|
if (!canBypass) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
||||||
|
error: "GoogleSsoEnforced"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
||||||
|
await orgDAL.updateById(selectedOrg.id, {
|
||||||
|
googleSsoAuthLastUsed: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||||
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||||
@@ -502,7 +528,8 @@ export const authLoginServiceFactory = ({
|
|||||||
selectedOrg.authEnforced &&
|
selectedOrg.authEnforced &&
|
||||||
selectedOrg.bypassOrgAuthEnabled &&
|
selectedOrg.bypassOrgAuthEnabled &&
|
||||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||||
decodedToken.authMethod !== AuthMethod.OIDC
|
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||||
|
decodedToken.authMethod !== AuthMethod.GOOGLE
|
||||||
) {
|
) {
|
||||||
await auditLogService.createAuditLog({
|
await auditLogService.createAuditLog({
|
||||||
orgId: organizationId,
|
orgId: organizationId,
|
||||||
@@ -705,7 +732,7 @@ export const authLoginServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* OAuth2 login for google,github, and other oauth2 provider
|
* OAuth2 login for google,github, and other oauth2 provider
|
||||||
* */
|
* */
|
||||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
|
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => {
|
||||||
// akhilmhdh: case sensitive email resolution
|
// akhilmhdh: case sensitive email resolution
|
||||||
const usersByUsername = await userDAL.findUserByUsername(email);
|
const usersByUsername = await userDAL.findUserByUsername(email);
|
||||||
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||||
@@ -759,6 +786,8 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
let orgId = "";
|
||||||
|
let orgName: undefined | string;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Create a new user based on oAuth
|
// Create a new user based on oAuth
|
||||||
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
||||||
@@ -784,7 +813,6 @@ export const authLoginServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||||
let orgId = "";
|
|
||||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||||
if (!defaultOrg) {
|
if (!defaultOrg) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -824,11 +852,39 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!orgId && orgSlug) {
|
||||||
|
const org = await orgDAL.findOrgBySlug(orgSlug);
|
||||||
|
|
||||||
|
if (org) {
|
||||||
|
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
|
||||||
|
const orgMembership = await orgDAL.findMembership({
|
||||||
|
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||||
|
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id,
|
||||||
|
[`${TableName.OrgMembership}.isActive` as "isActive"]: true,
|
||||||
|
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orgMembership) {
|
||||||
|
orgId = org.id;
|
||||||
|
orgName = org.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isUserCompleted = user.isAccepted;
|
const isUserCompleted = user.isAccepted;
|
||||||
const providerAuthToken = crypto.jwt().sign(
|
const providerAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
||||||
|
...(orgId && orgSlug && orgName !== undefined
|
||||||
|
? {
|
||||||
|
organizationId: orgId,
|
||||||
|
organizationName: orgName,
|
||||||
|
organizationSlug: orgSlug
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
isEmailVerified: user.isEmailVerified,
|
isEmailVerified: user.isEmailVerified,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
|
orgSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOauthTokenExchangeDTO = {
|
export type TOauthTokenExchangeDTO = {
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||||
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
||||||
@@ -176,7 +177,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
|
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||||
|
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false });
|
.where({ isGhost: false });
|
||||||
|
|
||||||
@@ -192,7 +194,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectName,
|
projectName,
|
||||||
createdAt
|
createdAt,
|
||||||
|
isActive
|
||||||
}) => ({
|
}) => ({
|
||||||
isGroupMember: true,
|
isGroupMember: true,
|
||||||
id,
|
id,
|
||||||
@@ -202,7 +205,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
id: projectId,
|
id: projectId,
|
||||||
name: projectName
|
name: projectName
|
||||||
},
|
},
|
||||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost, isOrgMembershipActive: isActive },
|
||||||
createdAt
|
createdAt
|
||||||
}),
|
}),
|
||||||
key: "id",
|
key: "id",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
@@ -189,7 +189,7 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
if (identityJwtAuth.boundClaims) {
|
if (identityJwtAuth.boundClaims) {
|
||||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
const value = getValueByDot(tokenData, claimKey);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
@@ -198,9 +198,7 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle both single and multi-valued claims
|
// handle both single and multi-valued claims
|
||||||
if (
|
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
|
||||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
|
||||||
) {
|
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
message: `Access denied: claim mismatch for field ${claimKey}`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
|
export const doesFieldValueMatchOidcPolicy = (fieldValue: string | number | boolean, policyValue: string) => {
|
||||||
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
if (typeof fieldValue === "boolean") {
|
||||||
|
return fieldValue === (policyValue === "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof fieldValue === "number") {
|
||||||
|
return fieldValue === parseInt(policyValue, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||||
|
};
|
||||||
|
|
||||||
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
|
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
|
||||||
if (Array.isArray(fieldValue)) {
|
if (Array.isArray(fieldValue)) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
@@ -146,7 +146,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
if (identityOidcAuth.boundClaims) {
|
if (identityOidcAuth.boundClaims) {
|
||||||
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
||||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
const value = getValueByDot(tokenData, claimKey);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
@@ -167,13 +167,13 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
if (identityOidcAuth.claimMetadataMapping) {
|
if (identityOidcAuth.claimMetadataMapping) {
|
||||||
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
||||||
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
||||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
const value = getValueByDot(tokenData, claimKey);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
message: `Access denied: token has no ${claimKey} field`
|
message: `Access denied: token has no ${claimKey} field`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
filteredClaims[permissionKey] = value;
|
filteredClaims[permissionKey] = value.toString();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,12 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
void qb
|
void qb
|
||||||
.whereNull(`${TableName.OrgMembership}.lastInvitedAt`)
|
.whereNull(`${TableName.OrgMembership}.lastInvitedAt`)
|
||||||
.whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]);
|
.whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]);
|
||||||
})
|
|
||||||
.orWhere((qb) => {
|
|
||||||
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
|
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
|
||||||
void qb
|
void qb.orWhere((qbInner) => {
|
||||||
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
|
void qbInner
|
||||||
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
|
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
|
||||||
|
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return memberships;
|
return memberships;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
authEnforced: true,
|
authEnforced: true,
|
||||||
|
googleSsoAuthEnforced: true,
|
||||||
scimEnabled: true,
|
scimEnabled: true,
|
||||||
kmsDefaultKeyId: true,
|
kmsDefaultKeyId: true,
|
||||||
defaultMembershipRole: true,
|
defaultMembershipRole: true,
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ export const orgServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
enforceMfa,
|
enforceMfa,
|
||||||
@@ -430,6 +431,21 @@ export const orgServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (googleSsoAuthEnforced !== undefined) {
|
||||||
|
if (!plan.enforceGoogleSSO) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authEnforced && googleSsoAuthEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (authEnforced) {
|
if (authEnforced) {
|
||||||
const samlCfg = await samlConfigDAL.findOne({
|
const samlCfg = await samlConfigDAL.findOne({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -460,6 +476,21 @@ export const orgServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (googleSsoAuthEnforced) {
|
||||||
|
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentOrg.googleSsoAuthLastUsed) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let defaultMembershipRole: string | undefined;
|
let defaultMembershipRole: string | undefined;
|
||||||
if (defaultMembershipRoleSlug) {
|
if (defaultMembershipRoleSlug) {
|
||||||
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
||||||
@@ -474,6 +505,7 @@ export const orgServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
slug: slug ? slugify(slug) : undefined,
|
slug: slug ? slugify(slug) : undefined,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
defaultMembershipRole,
|
defaultMembershipRole,
|
||||||
enforceMfa,
|
enforceMfa,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
authEnforced: boolean;
|
authEnforced: boolean;
|
||||||
|
googleSsoAuthEnforced: boolean;
|
||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
defaultMembershipRoleSlug: string;
|
defaultMembershipRoleSlug: string;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.join(TableName.OrgMembership, (qb) => {
|
||||||
|
qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn(
|
||||||
|
`${TableName.OrgMembership}.orgId`,
|
||||||
|
"=",
|
||||||
|
`${TableName.Project}.orgId`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
if (filter.usernames) {
|
if (filter.usernames) {
|
||||||
void qb.whereIn("username", filter.usernames);
|
void qb.whereIn("username", filter.usernames);
|
||||||
@@ -90,7 +98,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||||
|
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false })
|
.where({ isGhost: false })
|
||||||
.orderBy(`${TableName.Users}.username` as "username");
|
.orderBy(`${TableName.Users}.username` as "username");
|
||||||
@@ -107,12 +116,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectName,
|
projectName,
|
||||||
createdAt
|
createdAt,
|
||||||
|
isActive
|
||||||
}) => ({
|
}) => ({
|
||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
user: {
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
id: userId,
|
||||||
|
publicKey,
|
||||||
|
isGhost,
|
||||||
|
isOrgMembershipActive: isActive
|
||||||
|
},
|
||||||
project: {
|
project: {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
name: projectName
|
name: projectName
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
||||||
|
|
||||||
// projectMembers[0].project
|
|
||||||
if (includeGroupMembers) {
|
if (includeGroupMembers) {
|
||||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||||
const allMembers = [
|
const allMembers = [
|
||||||
|
|||||||
@@ -23,56 +23,120 @@ export const ChecklySyncFns = {
|
|||||||
|
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
if (config.groupId) {
|
||||||
|
// Handle group environment variables
|
||||||
|
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||||
|
secretSync.connection,
|
||||||
|
config.accountId,
|
||||||
|
config.groupId
|
||||||
|
);
|
||||||
|
|
||||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||||
|
|
||||||
for await (const key of Object.keys(secretMap)) {
|
// Prepare all variables to update at once
|
||||||
try {
|
const updatedVariables = { ...checklyGroupSecrets };
|
||||||
|
|
||||||
|
for (const key of Object.keys(secretMap)) {
|
||||||
const entry = secretMap[key];
|
const entry = secretMap[key];
|
||||||
|
|
||||||
// If value is empty, we skip the upsert - checkly does not allow empty values
|
// If value is empty, we skip adding it - checkly does not allow empty values
|
||||||
if (entry.value.trim() === "") {
|
if (entry.value.trim() === "") {
|
||||||
// Delete the secret from Checkly if its empty
|
// Delete the secret from the group if it's empty
|
||||||
if (!disableSecretDeletion) {
|
if (!disableSecretDeletion) {
|
||||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
delete updatedVariables[key];
|
||||||
key
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
continue; // Skip empty values
|
continue; // Skip empty values
|
||||||
}
|
}
|
||||||
|
|
||||||
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
// Add or update the variable
|
||||||
|
updatedVariables[key] = {
|
||||||
key,
|
key,
|
||||||
value: entry.value,
|
value: entry.value,
|
||||||
secret: true,
|
|
||||||
locked: true
|
locked: true
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove secrets that are not in the secretMap if deletion is enabled
|
||||||
|
if (!disableSecretDeletion) {
|
||||||
|
for (const key of Object.keys(checklyGroupSecrets)) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||||
|
|
||||||
|
if (!secretMap[key]) {
|
||||||
|
delete updatedVariables[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all group environment variables at once
|
||||||
|
try {
|
||||||
|
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||||
|
secretSync.connection,
|
||||||
|
config.accountId,
|
||||||
|
config.groupId,
|
||||||
|
Object.values(updatedVariables)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof SecretSyncError) throw error;
|
||||||
|
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
error,
|
||||||
secretKey: key
|
secretKey: "group_update"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Handle global variables (existing logic)
|
||||||
|
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||||
|
|
||||||
if (disableSecretDeletion) return;
|
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||||
|
|
||||||
for await (const key of Object.keys(checklySecrets)) {
|
for await (const key of Object.keys(secretMap)) {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-continue
|
const entry = secretMap[key];
|
||||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
|
||||||
|
|
||||||
if (!secretMap[key]) {
|
// If value is empty, we skip the upsert - checkly does not allow empty values
|
||||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
if (entry.value.trim() === "") {
|
||||||
key
|
// Delete the secret from Checkly if its empty
|
||||||
|
if (!disableSecretDeletion) {
|
||||||
|
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue; // Skip empty values
|
||||||
|
}
|
||||||
|
|
||||||
|
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
||||||
|
key,
|
||||||
|
value: entry.value,
|
||||||
|
secret: true,
|
||||||
|
locked: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableSecretDeletion) return;
|
||||||
|
|
||||||
|
for await (const key of Object.keys(checklySecrets)) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||||
|
|
||||||
|
if (!secretMap[key]) {
|
||||||
|
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
throw new SecretSyncError({
|
|
||||||
error,
|
|
||||||
secretKey: key
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -80,23 +144,54 @@ export const ChecklySyncFns = {
|
|||||||
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
|
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
if (config.groupId) {
|
||||||
|
// Handle group environment variables
|
||||||
|
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||||
|
secretSync.connection,
|
||||||
|
config.accountId,
|
||||||
|
config.groupId
|
||||||
|
);
|
||||||
|
|
||||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||||
|
|
||||||
|
// Filter out the secrets to remove
|
||||||
|
const remainingVariables = Object.keys(checklyGroupSecrets)
|
||||||
|
.filter((key) => !(key in secretMap))
|
||||||
|
.map((key) => checklyGroupSecrets[key]);
|
||||||
|
|
||||||
for await (const secret of Object.keys(checklySecrets)) {
|
|
||||||
try {
|
try {
|
||||||
if (secret in secretMap) {
|
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
secretSync.connection,
|
||||||
key: secret
|
config.accountId,
|
||||||
});
|
config.groupId,
|
||||||
}
|
remainingVariables
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
error,
|
||||||
secretKey: secret
|
secretKey: "group_remove"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Handle global variables (existing logic)
|
||||||
|
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||||
|
|
||||||
|
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||||
|
|
||||||
|
for await (const secret of Object.keys(checklySecrets)) {
|
||||||
|
try {
|
||||||
|
if (secret in secretMap) {
|
||||||
|
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||||
|
key: secret
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: secret
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,17 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
|||||||
|
|
||||||
const ChecklySyncDestinationConfigSchema = z.object({
|
const ChecklySyncDestinationConfigSchema = z.object({
|
||||||
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
|
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
|
||||||
accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters")
|
accountName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Account Name is required")
|
||||||
|
.max(255, "Account ID must be less than 255 characters")
|
||||||
|
.optional(),
|
||||||
|
groupId: z.string().min(1, "Group ID is required").max(255, "Group ID must be less than 255 characters").optional(),
|
||||||
|
groupName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Group Name is required")
|
||||||
|
.max(255, "Group Name must be less than 255 characters")
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export const GithubSyncFns = {
|
|||||||
const token =
|
const token =
|
||||||
connection.method === GitHubConnectionMethod.OAuth
|
connection.method === GitHubConnectionMethod.OAuth
|
||||||
? connection.credentials.accessToken
|
? connection.credentials.accessToken
|
||||||
: await getGitHubAppAuthToken(connection);
|
: await getGitHubAppAuthToken(connection, gatewayService);
|
||||||
|
|
||||||
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||||
const publicKey = await getPublicKey(secretSync, gatewayService, token);
|
const publicKey = await getPublicKey(secretSync, gatewayService, token);
|
||||||
@@ -264,7 +264,7 @@ export const GithubSyncFns = {
|
|||||||
const token =
|
const token =
|
||||||
connection.method === GitHubConnectionMethod.OAuth
|
connection.method === GitHubConnectionMethod.OAuth
|
||||||
? connection.credentials.accessToken
|
? connection.credentials.accessToken
|
||||||
: await getGitHubAppAuthToken(connection);
|
: await getGitHubAppAuthToken(connection, gatewayService);
|
||||||
|
|
||||||
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum RenderSyncScope {
|
export enum RenderSyncScope {
|
||||||
Service = "service"
|
Service = "service",
|
||||||
|
EnvironmentGroup = "environment-group"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RenderSyncType {
|
export enum RenderSyncType {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { isAxiosError } from "axios";
|
import { AxiosRequestConfig, isAxiosError } from "axios";
|
||||||
|
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { RenderSyncScope } from "./render-sync-enums";
|
||||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
|
||||||
|
const req: AxiosRequestConfig = {
|
||||||
|
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${input.token}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (input.destination.scope) {
|
||||||
|
case RenderSyncScope.Service: {
|
||||||
|
req.url = `/services/${input.destination.serviceId}/env-vars`;
|
||||||
|
|
||||||
|
const allSecrets: TRenderSecret[] = [];
|
||||||
|
let cursor: string | undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
|
const { data } = await makeRequestWithRetry(() =>
|
||||||
|
request.request<
|
||||||
|
{
|
||||||
|
envVar: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
cursor: string;
|
||||||
|
}[]
|
||||||
|
>({
|
||||||
|
...req,
|
||||||
|
params: {
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const secrets = data.map((item) => ({
|
||||||
|
key: item.envVar.key,
|
||||||
|
value: item.envVar.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
allSecrets.push(...secrets);
|
||||||
|
|
||||||
|
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||||
|
cursor = data[data.length - 1].cursor;
|
||||||
|
} else {
|
||||||
|
cursor = undefined;
|
||||||
|
}
|
||||||
|
} while (cursor);
|
||||||
|
|
||||||
|
return allSecrets;
|
||||||
|
}
|
||||||
|
case RenderSyncScope.EnvironmentGroup: {
|
||||||
|
req.url = `/env-groups/${input.destination.environmentGroupId}`;
|
||||||
|
|
||||||
|
const res = await makeRequestWithRetry(() =>
|
||||||
|
request.request<{
|
||||||
|
envVars: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}>(req)
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.envVars.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
value: item.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||||
const {
|
const {
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
|
const secrets = await getSecrets({
|
||||||
const allSecrets: TRenderSecret[] = [];
|
destination: destinationConfig,
|
||||||
let cursor: string | undefined;
|
token: apiKey
|
||||||
|
});
|
||||||
|
|
||||||
do {
|
return secrets;
|
||||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
|
||||||
|
|
||||||
const { data } = await makeRequestWithRetry(() =>
|
|
||||||
request.get<
|
|
||||||
{
|
|
||||||
envVar: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
cursor: string;
|
|
||||||
}[]
|
|
||||||
>(url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
Accept: "application/json"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const secrets = data.map((item) => ({
|
|
||||||
key: item.envVar.key,
|
|
||||||
value: item.envVar.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
allSecrets.push(...secrets);
|
|
||||||
|
|
||||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
|
||||||
cursor = data[data.length - 1].cursor;
|
|
||||||
} else {
|
|
||||||
cursor = undefined;
|
|
||||||
}
|
|
||||||
} while (cursor);
|
|
||||||
|
|
||||||
return allSecrets;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const batchUpdateEnvironmentSecrets = async (
|
const batchUpdateEnvironmentSecrets = async (
|
||||||
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
await makeRequestWithRetry(() =>
|
const req: AxiosRequestConfig = {
|
||||||
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||||
headers: {
|
method: "PUT",
|
||||||
Authorization: `Bearer ${apiKey}`,
|
headers: {
|
||||||
Accept: "application/json"
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (destinationConfig.scope) {
|
||||||
|
case RenderSyncScope.Service: {
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
url: `/services/${destinationConfig.serviceId}/env-vars`,
|
||||||
|
data: envVars
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RenderSyncScope.EnvironmentGroup: {
|
||||||
|
for await (const variable of envVars) {
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
|
||||||
|
data: {
|
||||||
|
value: variable.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
break;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEnvironmentSecret = async (
|
||||||
|
secretSync: TRenderSyncWithCredentials,
|
||||||
|
envVar: { key: string; value: string }
|
||||||
|
): Promise<void> => {
|
||||||
|
const {
|
||||||
|
destinationConfig,
|
||||||
|
connection: {
|
||||||
|
credentials: { apiKey }
|
||||||
|
}
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const req: AxiosRequestConfig = {
|
||||||
|
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (destinationConfig.scope) {
|
||||||
|
case RenderSyncScope.Service: {
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RenderSyncScope.EnvironmentGroup: {
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||||
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
await makeRequestWithRetry(() =>
|
const req: AxiosRequestConfig = {
|
||||||
request.post(
|
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
headers: {
|
||||||
{},
|
Authorization: `Bearer ${apiKey}`,
|
||||||
{
|
Accept: "application/json"
|
||||||
headers: {
|
}
|
||||||
Authorization: `Bearer ${apiKey}`,
|
};
|
||||||
Accept: "application/json"
|
|
||||||
}
|
switch (destinationConfig.scope) {
|
||||||
|
case RenderSyncScope.Service: {
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
method: "POST",
|
||||||
|
url: `/services/${destinationConfig.serviceId}/deploys`,
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RenderSyncScope.EnvironmentGroup: {
|
||||||
|
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
|
||||||
|
...req,
|
||||||
|
method: "GET",
|
||||||
|
url: `/env-groups/${destinationConfig.environmentGroupId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const link of data.serviceLinks) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
|
await makeRequestWithRetry(() =>
|
||||||
|
request.request({
|
||||||
|
...req,
|
||||||
|
url: `/services/${link.id}/deploys`,
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
break;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RenderSyncFns = {
|
export const RenderSyncFns = {
|
||||||
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
|
|||||||
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||||
|
|
||||||
for (const renderSecret of renderSecrets) {
|
for (const renderSecret of renderSecrets) {
|
||||||
if (!(renderSecret.key in secretMap)) {
|
if (renderSecret.key in secretMap) {
|
||||||
finalEnvVars.push({
|
finalEnvVars.push({
|
||||||
key: renderSecret.key,
|
key: renderSecret.key,
|
||||||
value: renderSecret.value
|
value: renderSecret.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
|
||||||
|
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
|
||||||
|
|
||||||
if (secretSync.syncOptions.autoRedeployServices) {
|
if (secretSync.syncOptions.autoRedeployServices) {
|
||||||
await redeployService(secretSync);
|
await redeployService(secretSync);
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
|||||||
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||||
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
||||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||||
|
environmentGroupId: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Environment Group ID is required")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
|
||||||
|
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -684,9 +684,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
throw new BadRequestError({ message: "Missing personal user id" });
|
throw new BadRequestError({ message: "Missing personal user id" });
|
||||||
}
|
}
|
||||||
void bd.orWhere({
|
void bd.orWhere({
|
||||||
key: el.key,
|
[`${TableName.SecretV2}.key` as "key"]: el.key,
|
||||||
type: el.type,
|
[`${TableName.SecretV2}.type` as "type"]: el.type,
|
||||||
userId: el.type === SecretType.Personal ? el.userId : null
|
[`${TableName.SecretV2}.userId` as "userId"]: el.type === SecretType.Personal ? el.userId : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -695,12 +695,60 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
`${TableName.SecretV2}.id`,
|
`${TableName.SecretV2}.id`,
|
||||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretV2JnTag,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretV2))
|
.select(selectAllTableCols(TableName.SecretV2))
|
||||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||||
return secrets.map((secret) => ({
|
|
||||||
...secret,
|
const docs = sqlNestRelationships({
|
||||||
isRotatedSecret: Boolean(secret.rotationId)
|
data: secrets,
|
||||||
}));
|
key: "id",
|
||||||
|
parentMapper: (secret) => ({
|
||||||
|
...secret,
|
||||||
|
isRotatedSecret: Boolean(secret.rotationId)
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "secretMetadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "find by secret keys" });
|
throw new DatabaseError({ error, name: "find by secret keys" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1074,12 +1074,22 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
currentPath: path
|
currentPath: path
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deepPaths) return { secrets: [], imports: [] };
|
if (!deepPaths?.length) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||||
|
name: "SecretPathNotFound"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
||||||
} else {
|
} else {
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) return { secrets: [], imports: [] };
|
if (!folder) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||||
|
name: "SecretPathNotFound"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
paths = [{ folderId: folder.id, path }];
|
paths = [{ folderId: folder.id, path }];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -637,7 +637,12 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deepPaths) return { secrets: [], imports: [] };
|
if (!deepPaths?.length) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||||
|
name: "SecretPathNotFound"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
||||||
} else {
|
} else {
|
||||||
@@ -647,7 +652,12 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) return { secrets: [], imports: [] };
|
if (!folder) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||||
|
name: "SecretPathNotFound"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
paths = [{ folderId: folder.id, path }];
|
paths = [{ folderId: folder.id, path }];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
@@ -9,9 +10,10 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
|
|||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
import { TListUserGroupsDTO, TUpdateUserMfaDTO } from "./user-types";
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ type TUserServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
|
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "find" | "updateById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
@@ -48,22 +51,38 @@ export const userServiceFactory = ({
|
|||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService,
|
smtpService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
userAliasDAL
|
||||||
}: TUserServiceFactoryDep) => {
|
}: TUserServiceFactoryDep) => {
|
||||||
const sendEmailVerificationCode = async (username: string) => {
|
const sendEmailVerificationCode = async (token: string) => {
|
||||||
|
const { authType, aliasId, username, authTokenType } = crypto.jwt().decode(token) as {
|
||||||
|
authType: string;
|
||||||
|
aliasId?: string;
|
||||||
|
username: string;
|
||||||
|
authTokenType: AuthTokenType;
|
||||||
|
};
|
||||||
|
if (authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new BadRequestError({ name: "Invalid auth token type" });
|
||||||
|
|
||||||
// akhilmhdh: case sensitive email resolution
|
// akhilmhdh: case sensitive email resolution
|
||||||
const users = await userDAL.findUserByUsername(username);
|
const users = await userDAL.findUserByUsername(username);
|
||||||
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
|
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
|
||||||
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
||||||
|
let { isEmailVerified } = user;
|
||||||
|
if (aliasId) {
|
||||||
|
const userAlias = await userAliasDAL.findOne({ userId: user.id, aliasType: authType, id: aliasId });
|
||||||
|
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${aliasId}' not found` });
|
||||||
|
isEmailVerified = userAlias.isEmailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.email)
|
if (!user.email)
|
||||||
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
|
||||||
if (user.isEmailVerified)
|
if (isEmailVerified)
|
||||||
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
|
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
|
||||||
|
|
||||||
const token = await tokenService.createTokenForUser({
|
const userToken = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
aliasId
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
@@ -71,7 +90,7 @@ export const userServiceFactory = ({
|
|||||||
subjectLine: "Infisical confirmation code",
|
subjectLine: "Infisical confirmation code",
|
||||||
recipients: [user.email],
|
recipients: [user.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
code: userToken
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -95,15 +114,21 @@ export const userServiceFactory = ({
|
|||||||
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
|
||||||
if (!user.email)
|
if (!user.email)
|
||||||
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
|
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
|
||||||
if (user.isEmailVerified)
|
|
||||||
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
|
||||||
|
|
||||||
await tokenService.validateTokenForUser({
|
const token = await tokenService.validateTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (token?.aliasId) {
|
||||||
|
const userAlias = await userAliasDAL.findOne({ userId: user.id, id: token.aliasId });
|
||||||
|
if (!userAlias) throw new NotFoundError({ name: `User alias with ID '${token.aliasId}' not found` });
|
||||||
|
if (userAlias?.isEmailVerified)
|
||||||
|
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
|
||||||
|
|
||||||
|
await userAliasDAL.updateById(token.aliasId, { isEmailVerified: true });
|
||||||
|
}
|
||||||
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
|
||||||
|
|
||||||
await userDAL.updateById(user.id, {
|
await userDAL.updateById(user.id, {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ This functionality works in the following way:
|
|||||||
{/*  */}
|
{/*  */}
|
||||||

|

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

|
||||||
|
</Note>
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
If the access request matches with a policy that allows break-glass approval
|
If the access request matches with a policy that allows break-glass approval
|
||||||
bypasses, the requester may bypass the policy and get access to the resource
|
bypasses, the requester may bypass the policy and get access to the resource
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 638 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 592 KiB |
@@ -27,22 +27,73 @@ $ ansible-galaxy collection install infisical.vault
|
|||||||
The python module dependencies are not installed by ansible-galaxy. They can be manually installed using pip:
|
The python module dependencies are not installed by ansible-galaxy. They can be manually installed using pip:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pip install infisical-python
|
$ pip install infisicalsdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using this collection
|
## Using this collection
|
||||||
|
|
||||||
You can either call modules by their Fully Qualified Collection Name (FQCN), such as `infisical.vault.read_secrets`, or you can call modules by their short name if you list the `infisical.vault` collection in the playbook's collections keyword:
|
You can either call modules by their Fully Qualified Collection Name (FQCN), such as `infisical.vault.read_secrets`, or you can call modules by their short name if you list the `infisical.vault` collection in the playbook's collections keyword:
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
```bash
|
The Infisical Ansible Collection supports [Universal Auth](/documentation/platform/identities/universal-auth) and [OIDC](/documentation/platform/identities/oidc-auth/general) for authenticating against Infisical.
|
||||||
|
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Universal Auth">
|
||||||
|
Using Universal Auth for authentication is the most straight-forward way to get started with using the Ansible collection.
|
||||||
|
|
||||||
|
To use Universal Auth, you need to provide the Client ID and Client Secret of your Infisical Machine Identity.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
lookup('infisical.vault.read_secrets', auth_method="universal-auth", universal_auth_client_id='<client-id>', universal_auth_client_secret='<client-secret>' ...rest)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also provide the `auth_method`, `universal_auth_client_id`, and `universal_auth_client_secret` parameters through environment variables:
|
||||||
|
|
||||||
|
| Parameter Name | Environment Variable Name |
|
||||||
|
| ------------------------------ | ---------------------------------------- |
|
||||||
|
| `auth_method` | `INFISICAL_AUTH_METHOD` |
|
||||||
|
| `universal_auth_client_id` | `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` |
|
||||||
|
| `universal_auth_client_secret` | `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` |
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="OIDC Auth">
|
||||||
|
To use OIDC Auth, you'll need to provide the ID of your machine identity, and the OIDC JWT to be used for authentication.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Please note that in order to use OIDC Auth, you must have `1.0.10` or newer of the `infisicalsdk` package installed.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
lookup('infisical.vault.read_secrets', auth_method="oidc-auth", identity_id='<identity-id>', jwt='<oidc-jwt>' ...rest)
|
||||||
|
```
|
||||||
|
You can also provide the `auth_method`, `identity_id`, and `jwt` parameters through environment variables:
|
||||||
|
|
||||||
|
| Parameter Name | Environment Variable Name |
|
||||||
|
| --------------- | ------------------------- |
|
||||||
|
| auth_method | `INFISICAL_AUTH_METHOD` |
|
||||||
|
| identity_id | `INFISICAL_IDENTITY_ID` |
|
||||||
|
| jwt | `INFISICAL_JWT` |
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```yaml
|
||||||
---
|
---
|
||||||
vars:
|
vars:
|
||||||
read_all_secrets_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', url='https://spotify.infisical.com') }}"
|
read_all_secrets_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', url='https://spotify.infisical.com') }}"
|
||||||
# [{ "key": "HOST", "value": "google.com" }, { "key": "SMTP", "value": "gmail.smtp.edu" }]
|
# [{ "key": "HOST", "value": "google.com" }, { "key": "SMTP", "value": "gmail.smtp.edu" }]
|
||||||
|
|
||||||
|
|
||||||
|
read_all_secrets_as_dict: "{{ lookup('infisical.vault.read_secrets', as_dict=True, universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', url='https://spotify.infisical.com') }}"
|
||||||
|
# { "SECRET_KEY_1": "secret-value-1", "SECRET_KEY_2": "secret-value-2" } -> Can be accessed as secrets.SECRET_KEY_1
|
||||||
|
|
||||||
|
|
||||||
read_secret_by_name_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', secret_name='HOST', url='https://spotify.infisical.com') }}"
|
read_secret_by_name_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', secret_name='HOST', url='https://spotify.infisical.com') }}"
|
||||||
# [{ "key": "HOST", "value": "google.com" }]
|
# { "key": "HOST", "value": "google.com" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,32 +6,10 @@ description: "Learn how to use Infisical to inject environment variables into a
|
|||||||
This approach allows you to inject secrets from Infisical directly into your application.
|
This approach allows you to inject secrets from Infisical directly into your application.
|
||||||
This is achieved by installing the Infisical CLI into your docker image and modifying your start command to execute with Infisical.
|
This is achieved by installing the Infisical CLI into your docker image and modifying your start command to execute with Infisical.
|
||||||
|
|
||||||
## Add the Infisical CLI to your Dockerfile
|
## Install the Infisical CLI to your Dockerfile
|
||||||
|
|
||||||
<Tabs>
|
To install the CLI, follow the instructions for your chosen distribution [here](/cli/overview).
|
||||||
<Tab title="Alpine">
|
|
||||||
```dockerfile
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
|
||||||
&& apk add infisical
|
|
||||||
```
|
|
||||||
|
|
||||||
</Tab>
|
|
||||||
<Tab title="RedHat/CentOs/Amazon-linux">
|
|
||||||
```dockerfile
|
|
||||||
RUN curl -1sLf \
|
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' | sh \
|
|
||||||
&& yum install -y infisical
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Debian/Ubuntu">
|
|
||||||
```dockerfile
|
|
||||||
RUN apt-get update && apt-get install -y bash curl && curl -1sLf \
|
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
|
||||||
&& apt-get update && apt-get install -y infisical
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
####
|
####
|
||||||
<Tip>
|
<Tip>
|
||||||
We recommend you to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
|
We recommend you to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ description: "Learn how to configure a Checkly Sync for Infisical."
|
|||||||
|
|
||||||
- **Checkly Connection**: The Checkly Connection to authenticate with.
|
- **Checkly Connection**: The Checkly Connection to authenticate with.
|
||||||
- **Account**: The Checkly account to sync secrets to.
|
- **Account**: The Checkly account to sync secrets to.
|
||||||
|
- **Group**: The Checkly check group to sync secrets to (Optional).
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Configure Sync Options">
|
<Step title="Configure Sync Options">
|
||||||
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ description: "Learn how to configure a Render Sync for Infisical."
|
|||||||

|

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

|

|
||||||
|
|||||||
+46
-2
@@ -5,14 +5,15 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
|
|||||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
TChecklyAccount,
|
TChecklyAccount,
|
||||||
useChecklyConnectionListAccounts
|
useChecklyConnectionListAccounts,
|
||||||
|
useChecklyConnectionListGroups
|
||||||
} from "@app/hooks/api/appConnections/checkly";
|
} from "@app/hooks/api/appConnections/checkly";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
export const ChecklySyncFields = () => {
|
export const ChecklySyncFields = () => {
|
||||||
const { control, setValue } = useFormContext<
|
const { control, setValue, watch } = useFormContext<
|
||||||
TSecretSyncForm & { destination: SecretSync.Checkly }
|
TSecretSyncForm & { destination: SecretSync.Checkly }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -25,12 +26,24 @@ export const ChecklySyncFields = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const accountId = watch("destinationConfig.accountId");
|
||||||
|
|
||||||
|
const { data: groups = [], isPending: isGroupsLoading } = useChecklyConnectionListGroups(
|
||||||
|
connectionId,
|
||||||
|
accountId,
|
||||||
|
{
|
||||||
|
enabled: Boolean(connectionId && accountId)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SecretSyncConnectionField
|
<SecretSyncConnectionField
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setValue("destinationConfig.accountId", "");
|
setValue("destinationConfig.accountId", "");
|
||||||
setValue("destinationConfig.accountName", "");
|
setValue("destinationConfig.accountName", "");
|
||||||
|
setValue("destinationConfig.groupId", undefined);
|
||||||
|
setValue("destinationConfig.groupName", undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -60,6 +73,37 @@ export const ChecklySyncFields = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.groupId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Select a group"
|
||||||
|
isOptional
|
||||||
|
helperText="If provided, secrets will be scoped to a check group instead"
|
||||||
|
tooltipClassName="max-w-md"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isLoading={isGroupsLoading && Boolean(connectionId)}
|
||||||
|
isDisabled={!connectionId}
|
||||||
|
isClearable
|
||||||
|
value={groups.find((p) => p.id === value) ?? null}
|
||||||
|
onChange={(option) => {
|
||||||
|
const v = option as SingleValue<TChecklyAccount>;
|
||||||
|
onChange(v?.id ?? null);
|
||||||
|
setValue("destinationConfig.groupName", v?.name ?? undefined);
|
||||||
|
}}
|
||||||
|
options={groups}
|
||||||
|
placeholder="Select a group..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+70
-24
@@ -5,7 +5,9 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se
|
|||||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
import { RENDER_SYNC_SCOPES } from "@app/helpers/secretSyncs";
|
||||||
import {
|
import {
|
||||||
|
TRenderEnvironmentGroup,
|
||||||
TRenderService,
|
TRenderService,
|
||||||
|
useRenderConnectionListEnvironmentGroups,
|
||||||
useRenderConnectionListServices
|
useRenderConnectionListServices
|
||||||
} from "@app/hooks/api/appConnections/render";
|
} from "@app/hooks/api/appConnections/render";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
@@ -19,6 +21,7 @@ export const RenderSyncFields = () => {
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
const connectionId = useWatch({ name: "connection.id", control });
|
const connectionId = useWatch({ name: "connection.id", control });
|
||||||
|
const selectedScope = useWatch({ name: "destinationConfig.scope", control });
|
||||||
|
|
||||||
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
|
const { data: services = [], isPending: isServicesPending } = useRenderConnectionListServices(
|
||||||
connectionId,
|
connectionId,
|
||||||
@@ -27,11 +30,17 @@ export const RenderSyncFields = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: groups = [], isPending: isGroupsPending } =
|
||||||
|
useRenderConnectionListEnvironmentGroups(connectionId, {
|
||||||
|
enabled: Boolean(connectionId) && selectedScope === RenderSyncScope.EnvironmentGroup
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SecretSyncConnectionField
|
<SecretSyncConnectionField
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setValue("destinationConfig.serviceId", "");
|
setValue("destinationConfig.serviceId", "");
|
||||||
|
setValue("destinationConfig.environmentGroupId", "");
|
||||||
setValue("destinationConfig.type", RenderSyncType.Env);
|
setValue("destinationConfig.type", RenderSyncType.Env);
|
||||||
setValue("destinationConfig.scope", RenderSyncScope.Service);
|
setValue("destinationConfig.scope", RenderSyncScope.Service);
|
||||||
}}
|
}}
|
||||||
@@ -83,30 +92,67 @@ export const RenderSyncFields = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
{selectedScope === RenderSyncScope.Service && (
|
||||||
name="destinationConfig.serviceId"
|
<Controller
|
||||||
control={control}
|
name="destinationConfig.serviceId"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
control={control}
|
||||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Service">
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FilterableSelect
|
<FormControl
|
||||||
isLoading={isServicesPending && Boolean(connectionId)}
|
errorText={error?.message}
|
||||||
isDisabled={!connectionId}
|
isError={Boolean(error?.message)}
|
||||||
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
label="Service"
|
||||||
onChange={(option) => {
|
>
|
||||||
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
<FilterableSelect
|
||||||
setValue(
|
isLoading={isServicesPending && Boolean(connectionId)}
|
||||||
"destinationConfig.serviceName",
|
isDisabled={!connectionId}
|
||||||
(option as SingleValue<TRenderService>)?.name ?? ""
|
value={services ? (services.find((service) => service.id === value) ?? []) : []}
|
||||||
);
|
onChange={(option) => {
|
||||||
}}
|
onChange((option as SingleValue<TRenderService>)?.id ?? null);
|
||||||
options={services}
|
setValue(
|
||||||
placeholder="Select a service..."
|
"destinationConfig.serviceName",
|
||||||
getOptionLabel={(option) => option.name}
|
(option as SingleValue<TRenderService>)?.name ?? ""
|
||||||
getOptionValue={(option) => option.id.toString()}
|
);
|
||||||
/>
|
}}
|
||||||
</FormControl>
|
options={services}
|
||||||
)}
|
placeholder="Select a service..."
|
||||||
/>
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id.toString()}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedScope === RenderSyncScope.EnvironmentGroup && (
|
||||||
|
<Controller
|
||||||
|
name="destinationConfig.environmentGroupId"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Environment Group"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isLoading={isGroupsPending && Boolean(connectionId)}
|
||||||
|
isDisabled={!connectionId}
|
||||||
|
value={groups ? (groups.find((g) => g.id === value) ?? []) : []}
|
||||||
|
onChange={(option) => {
|
||||||
|
onChange((option as SingleValue<TRenderEnvironmentGroup>)?.id ?? null);
|
||||||
|
setValue(
|
||||||
|
"destinationConfig.environmentGroupName",
|
||||||
|
(option as SingleValue<TRenderEnvironmentGroup>)?.name ?? ""
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={groups}
|
||||||
|
placeholder="Select an environment group..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id.toString()}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-2
@@ -6,7 +6,16 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
|
|||||||
|
|
||||||
export const ChecklySyncReviewFields = () => {
|
export const ChecklySyncReviewFields = () => {
|
||||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Checkly }>();
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Checkly }>();
|
||||||
const accountName = watch("destinationConfig.accountName");
|
const config = watch("destinationConfig");
|
||||||
|
|
||||||
return <GenericFieldLabel label="Account">{accountName}</GenericFieldLabel>;
|
return (
|
||||||
|
<>
|
||||||
|
<GenericFieldLabel label="Account">
|
||||||
|
{config.accountName ?? config.accountId}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
{config.groupId && (
|
||||||
|
<GenericFieldLabel label="Group">{config.groupName ?? config.groupId}</GenericFieldLabel>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-4
@@ -4,6 +4,7 @@ import { GenericFieldLabel } from "@app/components/secret-syncs";
|
|||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
import { Badge } from "@app/components/v2";
|
import { Badge } from "@app/components/v2";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
export const RenderSyncOptionsReviewFields = () => {
|
export const RenderSyncOptionsReviewFields = () => {
|
||||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||||
@@ -27,13 +28,20 @@ export const RenderSyncOptionsReviewFields = () => {
|
|||||||
|
|
||||||
export const RenderSyncReviewFields = () => {
|
export const RenderSyncReviewFields = () => {
|
||||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||||
const serviceName = watch("destinationConfig.serviceName");
|
const config = watch("destinationConfig");
|
||||||
const scope = watch("destinationConfig.scope");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
|
<GenericFieldLabel label="Scope">{config.scope}</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Service">{serviceName}</GenericFieldLabel>
|
{config.scope === RenderSyncScope.Service ? (
|
||||||
|
<GenericFieldLabel label="Service">
|
||||||
|
{config.serviceName ?? config.serviceId}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
) : (
|
||||||
|
<GenericFieldLabel label="Service">
|
||||||
|
{config.environmentGroupName ?? config.environmentGroupId}
|
||||||
|
</GenericFieldLabel>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -8,7 +8,15 @@ export const ChecklySyncDestinationSchema = BaseSecretSyncSchema().merge(
|
|||||||
destination: z.literal(SecretSync.Checkly),
|
destination: z.literal(SecretSync.Checkly),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
accountId: z.string(),
|
accountId: z.string(),
|
||||||
accountName: z.string()
|
accountName: z.string(),
|
||||||
|
groupId: z
|
||||||
|
.string()
|
||||||
|
.nullish()
|
||||||
|
.transform((val) => val || undefined),
|
||||||
|
groupName: z
|
||||||
|
.string()
|
||||||
|
.nullish()
|
||||||
|
.transform((val) => val || undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
|||||||
serviceId: z.string().trim().min(1, "Service is required"),
|
serviceId: z.string().trim().min(1, "Service is required"),
|
||||||
serviceName: z.string().trim().optional(),
|
serviceName: z.string().trim().optional(),
|
||||||
type: z.nativeEnum(RenderSyncType)
|
type: z.nativeEnum(RenderSyncType)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(RenderSyncScope.EnvironmentGroup),
|
||||||
|
environmentGroupId: z.string().trim().min(1, "Environment Group ID is required"),
|
||||||
|
environmentGroupName: z.string().trim().optional(),
|
||||||
|
type: z.nativeEnum(RenderSyncType)
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const DatePicker = ({
|
|||||||
>
|
>
|
||||||
{value
|
{value
|
||||||
? formatDateTime({ timestamp: value, timezone, dateFormat })
|
? formatDateTime({ timestamp: value, timezone, dateFormat })
|
||||||
: "Pick a date and time"}
|
: `Select Date${hideTime ? "" : " and Time"}`}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
@@ -122,7 +122,8 @@ export const DatePicker = ({
|
|||||||
root: `text-mineshaft-300 ${defaultClassNames}`,
|
root: `text-mineshaft-300 ${defaultClassNames}`,
|
||||||
[UI.DayButton]: "p-3 rounded hover:text-mineshaft-100",
|
[UI.DayButton]: "p-3 rounded hover:text-mineshaft-100",
|
||||||
[UI.Weekday]: "px-3 pt-3",
|
[UI.Weekday]: "px-3 pt-3",
|
||||||
[UI.Chevron]: "fill-mineshaft-300"
|
[UI.Chevron]: "fill-mineshaft-300/70 hover:fill-mineshaft-300",
|
||||||
|
disabled: "text-mineshaft-400 pointer-events-none"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -212,5 +212,9 @@ export const RENDER_SYNC_SCOPES: Record<RenderSyncScope, { name: string; descrip
|
|||||||
[RenderSyncScope.Service]: {
|
[RenderSyncScope.Service]: {
|
||||||
name: "Service",
|
name: "Service",
|
||||||
description: "Infisical will sync secrets to the specified Render service."
|
description: "Infisical will sync secrets to the specified Render service."
|
||||||
|
},
|
||||||
|
[RenderSyncScope.EnvironmentGroup]: {
|
||||||
|
name: "EnvironmentGroup",
|
||||||
|
description: "Infisical will sync secrets to the specified Render environment group."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Approver = {
|
|||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
sequence?: number;
|
sequence?: number;
|
||||||
approvalsRequired?: number;
|
approvalsRequired?: number;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Bypasser = {
|
export type Bypasser = {
|
||||||
@@ -82,6 +83,7 @@ export type TAccessApprovalRequest = {
|
|||||||
name: string;
|
name: string;
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers: {
|
approvers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
sequence?: number;
|
sequence?: number;
|
||||||
approvalsRequired?: number;
|
approvalsRequired?: number;
|
||||||
@@ -98,6 +100,7 @@ export type TAccessApprovalRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reviewers: {
|
reviewers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
status: string;
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -177,7 +180,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
@@ -190,7 +193,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
export type TUpdateAccessPolicyDTO = {
|
export type TUpdateAccessPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
environments?: string[];
|
environments?: string[];
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { TChecklyAccount } from "./types";
|
|||||||
const checklyConnectionKeys = {
|
const checklyConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "checkly"] as const,
|
all: [...appConnectionKeys.all, "checkly"] as const,
|
||||||
listAccounts: (connectionId: string) =>
|
listAccounts: (connectionId: string) =>
|
||||||
[...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const
|
[...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const,
|
||||||
|
listGroups: (connectionId: string, accountId: string) =>
|
||||||
|
[...checklyConnectionKeys.all, "groups", connectionId, accountId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useChecklyConnectionListAccounts = (
|
export const useChecklyConnectionListAccounts = (
|
||||||
@@ -35,3 +37,29 @@ export const useChecklyConnectionListAccounts = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useChecklyConnectionListGroups = (
|
||||||
|
connectionId: string,
|
||||||
|
accountId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TChecklyAccount[],
|
||||||
|
unknown,
|
||||||
|
TChecklyAccount[],
|
||||||
|
ReturnType<typeof checklyConnectionKeys.listGroups>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: checklyConnectionKeys.listGroups(connectionId, accountId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ groups: TChecklyAccount[] }>(
|
||||||
|
`/api/v1/app-connections/checkly/${connectionId}/accounts/${accountId}/groups`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.groups;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { appConnectionKeys } from "../queries";
|
import { appConnectionKeys } from "../queries";
|
||||||
import { TRenderService } from "./types";
|
import { TRenderEnvironmentGroup, TRenderService } from "./types";
|
||||||
|
|
||||||
const renderConnectionKeys = {
|
const renderConnectionKeys = {
|
||||||
all: [...appConnectionKeys.all, "render"] as const,
|
all: [...appConnectionKeys.all, "render"] as const,
|
||||||
listServices: (connectionId: string) =>
|
listServices: (connectionId: string) =>
|
||||||
[...renderConnectionKeys.all, "services", connectionId] as const
|
[...renderConnectionKeys.all, "services", connectionId] as const,
|
||||||
|
listEnvironmentGroups: (connectionId: string) =>
|
||||||
|
[...renderConnectionKeys.all, "environment-groups", connectionId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRenderConnectionListServices = (
|
export const useRenderConnectionListServices = (
|
||||||
@@ -35,3 +37,28 @@ export const useRenderConnectionListServices = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useRenderConnectionListEnvironmentGroups = (
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TRenderEnvironmentGroup[],
|
||||||
|
unknown,
|
||||||
|
TRenderEnvironmentGroup[],
|
||||||
|
ReturnType<typeof renderConnectionKeys.listEnvironmentGroups>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: renderConnectionKeys.listEnvironmentGroups(connectionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TRenderEnvironmentGroup[]>(
|
||||||
|
`/api/v1/app-connections/render/${connectionId}/environment-groups`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ export type TRenderService = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TRenderEnvironmentGroup = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export const useUpdateOrg = () => {
|
|||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
name,
|
name,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
slug,
|
slug,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -125,6 +126,7 @@ export const useUpdateOrg = () => {
|
|||||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||||
name,
|
name,
|
||||||
authEnforced,
|
authEnforced,
|
||||||
|
googleSsoAuthEnforced,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
slug,
|
slug,
|
||||||
defaultMembershipRoleSlug,
|
defaultMembershipRoleSlug,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Organization = {
|
|||||||
createAt: string;
|
createAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
authEnforced: boolean;
|
authEnforced: boolean;
|
||||||
|
googleSsoAuthEnforced: boolean;
|
||||||
bypassOrgAuthEnabled: boolean;
|
bypassOrgAuthEnabled: boolean;
|
||||||
orgAuthMethod: string;
|
orgAuthMethod: string;
|
||||||
scimEnabled: boolean;
|
scimEnabled: boolean;
|
||||||
@@ -34,6 +35,7 @@ export type UpdateOrgDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
authEnforced?: boolean;
|
authEnforced?: boolean;
|
||||||
|
googleSsoAuthEnforced?: boolean;
|
||||||
scimEnabled?: boolean;
|
scimEnabled?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
defaultMembershipRoleSlug?: string;
|
defaultMembershipRoleSlug?: string;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum ApproverType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Approver = {
|
export type Approver = {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
};
|
};
|
||||||
@@ -49,7 +50,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@@ -59,7 +60,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
export type TUpdateSecretPolicyDTO = {
|
export type TUpdateSecretPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Omit<Approver, "isOrgMembershipActive">[];
|
||||||
bypassers?: Bypasser[];
|
bypassers?: Bypasser[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type TSecretApprovalRequest = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
}[];
|
}[];
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -62,6 +63,7 @@ export type TSecretApprovalRequest = {
|
|||||||
status: "open" | "close";
|
status: "open" | "close";
|
||||||
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
||||||
approvers: {
|
approvers: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
|||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
|
export enum ChecklySyncScope {
|
||||||
|
Global = "global",
|
||||||
|
Group = "group"
|
||||||
|
}
|
||||||
export type TChecklySync = TRootSecretSync & {
|
export type TChecklySync = TRootSecretSync & {
|
||||||
destination: SecretSync.Checkly;
|
destination: SecretSync.Checkly;
|
||||||
destinationConfig: {
|
destinationConfig: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
accountName: string;
|
accountName: string;
|
||||||
|
groupId?: string;
|
||||||
|
groupName?: string;
|
||||||
};
|
};
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection.Checkly;
|
app: AppConnection.Checkly;
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/typ
|
|||||||
|
|
||||||
export type TRenderSync = TRootSecretSync & {
|
export type TRenderSync = TRootSecretSync & {
|
||||||
destination: SecretSync.Render;
|
destination: SecretSync.Render;
|
||||||
destinationConfig: {
|
destinationConfig:
|
||||||
scope: RenderSyncScope.Service;
|
| {
|
||||||
type: RenderSyncType;
|
type: RenderSyncType;
|
||||||
serviceId: string;
|
scope: RenderSyncScope.Service;
|
||||||
serviceName?: string;
|
serviceId: string;
|
||||||
};
|
serviceName?: string | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: RenderSyncType;
|
||||||
|
scope: RenderSyncScope.EnvironmentGroup;
|
||||||
|
environmentGroupId: string;
|
||||||
|
environmentGroupName?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
connection: {
|
connection: {
|
||||||
app: AppConnection.Render;
|
app: AppConnection.Render;
|
||||||
@@ -23,7 +30,8 @@ export type TRenderSync = TRootSecretSync & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum RenderSyncScope {
|
export enum RenderSyncScope {
|
||||||
Service = "service"
|
Service = "service",
|
||||||
|
EnvironmentGroup = "environment-group"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RenderSyncType {
|
export enum RenderSyncType {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export type SubscriptionPlan = {
|
|||||||
externalKms: boolean;
|
externalKms: boolean;
|
||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
|
enforceGoogleSSO: boolean;
|
||||||
projectTemplates: boolean;
|
projectTemplates: boolean;
|
||||||
kmip: boolean;
|
kmip: boolean;
|
||||||
secretScanning: boolean;
|
secretScanning: boolean;
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ export const useAddUserToWsNonE2EE = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendEmailVerificationCode = async (username: string) => {
|
export const sendEmailVerificationCode = async (token: string) => {
|
||||||
return apiRequest.post("/api/v2/users/me/emails/code", {
|
return apiRequest.post("/api/v2/users/me/emails/code", {
|
||||||
username
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSendEmailVerificationCode = () => {
|
export const useSendEmailVerificationCode = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (username: string) => {
|
mutationFn: async (token: string) => {
|
||||||
await sendEmailVerificationCode(username);
|
await sendEmailVerificationCode(token);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export type TProjectMembership = {
|
|||||||
export type TWorkspaceUser = {
|
export type TWorkspaceUser = {
|
||||||
id: string;
|
id: string;
|
||||||
user: {
|
user: {
|
||||||
|
isOrgMembershipActive: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -240,6 +240,13 @@ export const Navbar = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (org.googleSsoAuthEnforced) {
|
||||||
|
await logout.mutateAsync();
|
||||||
|
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handleOrgChange(org?.id);
|
handleOrgChange(org?.id);
|
||||||
}}
|
}}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|||||||
@@ -82,25 +82,40 @@ export const SelectOrganizationSection = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organization.authEnforced && !canBypassOrgAuth) {
|
if ((organization.authEnforced || organization.googleSsoAuthEnforced) && !canBypassOrgAuth) {
|
||||||
|
const authToken = jwtDecode(getAuthToken()) as { authMethod: AuthMethod };
|
||||||
|
|
||||||
// org has an org-level auth method enabled (e.g. SAML)
|
// org has an org-level auth method enabled (e.g. SAML)
|
||||||
// -> logout + redirect to SAML SSO
|
// -> logout + redirect to SAML SSO
|
||||||
await logout.mutateAsync();
|
|
||||||
let url = "";
|
let url = "";
|
||||||
if (organization.orgAuthMethod === AuthMethod.OIDC) {
|
if (organization.orgAuthMethod === AuthMethod.OIDC) {
|
||||||
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
|
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
|
||||||
callbackPort ? `&callbackPort=${callbackPort}` : ""
|
callbackPort ? `&callbackPort=${callbackPort}` : ""
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else if (organization.orgAuthMethod === AuthMethod.SAML) {
|
||||||
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
|
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
|
||||||
|
|
||||||
if (callbackPort) {
|
if (callbackPort) {
|
||||||
url += `?callback_port=${callbackPort}`;
|
url += `?callback_port=${callbackPort}`;
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
organization.googleSsoAuthEnforced &&
|
||||||
|
authToken.authMethod !== AuthMethod.GOOGLE
|
||||||
|
) {
|
||||||
|
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
|
||||||
|
|
||||||
|
if (callbackPort) {
|
||||||
|
url += `&callback_port=${callbackPort}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = url;
|
// we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO
|
||||||
return;
|
// see line 103-106
|
||||||
|
if (url) {
|
||||||
|
await logout.mutateAsync();
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||||
|
|||||||
+10
-1
@@ -114,7 +114,16 @@ export const EmailConfirmationStep = ({
|
|||||||
|
|
||||||
const resendCode = async () => {
|
const resendCode = async () => {
|
||||||
try {
|
try {
|
||||||
await sendEmailVerificationCode(username);
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
const token = queryParams.get("token");
|
||||||
|
if (!token) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to resend code, no token found",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendEmailVerificationCode(token);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully resent code",
|
text: "Successfully resent code",
|
||||||
type: "success"
|
type: "success"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
import { AllProjectView } from "./components/AllProjectView";
|
import { AllProjectView } from "./components/AllProjectView";
|
||||||
import { MyProjectView } from "./components/MyProjectView";
|
import { MyProjectView } from "./components/MyProjectView";
|
||||||
import { ProjectListToggle, ProjectListView } from "./components/ProjectListToggle";
|
import { ProjectListView } from "./components/ProjectListToggle";
|
||||||
|
|
||||||
// const formatDescription = (type: ProjectType) => {
|
// const formatDescription = (type: ProjectType) => {
|
||||||
// if (type === ProjectType.SecretManager)
|
// if (type === ProjectType.SecretManager)
|
||||||
@@ -28,7 +28,23 @@ import { ProjectListToggle, ProjectListView } from "./components/ProjectListTogg
|
|||||||
export const ProjectsPage = () => {
|
export const ProjectsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [projectListView, setProjectListView] = useState(ProjectListView.MyProjects);
|
const [projectListView, setProjectListView] = useState<ProjectListView>(() => {
|
||||||
|
const storedView = localStorage.getItem("projectListView");
|
||||||
|
|
||||||
|
if (
|
||||||
|
storedView &&
|
||||||
|
(storedView === ProjectListView.AllProjects || storedView === ProjectListView.MyProjects)
|
||||||
|
) {
|
||||||
|
return storedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectListView.MyProjects;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSetProjectListView = (value: ProjectListView) => {
|
||||||
|
localStorage.setItem("projectListView", value);
|
||||||
|
setProjectListView(value);
|
||||||
|
};
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
@@ -49,11 +65,7 @@ export const ProjectsPage = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="mb-4 flex flex-col items-start justify-start">
|
<div className="mb-4 flex flex-col items-start justify-start">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title="Projects"
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<ProjectListToggle value={projectListView} onChange={setProjectListView} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
description="Your team's complete security toolkit - organized and ready when you need them."
|
description="Your team's complete security toolkit - organized and ready when you need them."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,12 +74,16 @@ export const ProjectsPage = () => {
|
|||||||
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
||||||
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
||||||
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
||||||
|
projectListView={projectListView}
|
||||||
|
onProjectListViewChange={handleSetProjectListView}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AllProjectView
|
<AllProjectView
|
||||||
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
onAddNewProject={() => handlePopUpOpen("addNewWs")}
|
||||||
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
onUpgradePlan={() => handlePopUpOpen("upgradePlan")}
|
||||||
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
isAddingProjectsAllowed={isAddingProjectsAllowed}
|
||||||
|
projectListView={projectListView}
|
||||||
|
onProjectListViewChange={handleSetProjectListView}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NewProjectModal
|
<NewProjectModal
|
||||||
|
|||||||
@@ -49,11 +49,17 @@ import {
|
|||||||
useSearchProjects
|
useSearchProjects
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||||
|
import {
|
||||||
|
ProjectListToggle,
|
||||||
|
ProjectListView
|
||||||
|
} from "@app/pages/organization/ProjectsPage/components/ProjectListToggle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onAddNewProject: () => void;
|
onAddNewProject: () => void;
|
||||||
onUpgradePlan: () => void;
|
onUpgradePlan: () => void;
|
||||||
isAddingProjectsAllowed: boolean;
|
isAddingProjectsAllowed: boolean;
|
||||||
|
projectListView: ProjectListView;
|
||||||
|
onProjectListViewChange: (value: ProjectListView) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequestAccessModalProps = {
|
type RequestAccessModalProps = {
|
||||||
@@ -106,7 +112,9 @@ const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProp
|
|||||||
export const AllProjectView = ({
|
export const AllProjectView = ({
|
||||||
onAddNewProject,
|
onAddNewProject,
|
||||||
onUpgradePlan,
|
onUpgradePlan,
|
||||||
isAddingProjectsAllowed
|
isAddingProjectsAllowed,
|
||||||
|
projectListView,
|
||||||
|
onProjectListViewChange
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
@@ -176,10 +184,10 @@ export const AllProjectView = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex w-full flex-row">
|
<div className="flex w-full flex-row">
|
||||||
<div className="flex-grow" />
|
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
containerClassName="w-full"
|
containerClassName="w-full ml-2"
|
||||||
placeholder="Search by project name..."
|
placeholder="Search by project name..."
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
@@ -242,7 +250,7 @@ export const AllProjectView = ({
|
|||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
<div className="ml-2 flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
<Tooltip content="Disabled across All Project view.">
|
<Tooltip content="Disabled across All Project view.">
|
||||||
<div className="flex cursor-not-allowed items-center justify-center">
|
<div className="flex cursor-not-allowed items-center justify-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user