Fix merge conflicts, bring oidc auth up to speed with identity ui changes

This commit is contained in:
Tuan Dang
2024-07-10 10:31:48 +07:00
207 changed files with 12538 additions and 3922 deletions
+4
View File
@@ -43,6 +43,7 @@ import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
@@ -66,6 +67,7 @@ import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
declare module "fastify" {
@@ -128,6 +130,7 @@ declare module "fastify" {
identity: TIdentityServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityTokenAuth: TIdentityTokenAuthServiceFactory;
identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
@@ -159,6 +162,7 @@ declare module "fastify" {
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
secretSharing: TSecretSharingServiceFactory;
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
+8
View File
@@ -107,6 +107,9 @@ import {
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
TIdentityTokenAuths,
TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate,
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate,
@@ -453,6 +456,11 @@ declare module "knex/types/tables" {
TIntegrationAuthsUpdate
>;
[TableName.Identity]: KnexOriginal.CompositeTableType<TIdentities, TIdentitiesInsert, TIdentitiesUpdate>;
[TableName.IdentityTokenAuth]: KnexOriginal.CompositeTableType<
TIdentityTokenAuths,
TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate
>;
[TableName.IdentityUniversalAuth]: KnexOriginal.CompositeTableType<
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
@@ -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.SuperAdmin, "enabledLoginMethods"))) {
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
tb.specificType("enabledLoginMethods", "text[]");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SuperAdmin, "enabledLoginMethods")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("enabledLoginMethods");
});
}
}
@@ -0,0 +1,53 @@
import { Knex } from "knex";
import { WebhookType } from "@app/services/webhook/webhook-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasType = await knex.schema.hasColumn(TableName.Webhook, "type");
if (await knex.schema.hasTable(TableName.Webhook)) {
await knex.schema.alterTable(TableName.Webhook, (tb) => {
if (!hasUrlCipherText) {
tb.text("urlCipherText");
}
if (!hasUrlIV) {
tb.string("urlIV");
}
if (!hasUrlTag) {
tb.string("urlTag");
}
if (!hasType) {
tb.string("type").defaultTo(WebhookType.GENERAL);
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasType = await knex.schema.hasColumn(TableName.Webhook, "type");
if (await knex.schema.hasTable(TableName.Webhook)) {
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (hasUrlCipherText) {
t.dropColumn("urlCipherText");
}
if (hasUrlIV) {
t.dropColumn("urlIV");
}
if (hasUrlTag) {
t.dropColumn("urlTag");
}
if (hasType) {
t.dropColumn("type");
}
});
}
}
@@ -0,0 +1,188 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// migrate secret approval policy approvers to user id
const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId");
if (!hasApproverUserId) {
// add the new fields
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
// if (hasApproverId) tb.setNullable("approverId");
tb.uuid("approverUserId");
tb.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
// convert project membership id => user id
await knex(TableName.SecretApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverId`]))
});
// drop the old field
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
if (hasApproverId) tb.dropColumn("approverId");
tb.uuid("approverUserId").notNullable().alter();
});
}
// migrate secret approval request committer and statusChangeBy to user id
const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest);
const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId");
const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId");
const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy");
const hasStatusChangedByUserId = await knex.schema.hasColumn(
TableName.SecretApprovalRequest,
"statusChangedByUserId"
);
if (hasSecretApprovalRequestTable) {
// new fields
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
// if (hasCommitterId) tb.setNullable("committerId");
if (!hasCommitterUserId) {
tb.uuid("committerUserId");
tb.foreign("committerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
}
if (!hasStatusChangedByUserId) {
tb.uuid("statusChangedByUserId");
tb.foreign("statusChangedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
}
});
// copy the assigned project membership => user id to new fields
await knex(TableName.SecretApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
committerUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerId`])),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
statusChangedByUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangeBy`]))
});
// drop old fields
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
if (hasStatusChangeBy) tb.dropColumn("statusChangeBy");
if (hasCommitterId) tb.dropColumn("committerId");
tb.uuid("committerUserId").notNullable().alter();
});
}
// migrate secret approval request reviewer to user id
const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId");
if (!hasReviewerUserId) {
// new fields
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
// if (hasMemberId) tb.setNullable("member");
tb.uuid("reviewerUserId");
tb.foreign("reviewerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
// copy project membership => user id to new fields
await knex(TableName.SecretApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
reviewerUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.member`]))
});
// drop table
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
if (hasMemberId) tb.dropColumn("member");
tb.uuid("reviewerUserId").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId");
if (hasApproverUserId) {
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
if (!hasApproverId) {
tb.uuid("approverId");
tb.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
if (!hasApproverId) {
await knex(TableName.SecretApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`]))
});
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
tb.dropColumn("approverUserId");
tb.uuid("approverId").notNullable().alter();
});
}
}
const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest);
const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId");
const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId");
const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy");
const hasStatusChangedByUser = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangedByUserId");
if (hasSecretApprovalRequestTable) {
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
// if (hasCommitterId) tb.uuid("committerId").notNullable().alter();
if (!hasCommitterId) {
tb.uuid("committerId");
tb.foreign("committerId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
if (!hasStatusChangeBy) {
tb.uuid("statusChangeBy");
tb.foreign("statusChangeBy").references("id").inTable(TableName.ProjectMembership).onDelete("SET NULL");
}
});
await knex(TableName.SecretApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
committerId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
statusChangeBy: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`]))
});
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
if (hasCommitterUserId) tb.dropColumn("committerUserId");
if (hasStatusChangedByUser) tb.dropColumn("statusChangedByUserId");
if (hasCommitterId) tb.uuid("committerId").notNullable().alter();
});
}
const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId");
if (hasReviewerUserId) {
if (!hasMemberId) {
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
// if (hasMemberId) tb.uuid("member").notNullable().alter();
tb.uuid("member");
tb.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
});
}
await knex(TableName.SecretApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`]))
});
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
tb.uuid("member").notNullable().alter();
tb.dropColumn("reviewerUserId");
});
}
}
@@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.IdentityTokenAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
});
await createOnUpdateTrigger(knex, TableName.IdentityTokenAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityTokenAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityTokenAuth);
}
@@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityAccessToken)) {
const hasNameColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "name");
if (!hasNameColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("name").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityAccessToken)) {
if (await knex.schema.hasColumn(TableName.IdentityAccessToken, "name")) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("name");
});
}
}
}
@@ -19,7 +19,8 @@ export const IdentityAccessTokensSchema = z.object({
identityUAClientSecretId: z.string().nullable().optional(),
identityId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
name: z.string().nullable().optional()
});
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityTokenAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid()
});
export type TIdentityTokenAuths = z.infer<typeof IdentityTokenAuthsSchema>;
export type TIdentityTokenAuthsInsert = Omit<z.input<typeof IdentityTokenAuthsSchema>, TImmutableDBKeys>;
export type TIdentityTokenAuthsUpdate = Partial<Omit<z.input<typeof IdentityTokenAuthsSchema>, TImmutableDBKeys>>;
+1
View File
@@ -33,6 +33,7 @@ export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";
export * from "./identity-token-auths";
export * from "./identity-ua-client-secrets";
export * from "./identity-universal-auths";
export * from "./incident-contacts";
+2
View File
@@ -53,6 +53,7 @@ export enum TableName {
Webhook = "webhooks",
Identity = "identities",
IdentityAccessToken = "identity_access_tokens",
IdentityTokenAuth = "identity_token_auths",
IdentityUniversalAuth = "identity_universal_auths",
IdentityKubernetesAuth = "identity_kubernetes_auths",
IdentityGcpAuth = "identity_gcp_auths",
@@ -162,6 +163,7 @@ export enum ProjectUpgradeStatus {
}
export enum IdentityAuthMethod {
TOKEN_AUTH = "token-auth",
Univeral = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
@@ -9,10 +9,10 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
approverUserId: z.string().uuid()
});
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
@@ -9,11 +9,11 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
reviewerUserId: z.string().uuid()
});
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
@@ -15,11 +15,11 @@ export const SecretApprovalRequestsSchema = z.object({
conflicts: z.unknown().nullable().optional(),
slug: z.string(),
folderId: z.string().uuid(),
statusChangeBy: z.string().uuid().nullable().optional(),
committerId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional()
isReplicated: z.boolean().nullable().optional(),
committerUserId: z.string().uuid(),
statusChangedByUserId: z.string().uuid().nullable().optional()
});
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
+2 -1
View File
@@ -18,7 +18,8 @@ export const SuperAdminSchema = z.object({
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional()
defaultAuthOrgId: z.string().uuid().nullable().optional(),
enabledLoginMethods: z.string().array().nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
+5 -1
View File
@@ -21,7 +21,11 @@ export const WebhooksSchema = z.object({
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
envId: z.string().uuid()
envId: z.string().uuid(),
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;
@@ -25,10 +25,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional()
.nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z.string().array().min(1),
approverUserIds: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@@ -66,7 +66,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approverUserIds: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z
.string()
@@ -74,7 +74,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val))
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@@ -139,7 +139,15 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array()
approvals: sapPubSchema
.extend({
userApprovers: z
.object({
userId: z.string()
})
.array()
})
.array()
})
}
},
@@ -170,7 +178,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
policy: sapPubSchema.merge(z.object({ approvers: z.string().array() })).optional()
policy: sapPubSchema
.extend({
userApprovers: z.object({ userId: z.string() }).array()
})
.optional()
})
}
},
@@ -6,7 +6,8 @@ import {
SecretApprovalRequestsSecretsSchema,
SecretsSchema,
SecretTagsSchema,
SecretVersionsSchema
SecretVersionsSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
@@ -14,6 +15,15 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge(
UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
username: true
})
);
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -41,9 +51,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
approvers: z.string().array(),
secretPath: z.string().optional().nullable()
}),
committerUser: approvalRequestUser,
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(),
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
approvers: z.string().array()
}).array()
})
@@ -195,7 +206,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
// eslint-disable-next-line
metadata: {
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeBy as string,
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangedByUserId as string,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
// eslint-disable-next-line
@@ -216,6 +227,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
})
.array()
.optional();
server.route({
method: "GET",
url: "/:id",
@@ -235,12 +247,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(),
name: z.string(),
approvals: z.number(),
approvers: z.string().array(),
approvers: approvalRequestUser.array(),
secretPath: z.string().optional().nullable()
}),
environment: z.string(),
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
approvers: z.string().array(),
statusChangedByUser: approvalRequestUser.optional(),
committerUser: approvalRequestUser,
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
secretPath: z.string(),
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true })
.merge(
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -55,13 +56,34 @@ export const auditLogDALFactory = (db: TDbClient) => {
// delete all audit log that have expired
const pruneAuditLog = async (tx?: Knex) => {
try {
const today = new Date();
const docs = await (tx || db)(TableName.AuditLog).where("expiresAt", "<", today).del();
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "PruneAuditLog" });
}
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
do {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
.where("expiresAt", "<", today)
.select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery)
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 100); // time to breathe for db
});
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning");
}
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
};
return { ...auditLogOrm, pruneAuditLog, find };
@@ -66,6 +66,13 @@ export enum EventType {
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth",
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
@@ -452,6 +459,66 @@ interface DeleteIdentityUniversalAuthEvent {
};
}
interface CreateTokenIdentityTokenAuthEvent {
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
identityAccessTokenId: string;
};
}
interface UpdateTokenIdentityTokenAuthEvent {
type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
tokenId: string;
name?: string;
};
}
interface GetTokensIdentityTokenAuthEvent {
type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface AddIdentityTokenAuthEvent {
type: EventType.ADD_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityTokenAuthEvent {
type: EventType.UPDATE_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityTokenAuthEvent {
type: EventType.GET_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface DeleteIdentityTokenAuthEvent {
type: EventType.REVOKE_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityKubernetesAuthEvent {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
metadata: {
@@ -833,7 +900,6 @@ interface CreateWebhookEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -844,7 +910,6 @@ interface UpdateWebhookStatusEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -855,7 +920,6 @@ interface DeleteWebhookEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -1116,6 +1180,13 @@ export type Event =
| UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| CreateTokenIdentityTokenAuthEvent
| UpdateTokenIdentityTokenAuthEvent
| GetTokensIdentityTokenAuthEvent
| AddIdentityTokenAuthEvent
| UpdateIdentityTokenAuthEvent
| GetIdentityTokenAuthEvent
| DeleteIdentityTokenAuthEvent
| LoginIdentityKubernetesAuthEvent
| DeleteIdentityKubernetesAuthEvent
| AddIdentityKubernetesAuthEvent
@@ -3,7 +3,8 @@ import { z } from "zod";
export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2",
Oracle = "oracledb"
Oracle = "oracledb",
MsSQL = "mssql"
}
export const DynamicSecretSqlDBSchema = z.object({
@@ -34,6 +34,7 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -417,6 +418,13 @@ export const ldapConfigServiceFactory = ({
}: TLdapLoginDTO) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) {
throw new BadRequestError({
message: "Login with LDAP is disabled by administrator."
});
}
let userAlias = await userAliasDAL.findOne({
externalId,
orgId,
@@ -473,7 +481,7 @@ export const ldapConfigServiceFactory = ({
userAlias = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
if (serverCfg.trustLdapEmails) {
newUser = await userDAL.findOne(
{
email,
@@ -5,6 +5,7 @@
// TODO(akhilmhdh): With tony find out the api structure and fill it here
import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
@@ -200,13 +201,13 @@ export const licenseServiceFactory = ({
await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`);
};
const updateSubscriptionOrgMemberCount = async (orgId: string) => {
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
if (instanceType === InstanceType.Cloud) {
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ message: "Org not found" });
const quantity = await licenseDAL.countOfOrgMembers(orgId);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId);
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
if (org?.customerId) {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity,
@@ -215,8 +216,8 @@ export const licenseServiceFactory = ({
}
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null);
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, {
usedSeats,
usedIdentitySeats
@@ -26,6 +26,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -157,6 +158,13 @@ export const oidcConfigServiceFactory = ({
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
throw new BadRequestError({
message: "Login with OIDC is disabled by administrator."
});
}
const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({
externalId,
@@ -28,6 +28,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -335,6 +336,13 @@ export const samlConfigServiceFactory = ({
}: TSamlLoginDTO) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) {
throw new BadRequestError({
message: "Login with SAML is disabled by administrator."
});
}
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
@@ -1,49 +1,59 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretApprovalPolicies } from "@app/db/schemas";
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
const sapFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
tx(TableName.SecretApprovalPolicy)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.join(
.leftJoin(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
tx.ref("id").withSchema(TableName.Environment).as("envId"),
tx.ref("projectId").withSchema(TableName.Environment)
)
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
.orderBy("createdAt", "asc");
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await sapFindQuery(tx || db.replicaNode(), {
const doc = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
});
const formatedDoc = mergeOneToManyRelation(
doc,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const formatedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => ({
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
projectId: data.projectId,
...SecretApprovalPoliciesSchema.parse(data)
}),
({ approverId }) => approverId,
"approvers"
);
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formatedDoc?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
@@ -52,18 +62,25 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
try {
const docs = await sapFindQuery(tx || db.replicaNode(), filter);
const formatedDoc = mergeOneToManyRelation(
docs,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
projectId: data.projectId,
...SecretApprovalPoliciesSchema.parse(data)
}),
({ approverId }) => approverId,
"approvers"
);
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formatedDoc;
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
@@ -7,7 +7,6 @@ import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@@ -29,7 +28,6 @@ type TSecretApprovalPolicyServiceFactoryDep = {
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -38,8 +36,7 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyDAL,
permissionService,
secretApprovalPolicyApproverDAL,
projectEnvDAL,
projectMembershipDAL
projectEnvDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({
name,
@@ -48,12 +45,12 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId,
actorAuthMethod,
approvals,
approvers,
approverUserIds,
projectId,
secretPath,
environment
}: TCreateSapDTO) => {
if (approvals > approvers.length)
if (approvals > approverUserIds.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
@@ -70,13 +67,6 @@ export const secretApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApprovers = await projectMembershipDAL.find({
projectId,
$in: { id: approvers }
});
if (secretApprovers.length !== approvers.length)
throw new BadRequestError({ message: "Approver not found in project" });
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create(
{
@@ -88,8 +78,8 @@ export const secretApprovalPolicyServiceFactory = ({
tx
);
await secretApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approverUserIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
tx
@@ -100,7 +90,7 @@ export const secretApprovalPolicyServiceFactory = ({
};
const updateSecretApprovalPolicy = async ({
approvers,
approverUserIds,
secretPath,
name,
actorId,
@@ -132,22 +122,11 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
if (approvers) {
const secretApprovers = await projectMembershipDAL.find(
{
projectId: secretApprovalPolicy.projectId,
$in: { id: approvers }
},
{ tx }
);
if (secretApprovers.length !== approvers.length)
throw new BadRequestError({ message: "Approver not found in project" });
if (doc.approvals > secretApprovers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
if (approverUserIds) {
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approverUserIds.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
tx
@@ -4,7 +4,7 @@ export type TCreateSapDTO = {
approvals: number;
secretPath?: string | null;
environment: string;
approvers: string[];
approverUserIds: string[];
projectId: string;
name: string;
} & Omit<TProjectPermission, "projectId">;
@@ -13,7 +13,7 @@ export type TUpdateSapDTO = {
secretPolicyId: string;
approvals?: number;
secretPath?: string | null;
approvers: string[];
approverUserIds: string[];
name?: string;
} & Omit<TProjectPermission, "projectId">;
@@ -5,7 +5,8 @@ import {
SecretApprovalRequestsSchema,
TableName,
TSecretApprovalRequests,
TSecretApprovalRequestsSecrets
TSecretApprovalRequestsSecrets,
TUsers
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, stripUndefinedInWhere, TFindFilter } from "@app/lib/knex";
@@ -16,7 +17,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType<typeof secretApprovalR
type TFindQueryFilter = {
projectId: string;
membershipId: string;
userId: string;
status?: RequestState;
environment?: string;
committer?: string;
@@ -37,27 +38,63 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.leftJoin<TUsers>(
db(TableName.Users).as("statusChangedByUser"),
`${TableName.SecretApprovalRequest}.statusChangedByUserId`,
`statusChangedByUser.id`
)
.join<TUsers>(
db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id`
)
.join(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestReviewer}.requestId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalReviewerUser"),
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
`secretApprovalReviewerUser.id`
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
tx.ref("lastName").withSchema("statusChangedByUser").as("statusChangedByUserLastName"),
tx.ref("email").withSchema("committerUser").as("committerUserEmail"),
tx.ref("username").withSchema("committerUser").as("committerUserUsername"),
tx.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
);
const findById = async (id: string, tx?: Knex) => {
@@ -71,6 +108,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
...SecretApprovalRequestsSchema.parse(el),
projectId: el.projectId,
environment: el.environment,
statusChangedByUser: el.statusChangedByUserId
? {
userId: el.statusChangedByUserId,
email: el.statusChangedByUserEmail,
firstName: el.statusChangedByUserFirstName,
lastName: el.statusChangedByUserLastName,
username: el.statusChangedByUserUsername
}
: undefined,
committerUser: {
userId: el.committerUserId,
email: el.committerUserEmail,
firstName: el.committerUserFirstName,
lastName: el.committerUserLastName,
username: el.committerUserUsername
},
policy: {
id: el.policyId,
name: el.policyName,
@@ -80,11 +133,34 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({
reviewerUserId: userId,
reviewerStatus: status,
reviewerEmail: email,
reviewerLastName: lastName,
reviewerUsername: username,
reviewerFirstName: firstName
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({
approverUserId,
approverEmail: email,
approverUsername: username,
approverLastName: lastName,
approverFirstName: firstName
}) => ({
userId: approverUserId,
email,
firstName,
lastName,
username
})
}
]
});
if (!formatedDoc?.[0]) return;
@@ -97,7 +173,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}
};
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
const findProjectRequestCount = async (projectId: string, userId: string, tx?: Knex) => {
try {
const docs = await (tx || db)
.with(
@@ -114,8 +190,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
@@ -142,7 +218,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
};
const findByProjectId = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, membershipId }: TFindQueryFilter,
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
tx?: Knex
) => {
try {
@@ -161,6 +237,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id`
)
.leftJoin(
TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`,
@@ -176,20 +257,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
projectId,
[`${TableName.Environment}.slug` as "slug"]: environment,
[`${TableName.SecretApprovalRequest}.status`]: status,
committerId: committer
committerUserId: committer
})
)
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
@@ -201,7 +283,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
)
.orderBy("createdAt", "desc");
@@ -223,18 +309,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath
},
committerUser: {
userId: el.committerUserId,
email: el.committerUserEmail,
firstName: el.committerUserFirstName,
lastName: el.committerUserLastName,
username: el.committerUserUsername
}
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
mapper: ({ reviewerUserId, reviewerStatus: s }) =>
reviewerUserId ? { userId: reviewerUserId, status: s } : undefined
},
{
key: "approverId",
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverId }) => approverId
mapper: ({ approverUserId }) => approverUserId
},
{
key: "commitId",
@@ -87,7 +87,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(
await permissionService.getProjectPermission(
actor as ActorType.USER,
actorId,
projectId,
@@ -95,7 +95,7 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId
);
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
return count;
};
@@ -113,19 +113,13 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId,
committer,
environment,
status,
membershipId: membership.id,
userId: actorId,
limit,
offset
});
@@ -145,7 +139,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalRequest.projectId,
@@ -154,8 +148,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -180,7 +174,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
@@ -189,8 +183,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -198,7 +192,7 @@ export const secretApprovalRequestServiceFactory = ({
const review = await secretApprovalRequestReviewerDAL.findOne(
{
requestId: secretApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@@ -207,7 +201,7 @@ export const secretApprovalRequestServiceFactory = ({
{
status,
requestId: secretApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@@ -230,7 +224,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
@@ -239,8 +233,8 @@ export const secretApprovalRequestServiceFactory = ({
);
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
@@ -253,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
status,
statusChangeBy: membership.id
statusChangedByUserId: actorId
});
return { ...secretApprovalRequest, ...updatedRequest };
};
@@ -270,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy, folderId, projectId } = secretApprovalRequest;
const { membership, hasRole } = await permissionService.getProjectPermission(
const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
projectId,
@@ -280,19 +274,19 @@ export const secretApprovalRequestServiceFactory = ({
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
secretApprovalRequest.committerUserId !== actorId &&
!policy.approvers.find(({ userId }) => userId === actorId)
) {
throw new UnauthorizedError({ message: "User has no access" });
}
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status as ApprovalStatus }),
(prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }),
{}
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
@@ -472,7 +466,7 @@ export const secretApprovalRequestServiceFactory = ({
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangeBy: membership.id
statusChangedByUserId: actorId
},
tx
);
@@ -509,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission, membership } = await permissionService.getProjectPermission(
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
@@ -663,7 +657,7 @@ export const secretApprovalRequestServiceFactory = ({
policyId: policy.id,
status: "open",
hasMerged: false,
committerId: membership.id
committerUserId: actorId
},
tx
);
@@ -11,7 +11,6 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
@@ -46,7 +45,6 @@ type TSecretReplicationServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne">;
secretApprovalRequestSecretDAL: Pick<
TSecretApprovalRequestSecretDALFactory,
"insertMany" | "insertApprovalSecretTags"
@@ -92,7 +90,6 @@ export const secretReplicationServiceFactory = ({
secretApprovalRequestSecretDAL,
secretApprovalRequestDAL,
secretQueueService,
projectMembershipDAL,
projectBotService
}: TSecretReplicationServiceFactoryDep) => {
const getReplicatedSecrets = (
@@ -297,12 +294,6 @@ export const secretReplicationServiceFactory = ({
);
// this means it should be a approval request rather than direct replication
if (policy && actor === ActorType.USER) {
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
if (!membership) {
logger.error("Project membership not found in %s for user %s", projectId, actorId);
return;
}
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
destinationReplicationFolderId,
@@ -316,7 +307,7 @@ export const secretReplicationServiceFactory = ({
policyId: policy.id,
status: "open",
hasMerged: false,
committerId: membership.id,
committerUserId: actorId,
isReplicated: true
},
tx
@@ -331,7 +331,7 @@ export const secretRotationQueueFactory = ({
logger.info("Finished rotating: rotation id: ", rotationId);
} catch (error) {
logger.error(error);
logger.error(error, "Failed to execute secret rotation");
if (error instanceof DisableRotationErrors) {
if (job.id) {
await queue.stopRepeatableJobByJobId(QueueName.SecretRotation, job.id);
@@ -133,7 +133,7 @@ export const secretRotationServiceFactory = ({
creds: []
};
const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData));
const secretRotation = secretRotationDAL.transaction(async (tx) => {
const secretRotation = await secretRotationDAL.transaction(async (tx) => {
const doc = await secretRotationDAL.create(
{
provider,
@@ -148,13 +148,13 @@ export const secretRotationServiceFactory = ({
},
tx
);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
});
await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval);
return secretRotation;
};
@@ -212,9 +212,9 @@ export const secretRotationServiceFactory = ({
);
const deletedDoc = await secretRotationDAL.transaction(async (tx) => {
const strat = await secretRotationDAL.deleteById(rotationId, tx);
await secretRotationQueue.removeFromQueue(strat.id, strat.interval);
return strat;
});
await secretRotationQueue.removeFromQueue(deletedDoc.id, deletedDoc.interval);
return { ...doc, ...deletedDoc };
};
+9 -4
View File
@@ -5,6 +5,9 @@ import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- If `process.pkg` is set, and it's true, then it means that the app is currently running in a packaged environment (a binary)
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
const zodStrBool = z
.enum(["true", "false"])
.optional()
@@ -20,7 +23,7 @@ const databaseReadReplicaSchema = z
const envSchema = z
.object({
PORT: z.coerce.number().default(4000),
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
DISABLE_SECRET_SCANNING: z
.enum(["true", "false"])
.default("false")
@@ -131,11 +134,13 @@ const envSchema = z
// GENERIC
STANDALONE_MODE: z
.enum(["true", "false"])
.transform((val) => val === "true")
.transform((val) => val === "true" || IS_PACKAGED)
.optional(),
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional())
CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional())
})
.transform((data) => ({
...data,
@@ -146,7 +151,7 @@ const envSchema = z
isSmtpConfigured: Boolean(data.SMTP_HOST),
isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development",
isProductionMode: data.NODE_ENV === "production",
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
+1
View File
@@ -0,0 +1 @@
export const isMigrationMode = () => !!process.argv.slice(2).find((arg) => arg === "migration:latest"); // example -> ./binary migration:latest
+1
View File
@@ -1,6 +1,7 @@
// Some of the functions are taken from https://github.com/rayepps/radash
// Full credits goes to https://github.com/rayapps to those functions
// Code taken to keep in in house and to adjust somethings for our needs
export * from "./argv";
export * from "./array";
export * from "./dates";
export * from "./object";
+22 -1
View File
@@ -1,8 +1,10 @@
import dotenv from "dotenv";
import path from "path";
import { initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
import { isMigrationMode } from "./lib/fn";
import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue";
import { main } from "./server/app";
@@ -10,6 +12,7 @@ import { bootstrapCheck } from "./server/boot-strap-check";
import { smtpServiceFactory } from "./services/smtp/smtp-service";
dotenv.config();
const run = async () => {
const logger = await initLogger();
const appCfg = initEnvConfig(logger);
@@ -22,12 +25,30 @@ const run = async () => {
}))
});
// Case: App is running in packaged mode (binary), and migration mode is enabled.
// Run the migrations and exit the process after completion.
if (IS_PACKAGED && isMigrationMode()) {
try {
logger.info("Running Postgres migrations..");
await db.migrate.latest({
directory: path.join(__dirname, "./db/migrations")
});
logger.info("Postgres migrations completed");
} catch (err) {
logger.error(err, "Failed to run migrations");
process.exit(1);
}
process.exit(0);
}
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line
process.on("SIGINT", async () => {
await server.close();
+3 -3
View File
@@ -15,7 +15,7 @@ import { Knex } from "knex";
import { Logger } from "pino";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service";
@@ -80,8 +80,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {
standaloneMode: appCfg.STANDALONE_MODE,
dir: path.join(__dirname, "../../"),
standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED,
dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"),
port: appCfg.PORT
});
}
+6
View File
@@ -82,3 +82,9 @@ export const publicSecretShareCreationLimit: RateLimitOptions = {
max: 5,
keyGenerator: (req) => req.realIp
};
export const userEngagementLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: 5,
keyGenerator: (req) => req.realIp
};
+21 -7
View File
@@ -1,9 +1,10 @@
// this plugins allows to run infisical in standalone mode
// standalone mode = infisical backend and nextjs frontend in one server
// this way users don't need to deploy two things
import path from "node:path";
import { IS_PACKAGED } from "@app/lib/config/env";
// to enabled this u need to set standalone mode to true
export const registerExternalNextjs = async (
server: FastifyZodProvider,
@@ -18,20 +19,33 @@ export const registerExternalNextjs = async (
}
) => {
if (standaloneMode) {
const nextJsBuildPath = path.join(dir, "frontend-build");
const frontendName = IS_PACKAGED ? "frontend" : "frontend-build";
const nextJsBuildPath = path.join(dir, frontendName);
const { default: conf } = (await import(
path.join(dir, "frontend-build/.next/required-server-files.json"),
path.join(dir, `${frontendName}/.next/required-server-files.json`),
// @ts-expect-error type
{
assert: { type: "json" }
}
)) as { default: { config: string } };
/* eslint-disable */
const { default: NextServer } = (
await import(path.join(dir, "frontend-build/node_modules/next/dist/server/next-server.js"))
).default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let NextServer: any;
if (!IS_PACKAGED) {
/* eslint-disable */
const { default: nextServer } = (
await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`))
).default;
NextServer = nextServer;
} else {
/* eslint-disable */
const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`));
NextServer = nextServer.default;
}
const nextApp = new NextServer({
dev: false,
+20 -3
View File
@@ -107,6 +107,8 @@ import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
@@ -166,6 +168,7 @@ import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-servi
import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
@@ -235,6 +238,7 @@ export const registerRoutes = async (
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
@@ -322,7 +326,6 @@ export const registerRoutes = async (
auditLogStreamDAL
});
const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({
projectMembershipDAL,
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
permissionService,
@@ -771,7 +774,6 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretQueueService,
projectMembershipDAL,
projectBotService
});
const secretRotationQueue = secretRotationQueueFactory({
@@ -812,6 +814,7 @@ export const registerRoutes = async (
permissionService,
identityDAL,
identityOrgMembershipDAL,
identityProjectDAL,
licenseService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
@@ -832,6 +835,14 @@ export const registerRoutes = async (
permissionService,
identityProjectDAL
});
const identityTokenAuthService = identityTokenAuthServiceFactory({
identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
permissionService,
licenseService
});
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
permissionService,
@@ -937,6 +948,10 @@ export const registerRoutes = async (
oidcConfigDAL
});
const userEngagementService = userEngagementServiceFactory({
userDAL
});
await superAdminService.initServerCfg();
//
// setup the communication with license key server
@@ -980,6 +995,7 @@ export const registerRoutes = async (
identity: identityService,
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityTokenAuth: identityTokenAuthService,
identityUa: identityUaService,
identityKubernetesAuth: identityKubernetesAuthService,
identityGcpAuth: identityGcpAuthService,
@@ -1009,7 +1025,8 @@ export const registerRoutes = async (
telemetry: telemetryService,
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
secretSharing: secretSharingService
secretSharing: secretSharingService,
userEngagement: userEngagementService
});
const cronJobs: CronJob[] = [];
+86 -2
View File
@@ -8,6 +8,7 @@ import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerAdminRouter = async (server: FastifyZodProvider) => {
@@ -54,7 +55,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional(),
trustOidcEmails: z.boolean().optional(),
defaultAuthOrgId: z.string().optional().nullable()
defaultAuthOrgId: z.string().optional().nullable(),
enabledLoginMethods: z
.nativeEnum(LoginMethod)
.array()
.optional()
.refine((methods) => !methods || methods.length > 0, {
message: "At least one login method should be enabled."
})
}),
response: {
200: z.object({
@@ -70,11 +78,87 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
});
},
handler: async (req) => {
const config = await server.services.superAdmin.updateServerCfg(req.body);
const config = await server.services.superAdmin.updateServerCfg(req.body, req.permission.id);
return { config };
}
});
server.route({
method: "GET",
url: "/user-management/users",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
searchTerm: z.string().default(""),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20)
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.getUsers({
...req.query
});
return {
users
};
}
});
server.route({
method: "DELETE",
url: "/user-management/users/:userId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
userId: z.string()
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
})
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.deleteUser(req.params.userId);
return {
users
};
}
});
server.route({
method: "POST",
url: "/signup",
@@ -1,6 +1,12 @@
import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import {
IdentitiesSchema,
IdentityOrgMembershipsSchema,
OrgMembershipRole,
OrgRolesSchema,
ProjectsSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -260,4 +266,63 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
return { identities };
}
});
server.route({
method: "GET",
url: "/:identityId/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List project memberships that identity with id is part of",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identityMemberships: z.array(
z.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true })
})
)
})
}
},
handler: async (req) => {
const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
return { identityMemberships };
}
});
};
@@ -0,0 +1,468 @@
import { z } from "zod";
import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach Token Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.attachTokenAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit
}
}
});
return {
identityTokenAuth
};
}
});
server.route({
method: "PATCH",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
accessTokenTTL: z.number().int().min(0).optional(),
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.updateTokenAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit
}
}
});
return {
identityTokenAuth
};
}
});
server.route({
method: "GET",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.getTokenAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.GET_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId
}
}
});
return { identityTokenAuth };
}
});
server.route({
method: "DELETE",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.revokeIdentityTokenAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId
}
}
});
return { identityTokenAuth };
}
});
// proposed
// update token by id: PATCH /token-auth/tokens/:tokenId
// revoke token by id: POST /token-auth/tokens/:tokenId/revoke
// current
// revoke token by id: POST /token/revoke-by-id
// token-auth/identities/:identityId/tokens
server.route({
method: "POST",
url: "/token-auth/identities/:identityId/tokens",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
body: z.object({
name: z.string().optional()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityTokenAuth.createTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
identityAccessTokenId: identityAccessToken.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "GET",
url: "/token-auth/identities/:identityId/tokens",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get tokens for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(20)
}),
response: {
200: z.object({
tokens: IdentityAccessTokensSchema.array()
})
}
},
handler: async (req) => {
const { tokens, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokens({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: req.params.identityId
}
}
});
return { tokens };
}
});
server.route({
method: "PATCH",
url: "/token-auth/tokens/:tokenId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
tokenId: z.string()
}),
body: z.object({
name: z.string().optional()
}),
response: {
200: z.object({
token: IdentityAccessTokensSchema
})
}
},
handler: async (req) => {
const { token, identityMembershipOrg } = await server.services.identityTokenAuth.updateTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: token.identityId,
tokenId: token.id,
name: req.body.name
}
}
});
return { token };
}
});
server.route({
method: "POST",
url: "/token-auth/tokens/:tokenId/revoke",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Revoke token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
tokenId: z.string()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.identityTokenAuth.revokeTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId
});
return {
message: "Successfully revoked access token"
};
}
});
};
+4
View File
@@ -10,6 +10,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
@@ -26,6 +27,7 @@ import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router";
import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router";
@@ -34,6 +36,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(
async (authRouter) => {
await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityTokenAuthRouter);
await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityKubernetesRouter);
await authRouter.register(registerIdentityGcpAuthRouter);
@@ -79,4 +82,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
};
@@ -0,0 +1,27 @@
import { z } from "zod";
import { userEngagementLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserEngagementRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/me/wish",
config: {
rateLimit: userEngagementLimit
},
schema: {
body: z.object({
text: z.string().min(1)
}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.userEngagement.createUserWish(req.permission.id, req.body.text);
}
});
};
+26 -14
View File
@@ -6,13 +6,17 @@ import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { WebhookType } from "@app/services/webhook/webhook-types";
export const sanitizedWebhookSchema = WebhooksSchema.omit({
encryptedSecretKey: true,
iv: true,
tag: true,
algorithm: true,
keyEncoding: true
keyEncoding: true,
urlCipherText: true,
urlIV: true,
urlTag: true
}).merge(
z.object({
projectId: z.string(),
@@ -33,13 +37,24 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
webhookUrl: z.string().url().trim(),
webhookSecretKey: z.string().trim().optional(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
}),
body: z
.object({
type: z.nativeEnum(WebhookType).default(WebhookType.GENERAL),
workspaceId: z.string().trim(),
environment: z.string().trim(),
webhookUrl: z.string().url().trim(),
webhookSecretKey: z.string().trim().optional(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
})
.superRefine((data, ctx) => {
if (data.type === WebhookType.SLACK && !data.webhookUrl.includes("hooks.slack.com")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Incoming Webhook URL is invalid.",
path: ["webhookUrl"]
});
}
}),
response: {
200: z.object({
message: z.string(),
@@ -66,8 +81,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
environment: webhook.environment.slug,
webhookId: webhook.id,
isDisabled: webhook.isDisabled,
secretPath: webhook.secretPath,
webhookUrl: webhook.url
secretPath: webhook.secretPath
}
}
});
@@ -116,8 +130,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
environment: webhook.environment.slug,
webhookId: webhook.id,
isDisabled: webhook.isDisabled,
secretPath: webhook.secretPath,
webhookUrl: webhook.url
secretPath: webhook.secretPath
}
}
});
@@ -156,8 +169,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
environment: webhook.environment.slug,
webhookId: webhook.id,
isDisabled: webhook.isDisabled,
secretPath: webhook.secretPath,
webhookUrl: webhook.url
secretPath: webhook.secretPath
}
}
});
+1 -1
View File
@@ -297,7 +297,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const user = await server.services.user.deleteMe(req.permission.id);
const user = await server.services.user.deleteUser(req.permission.id);
return { user };
}
});
@@ -949,7 +949,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1133,7 +1133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1271,7 +1271,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1397,7 +1397,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1524,7 +1524,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -1638,7 +1638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: approval.committerId,
committedBy: approval.committerUserId,
secretApprovalRequestId: approval.id,
secretApprovalRequestSlug: approval.slug
}
@@ -17,6 +17,7 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { LoginMethod } from "../super-admin/super-admin-types";
import { TUserDALFactory } from "../user/user-dal";
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
import {
@@ -158,9 +159,22 @@ export const authLoginServiceFactory = ({
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
const serverCfg = await getServerCfg();
if (
serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
!providerAuthToken
) {
throw new BadRequestError({
message: "Login with email is disabled by administrator."
});
}
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
}
@@ -507,6 +521,40 @@ export const authLoginServiceFactory = ({
let user = await userDAL.findUserByUsername(email);
const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods) {
switch (authMethod) {
case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
throw new BadRequestError({
message: "Login with Github is disabled by administrator.",
name: "Oauth 2 login"
});
}
break;
}
case AuthMethod.GOOGLE: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
throw new BadRequestError({
message: "Login with Google is disabled by administrator.",
name: "Oauth 2 login"
});
}
break;
}
case AuthMethod.GITLAB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
throw new BadRequestError({
message: "Login with Gitlab is disabled by administrator.",
name: "Oauth 2 login"
});
}
break;
}
default:
break;
}
}
const appCfg = getConfig();
if (!user) {
@@ -364,7 +364,7 @@ export const authSignupServiceFactory = ({
tx
);
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx)));
await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id],
@@ -131,7 +131,10 @@ export const identityAccessTokenServiceFactory = ({
});
if (!identityAccessToken) throw new UnauthorizedError();
const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
isAccessTokenRevoked: true
});
return { revokedToken };
};
@@ -141,6 +144,10 @@ export const identityAccessTokenServiceFactory = ({
isAccessTokenRevoked: false
});
if (!identityAccessToken) throw new UnauthorizedError();
if (identityAccessToken.isAccessTokenRevoked)
throw new UnauthorizedError({
message: "Failed to authorize revoked access token"
});
if (ipAddress && identityAccessToken) {
checkIPAgainstBlocklist({
@@ -10,6 +10,103 @@ export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFac
export const identityProjectDALFactory = (db: TDbClient) => {
const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership);
const findByIdentityId = async (identityId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
)
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project)
);
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({
id,
identityId,
createdAt,
updatedAt,
identity: {
id: identityId,
name: identityName,
authMethod: identityAuthMethod
},
project: {
id: projectId,
name: projectName
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdentityId" });
}
};
const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
@@ -105,5 +202,9 @@ export const identityProjectDALFactory = (db: TDbClient) => {
}
};
return { ...identityProjectOrm, findByProjectId };
return {
...identityProjectOrm,
findByIdentityId,
findByProjectId
};
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityTokenAuthDALFactory = ReturnType<typeof identityTokenAuthDALFactory>;
export const identityTokenAuthDALFactory = (db: TDbClient) => {
const tokenAuthOrm = ormify(db, TableName.IdentityTokenAuth);
return tokenAuthOrm;
};
@@ -0,0 +1,470 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal";
import {
TAttachTokenAuthDTO,
TCreateTokenAuthTokenDTO,
TGetTokenAuthDTO,
TGetTokenAuthTokensDTO,
TRevokeTokenAuthDTO,
TRevokeTokenAuthTokenDTO,
TUpdateTokenAuthDTO,
TUpdateTokenAuthTokenDTO
} from "./identity-token-auth-types";
type TIdentityTokenAuthServiceFactoryDep = {
identityTokenAuthDAL: Pick<
TIdentityTokenAuthDALFactory,
"transaction" | "create" | "findOne" | "updateById" | "delete"
>;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<
TIdentityAccessTokenDALFactory,
"create" | "find" | "update" | "findById" | "findOne" | "updateById"
>;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAuthServiceFactory>;
export const identityTokenAuthServiceFactory = ({
identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
permissionService,
licenseService
}: TIdentityTokenAuthServiceFactoryDep) => {
const attachTokenAuth = async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add Token Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const doc = await identityTokenAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
return doc;
});
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
};
const updateTokenAuth = async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "Failed to update Token Auth"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityTokenAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityTokenAuth.accessTokenMaxTTL) >
(accessTokenMaxTTL || identityTokenAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedTokenAuth = await identityTokenAuthDAL.updateById(identityTokenAuth.id, {
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return {
...updatedTokenAuth,
orgId: identityMembershipOrg.orgId
};
};
const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth attached"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityTokenAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new UnauthorizedError({
message: "Failed to revoke Token Auth of identity with more privileged role"
});
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityTokenAuth;
};
const createTokenAuthToken = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId,
name
}: TCreateTokenAuthTokenDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to create token for identity with more privileged role"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityTokenAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
name
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityTokenAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg };
};
const getTokenAuthTokens = async ({
identityId,
offset = 0,
limit = 20,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetTokenAuthTokensDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const tokens = await identityAccessTokenDAL.find(
{
identityId
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return { tokens, identityMembershipOrg };
};
const updateTokenAuthToken = async ({
tokenId,
name,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findById(tokenId);
if (!foundToken) throw new NotFoundError({ message: "Failed to find token" });
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to update token for identity with more privileged role"
});
const [token] = await identityAccessTokenDAL.update(
{
identityId: foundToken.identityId,
id: tokenId
},
{
name
}
);
return { token, identityMembershipOrg };
};
const revokeTokenAuthToken = async ({
tokenId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false
});
if (!identityAccessToken)
throw new NotFoundError({
message: "Failed to find token"
});
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
identityId: identityAccessToken.identityId
});
if (!identityOrgMembership) {
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
isAccessTokenRevoked: true
});
return { revokedToken };
};
return {
attachTokenAuth,
updateTokenAuth,
getTokenAuth,
revokeIdentityTokenAuth,
createTokenAuthToken,
getTokenAuthTokens,
updateTokenAuthToken,
revokeTokenAuthToken
};
};
@@ -0,0 +1,45 @@
import { TProjectPermission } from "@app/lib/types";
export type TAttachTokenAuthDTO = {
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthDTO = {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateTokenAuthTokenDTO = {
identityId: string;
name?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthTokensDTO = {
identityId: string;
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthTokenDTO = {
tokenId: string;
name?: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthTokenDTO = {
tokenId: string;
} & Omit<TProjectPermission, "projectId">;
@@ -5,17 +5,25 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types";
import {
TCreateIdentityDTO,
TDeleteIdentityDTO,
TGetIdentityByIdDTO,
TListProjectIdentitiesByIdentityIdDTO,
TUpdateIdentityDTO
} from "./identity-types";
type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
};
@@ -25,6 +33,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
export const identityServiceFactory = ({
identityDAL,
identityOrgMembershipDAL,
identityProjectDAL,
permissionService,
licenseService
}: TIdentityServiceFactoryDep) => {
@@ -196,11 +205,35 @@ export const identityServiceFactory = ({
return identityMemberships;
};
const listProjectIdentitiesByIdentityId = async ({
identityId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectIdentitiesByIdentityIdDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityProjectDAL.findByIdentityId(identityId);
return identityMemberships;
};
return {
createIdentity,
updateIdentity,
deleteIdentity,
listOrgIdentities,
getIdentityById
getIdentityById,
listProjectIdentitiesByIdentityId
};
};
@@ -25,3 +25,7 @@ export interface TIdentityTrustedIp {
type: IPType;
prefix: number;
}
export type TListProjectIdentitiesByIdentityIdDTO = {
identityId: string;
} & Omit<TOrgPermission, "orgId">;
@@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type";
import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { TAdminSignUpDTO } from "./super-admin-types";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory;
@@ -79,7 +79,37 @@ export const superAdminServiceFactory = ({
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => {
if (data.enabledLoginMethods) {
const superAdminUser = await userDAL.findById(userId);
const loginMethodToAuthMethod = {
[LoginMethod.EMAIL]: [AuthMethod.EMAIL],
[LoginMethod.GOOGLE]: [AuthMethod.GOOGLE],
[LoginMethod.GITLAB]: [AuthMethod.GITLAB],
[LoginMethod.GITHUB]: [AuthMethod.GITHUB],
[LoginMethod.LDAP]: [AuthMethod.LDAP],
[LoginMethod.OIDC]: [AuthMethod.OIDC],
[LoginMethod.SAML]: [
AuthMethod.AZURE_SAML,
AuthMethod.GOOGLE_SAML,
AuthMethod.JUMPCLOUD_SAML,
AuthMethod.KEYCLOAK_SAML,
AuthMethod.OKTA_SAML
]
};
if (
!data.enabledLoginMethods.some((loginMethod) =>
loginMethodToAuthMethod[loginMethod as LoginMethod].some(
(authMethod) => superAdminUser.authMethods?.includes(authMethod)
)
)
) {
throw new BadRequestError({
message: "You must configure at least one auth method to prevent account lockout"
});
}
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
@@ -167,7 +197,7 @@ export const superAdminServiceFactory = ({
orgName: initialOrganizationName
});
await updateServerCfg({ initialized: true });
await updateServerCfg({ initialized: true }, userInfo.user.id);
const token = await authService.generateUserTokens({
user: userInfo.user,
authMethod: AuthMethod.EMAIL,
@@ -179,9 +209,25 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization };
};
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
offset,
searchTerm,
sortBy: "username"
});
};
const deleteUser = async (userId: string) => {
const user = await userDAL.deleteById(userId);
return user;
};
return {
initServerCfg,
updateServerCfg,
adminSignUp
adminSignUp,
getUsers,
deleteUser
};
};
@@ -15,3 +15,19 @@ export type TAdminSignUpDTO = {
ip: string;
userAgent: string;
};
export type TAdminGetUsersDTO = {
offset: number;
limit: number;
searchTerm: string;
};
export enum LoginMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
SAML = "saml",
LDAP = "ldap",
OIDC = "oidc"
}
@@ -0,0 +1,89 @@
import { PlainClient } from "@team-plain/typescript-sdk";
import { getConfig } from "@app/lib/config/env";
import { InternalServerError } from "@app/lib/errors";
import { TUserDALFactory } from "../user/user-dal";
type TUserEngagementServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById">;
};
export type TUserEngagementServiceFactory = ReturnType<typeof userEngagementServiceFactory>;
export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => {
const createUserWish = async (userId: string, text: string) => {
const user = await userDAL.findById(userId);
const appCfg = getConfig();
if (!appCfg.PLAIN_API_KEY) {
throw new InternalServerError({
message: "Plain is not configured."
});
}
const client = new PlainClient({
apiKey: appCfg.PLAIN_API_KEY
});
const customerUpsertRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email
},
onCreate: {
fullName: `${user.firstName} ${user.lastName}`,
shortName: user.firstName,
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: user.id
},
onUpdate: {
fullName: {
value: `${user.firstName} ${user.lastName}`
},
shortName: {
value: user.firstName
},
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: {
value: user.id
}
}
});
if (customerUpsertRes.error) {
throw new InternalServerError({ message: customerUpsertRes.error.message });
}
const createThreadRes = await client.createThread({
title: "Wish",
customerIdentifier: {
externalId: customerUpsertRes.data.customer.externalId
},
components: [
{
componentText: {
text
}
}
],
labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",")
});
if (createThreadRes.error) {
throw new InternalServerError({
message: createThreadRes.error.message
});
}
};
return {
createUserWish
};
};
+38 -3
View File
@@ -7,10 +7,11 @@ import {
TUserActionsUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate
TUserEncryptionKeysUpdate,
TUsers
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TUserDALFactory = ReturnType<typeof userDALFactory>;
@@ -18,6 +19,39 @@ export const userDALFactory = (db: TDbClient) => {
const userOrm = ormify(db, TableName.Users);
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
const getUsersByFilter = async ({
limit,
offset,
searchTerm,
sortBy
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TUsers;
}) => {
try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
if (searchTerm) {
query = query.where((qb) => {
void qb
.whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`);
});
}
if (sortBy) {
query = query.orderBy(sortBy);
}
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users));
} catch (error) {
throw new DatabaseError({ error, name: "Get users by filter" });
}
};
// USER ENCRYPTION FUNCTIONS
// -------------------------
const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
@@ -159,6 +193,7 @@ export const userDALFactory = (db: TDbClient) => {
upsertUserEncryptionKey,
createUserEncryption,
findOneUserAction,
createUserAction
createUserAction,
getUsersByFilter
};
};
+2 -2
View File
@@ -201,7 +201,7 @@ export const userServiceFactory = ({
return user;
};
const deleteMe = async (userId: string) => {
const deleteUser = async (userId: string) => {
const user = await userDAL.deleteById(userId);
return user;
};
@@ -301,7 +301,7 @@ export const userServiceFactory = ({
toggleUserMfa,
updateUserName,
updateAuthMethods,
deleteMe,
deleteUser,
getMe,
createUserAction,
getUserAction,
+84 -42
View File
@@ -4,55 +4,63 @@ import { AxiosError } from "axios";
import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const triggerWebhookRequest = async (
{ url, encryptedSecretKey, iv, tag, keyEncoding }: TWebhooks,
data: Record<string, unknown>
) => {
const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() };
const appCfg = getConfig();
export const decryptWebhookDetails = (webhook: TWebhooks) => {
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
let decryptedSecretKey = "";
let decryptedUrl = url;
if (encryptedSecretKey) {
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
let secretKey;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
// case: encoding scheme is base64
secretKey = decryptSymmetric({
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string,
key: rootEncryptionKey
});
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
// case: encoding scheme is utf8
secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string,
key: encryptionKey
});
}
if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`;
}
decryptedSecretKey = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string
});
}
if (urlCipherText) {
decryptedUrl = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: urlCipherText,
iv: urlIV as string,
tag: urlTag as string
});
}
return {
secretKey: decryptedSecretKey,
url: decryptedUrl
};
};
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`;
}
const req = await request.post(url, payload, {
headers,
timeout: WEBHOOK_TRIGGER_TIMEOUT,
signal: AbortSignal.timeout(WEBHOOK_TRIGGER_TIMEOUT)
});
return req;
};
@@ -60,15 +68,48 @@ export const getWebhookPayload = (
eventName: string,
workspaceId: string,
environment: string,
secretPath?: string
) => ({
event: eventName,
project: {
workspaceId,
environment,
secretPath
secretPath?: string,
type?: string | null
) => {
switch (type) {
case WebhookType.SLACK:
return {
text: "A secret value has been added or modified.",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Workspace ID",
value: workspaceId,
short: false
},
{
title: "Environment",
value: environment,
short: false
},
{
title: "Secret Path",
value: secretPath,
short: false
}
]
}
]
};
case WebhookType.GENERAL:
default:
return {
event: eventName,
project: {
workspaceId,
environment,
secretPath
}
};
}
});
};
export type TFnTriggerWebhookDTO = {
projectId: string;
@@ -95,9 +136,10 @@ export const fnTriggerWebhook = async ({
logger.info("Secret webhook job started", { environment, secretPath, projectId });
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath))
triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath, hook.type))
)
);
// filter hooks by status
const successWebhooks = webhooksTriggered
.filter(({ status }) => status === "fulfilled")
+33 -27
View File
@@ -1,15 +1,14 @@
import { ForbiddenError } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas";
import { TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { encryptSymmetric, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import {
TCreateWebhookDTO,
TDeleteWebhookDTO,
@@ -36,7 +35,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
webhookUrl,
environment,
secretPath,
webhookSecretKey
webhookSecretKey,
type
}: TCreateWebhookDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -50,30 +50,29 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
if (!env) throw new BadRequestError({ message: "Env not found" });
const insertDoc: TWebhooksInsert = {
url: webhookUrl,
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/"
secretPath: secretPath || "/",
type
};
if (webhookSecretKey) {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric(webhookSecretKey, rootEncryptionKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(webhookSecretKey, encryptionKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM;
insertDoc.keyEncoding = SecretKeyEncoding.UTF8;
}
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
if (webhookUrl) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
insertDoc.urlCipherText = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
const webhook = await webhookDAL.create(insertDoc);
@@ -131,7 +130,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath)
getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath, webhook.type)
);
} catch (err) {
webhookError = (err as Error).message;
@@ -162,7 +161,14 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
return webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
return webhooks.map((w) => {
const { url } = decryptWebhookDetails(w);
return {
...w,
url
};
});
};
return {
@@ -5,6 +5,7 @@ export type TCreateWebhookDTO = {
secretPath?: string;
webhookUrl: string;
webhookSecretKey?: string;
type: string;
} & TProjectPermission;
export type TUpdateWebhookDTO = {
@@ -24,3 +25,8 @@ export type TListWebhookDTO = {
environment?: string;
secretPath?: string;
} & TProjectPermission;
export enum WebhookType {
GENERAL = "general",
SLACK = "slack"
}