diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 325da75e1..5c05e3f14 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -3,13 +3,12 @@ import "fastify"; import { Redis } from "ioredis"; import { TUsers } from "@app/db/schemas"; -import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; -import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; -import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; -import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; -import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; +import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; +import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types"; +import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; +import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; +import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types"; import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; @@ -25,14 +24,13 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; -import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; -import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; -import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; -import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types"; -import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; -import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; +import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; +import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; +import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types"; +import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types"; +import { TScimServiceFactory } from "@app/ee/services/scim/scim-types"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; @@ -44,7 +42,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; -import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; +import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index 5a8dd3d05..cdb8c3028 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -1,6 +1,6 @@ import knex, { Knex } from "knex"; -export type TDbClient = ReturnType; +export type TDbClient = Knex; export const initDbConnection = ({ dbConnectionUri, dbRootCert, diff --git a/backend/src/db/migrations/20250613153957_add-machine-identity-delete-protection.ts b/backend/src/db/migrations/20250613153957_add-machine-identity-delete-protection.ts new file mode 100644 index 000000000..2cf19c1b8 --- /dev/null +++ b/backend/src/db/migrations/20250613153957_add-machine-identity-delete-protection.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection"); + if (!hasCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.boolean("hasDeleteProtection").notNullable().defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection"); + if (hasCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.dropColumn("hasDeleteProtection"); + }); + } +} diff --git a/backend/src/db/migrations/20250618172150_increase-aws-arn-field-size.ts b/backend/src/db/migrations/20250618172150_increase-aws-arn-field-size.ts new file mode 100644 index 000000000..a349ec2bc --- /dev/null +++ b/backend/src/db/migrations/20250618172150_increase-aws-arn-field-size.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns"); + if (hasColumn) { + await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => { + t.string("allowedPrincipalArns", 2048).notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns"); + if (hasColumn) { + await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => { + t.string("allowedPrincipalArns", 255).notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index adf3a6ef2..a592e2480 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({ name: z.string(), authMethod: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + hasDeleteProtection: z.boolean().default(false) }); export type TIdentities = z.infer; diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index d10e1800b..ec235d34e 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { id: z.string().trim().describe(GROUPS.GET_BY_ID.id) }), response: { - 200: GroupsSchema + 200: GroupsSchema.extend({ + customRoleSlug: z.string().nullable() + }) } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index eed5cd34a..ce745245f 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -285,6 +285,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv commits: secretRawSchema .omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true }) .extend({ + secretValueHidden: z.boolean(), secretValue: z.string().optional(), isRotatedSecret: z.boolean().optional(), op: z.string(), @@ -296,6 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv version: z.number(), secretKey: z.string(), secretValue: z.string().optional(), + secretValueHidden: z.boolean(), secretComment: z.string().optional() }) .optional() @@ -306,6 +308,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv version: z.number(), secretKey: z.string(), secretValue: z.string().optional(), + secretValueHidden: z.boolean(), secretComment: z.string().optional(), tags: SanitizedTagSchema.array().optional(), secretMetadata: ResourceMetadataSchema.nullish() diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts index c141c762b..ee7157e72 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts @@ -1,15 +1,15 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TAccessApprovalPolicyApproverDALFactory = ReturnType; +export type TAccessApprovalPolicyApproverDALFactory = TOrmify; export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => { const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover); return { ...accessApprovalPolicyApproverOrm }; }; -export type TAccessApprovalPolicyBypasserDALFactory = ReturnType; +export type TAccessApprovalPolicyBypasserDALFactory = TOrmify; export const accessApprovalPolicyBypasserDALFactory = (db: TDbClient) => { const accessApprovalPolicyBypasserOrm = ormify(db, TableName.AccessApprovalPolicyBypasser); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index dbcc5ed14..9fa48ca15 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -3,13 +3,363 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies, TUsers } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter, TOrmify } from "@app/lib/knex"; -import { ApproverType, BypasserType } from "./access-approval-policy-types"; +import { + ApproverType, + BypasserType, + TCreateAccessApprovalPolicy, + TDeleteAccessApprovalPolicy, + TGetAccessApprovalPolicyByIdDTO, + TGetAccessPolicyCountByEnvironmentDTO, + TListAccessApprovalPoliciesDTO, + TUpdateAccessApprovalPolicy +} from "./access-approval-policy-types"; -export type TAccessApprovalPolicyDALFactory = ReturnType; +export interface TAccessApprovalPolicyDALFactory + extends Omit, "findById" | "find"> { + find: ( + filter: TFindFilter< + TAccessApprovalPolicies & { + projectId: string; + } + >, + customFilter?: { + policyId?: string; + }, + tx?: Knex + ) => Promise< + { + approvers: ( + | { + id: string | null | undefined; + type: ApproverType.User; + name: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + id: string | null | undefined; + type: ApproverType.Group; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + bypassers: ( + | { + id: string | null | undefined; + type: BypasserType.User; + name: string; + } + | { + id: string | null | undefined; + type: BypasserType.Group; + } + )[]; + }[] + >; + findById: ( + policyId: string, + tx?: Knex + ) => Promise< + | { + approvers: { + id: string | null | undefined; + type: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + }[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + } + | undefined + >; + softDeleteById: ( + policyId: string, + tx?: Knex + ) => Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + }>; + findLastValidPolicy: ( + { + envId, + secretPath + }: { + envId: string; + secretPath: string; + }, + tx?: Knex + ) => Promise< + | { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + } + | undefined + >; +} -export const accessApprovalPolicyDALFactory = (db: TDbClient) => { +export interface TAccessApprovalPolicyServiceFactory { + getAccessPolicyCountByEnvSlug: ({ + actor, + actorOrgId, + actorAuthMethod, + projectSlug, + actorId, + envSlug + }: TGetAccessPolicyCountByEnvironmentDTO) => Promise<{ + count: number; + }>; + createAccessApprovalPolicy: ({ + name, + actor, + actorId, + actorOrgId, + secretPath, + actorAuthMethod, + approvals, + approvers, + bypassers, + projectSlug, + environment, + enforcementLevel, + allowedSelfApprovals, + approvalsRequired + }: TCreateAccessApprovalPolicy) => Promise<{ + environment: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + projectId: string; + slug: string; + position: number; + }; + projectId: string; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + }>; + deleteAccessApprovalPolicy: ({ + policyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TDeleteAccessApprovalPolicy) => Promise<{ + approvers: { + id: string | null | undefined; + type: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + }[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + }>; + updateAccessApprovalPolicy: ({ + policyId, + approvers, + bypassers, + secretPath, + name, + actorId, + actor, + actorOrgId, + actorAuthMethod, + approvals, + enforcementLevel, + allowedSelfApprovals, + approvalsRequired + }: TUpdateAccessApprovalPolicy) => Promise<{ + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + }>; + getAccessApprovalPolicyByProjectSlug: ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectSlug + }: TListAccessApprovalPoliciesDTO) => Promise< + { + approvers: ( + | { + id: string | null | undefined; + type: ApproverType; + name: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + id: string | null | undefined; + type: ApproverType; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + bypassers: ( + | { + id: string | null | undefined; + type: BypasserType; + name: string; + } + | { + id: string | null | undefined; + type: BypasserType; + } + )[]; + }[] + >; + getAccessApprovalPolicyById: ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + policyId + }: TGetAccessApprovalPolicyByIdDTO) => Promise<{ + approvers: ( + | { + id: string | null | undefined; + type: ApproverType.User; + name: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + id: string | null | undefined; + type: ApproverType.Group; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + bypassers: ( + | { + id: string | null | undefined; + type: BypasserType.User; + name: string; + } + | { + id: string | null | undefined; + type: BypasserType.Group; + } + )[]; + }>; +} + +export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPolicyDALFactory => { const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); const accessApprovalPolicyFindQuery = async ( @@ -61,7 +411,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { return result; }; - const findById = async (policyId: string, tx?: Knex) => { + const findById: TAccessApprovalPolicyDALFactory["findById"] = async (policyId, tx) => { try { const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { [`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId @@ -112,13 +462,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { } }; - const find = async ( - filter: TFindFilter, - customFilter?: { - policyId?: string; - }, - tx?: Knex - ) => { + const find: TAccessApprovalPolicyDALFactory["find"] = async (filter, customFilter, tx) => { try { const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter); @@ -141,7 +485,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { label: "approvers" as const, mapper: ({ approverUserId: id, approverUsername, approverSequence, approvalsRequired }) => ({ id, - type: ApproverType.User, + type: ApproverType.User as const, name: approverUsername, sequence: approverSequence, approvalsRequired @@ -152,7 +496,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { label: "approvers" as const, mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({ id, - type: ApproverType.Group, + type: ApproverType.Group as const, sequence: approverSequence, approvalsRequired }) @@ -162,7 +506,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { label: "bypassers" as const, mapper: ({ bypasserUserId: id, bypasserUsername }) => ({ id, - type: BypasserType.User, + type: BypasserType.User as const, name: bypasserUsername }) }, @@ -171,7 +515,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { label: "bypassers" as const, mapper: ({ bypasserGroupId: id }) => ({ id, - type: BypasserType.Group + type: BypasserType.Group as const }) } ] @@ -186,12 +530,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { } }; - const softDeleteById = async (policyId: string, tx?: Knex) => { + const softDeleteById: TAccessApprovalPolicyDALFactory["softDeleteById"] = async (policyId, tx) => { const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx); return softDeletedPolicy; }; - const findLastValidPolicy = async ({ envId, secretPath }: { envId: string; secretPath: string }, tx?: Knex) => { + const findLastValidPolicy: TAccessApprovalPolicyDALFactory["findLastValidPolicy"] = async ( + { envId, secretPath }, + tx + ) => { try { const result = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy) .where( diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 7ff9d65cc..4e818df2a 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -24,9 +24,8 @@ import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { ApproverType, BypasserType, - TCreateAccessApprovalPolicy, + TAccessApprovalPolicyServiceFactory, TDeleteAccessApprovalPolicy, - TGetAccessApprovalPolicyByIdDTO, TGetAccessPolicyCountByEnvironmentDTO, TListAccessApprovalPoliciesDTO, TUpdateAccessApprovalPolicy @@ -48,8 +47,6 @@ type TAccessApprovalPolicyServiceFactoryDep = { orgMembershipDAL: Pick; }; -export type TAccessApprovalPolicyServiceFactory = ReturnType; - export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, @@ -63,8 +60,8 @@ export const accessApprovalPolicyServiceFactory = ({ additionalPrivilegeDAL, accessApprovalRequestReviewerDAL, orgMembershipDAL -}: TAccessApprovalPolicyServiceFactoryDep) => { - const createAccessApprovalPolicy = async ({ +}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { + const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({ name, actor, actorId, @@ -79,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({ enforcementLevel, allowedSelfApprovals, approvalsRequired - }: TCreateAccessApprovalPolicy) => { + }) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -240,31 +237,26 @@ export const accessApprovalPolicyServiceFactory = ({ return { ...accessApproval, environment: env, projectId: project.id }; }; - const getAccessApprovalPolicyByProjectSlug = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectSlug - }: TListAccessApprovalPoliciesDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); + const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] = + async ({ actorId, actor, actorOrgId, actorAuthMethod, projectSlug }: TListAccessApprovalPoliciesDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - // Anyone in the project should be able to get the policies. - await permissionService.getProjectPermission({ - actor, - actorId, - projectId: project.id, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.SecretManager - }); + // Anyone in the project should be able to get the policies. + await permissionService.getProjectPermission({ + actor, + actorId, + projectId: project.id, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); - const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); - return accessApprovalPolicies; - }; + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); + return accessApprovalPolicies; + }; - const updateAccessApprovalPolicy = async ({ + const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({ policyId, approvers, bypassers, @@ -483,6 +475,7 @@ export const accessApprovalPolicyServiceFactory = ({ return doc; }); + return { ...updatedPolicy, environment: accessApprovalPolicy.environment, @@ -490,7 +483,7 @@ export const accessApprovalPolicyServiceFactory = ({ }; }; - const deleteAccessApprovalPolicy = async ({ + const deleteAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["deleteAccessApprovalPolicy"] = async ({ policyId, actor, actorId, @@ -539,7 +532,7 @@ export const accessApprovalPolicyServiceFactory = ({ return policy; }; - const getAccessPolicyCountByEnvSlug = async ({ + const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({ actor, actorOrgId, actorAuthMethod, @@ -576,13 +569,13 @@ export const accessApprovalPolicyServiceFactory = ({ return { count: policies.length }; }; - const getAccessApprovalPolicyById = async ({ + const getAccessApprovalPolicyById: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyById"] = async ({ actorId, actor, actorOrgId, actorAuthMethod, policyId - }: TGetAccessApprovalPolicyByIdDTO) => { + }) => { const [policy] = await accessApprovalPolicyDAL.find({}, { policyId }); if (!policy) { diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index bdb50dde1..6806c7123 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -1,7 +1,7 @@ import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod } from "@app/services/auth/auth-type"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; export type TIsApproversValid = { userIds: string[]; @@ -76,3 +76,217 @@ export type TGetAccessApprovalPolicyByIdDTO = { export type TListAccessApprovalPoliciesDTO = { projectSlug: string; } & Omit; + +export interface TAccessApprovalPolicyServiceFactory { + getAccessPolicyCountByEnvSlug: ({ + actor, + actorOrgId, + actorAuthMethod, + projectSlug, + actorId, + envSlug + }: TGetAccessPolicyCountByEnvironmentDTO) => Promise<{ + count: number; + }>; + createAccessApprovalPolicy: ({ + name, + actor, + actorId, + actorOrgId, + secretPath, + actorAuthMethod, + approvals, + approvers, + bypassers, + projectSlug, + environment, + enforcementLevel, + allowedSelfApprovals, + approvalsRequired + }: TCreateAccessApprovalPolicy) => Promise<{ + environment: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + projectId: string; + slug: string; + position: number; + }; + projectId: string; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + }>; + deleteAccessApprovalPolicy: ({ + policyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TDeleteAccessApprovalPolicy) => Promise<{ + approvers: { + id: string | null | undefined; + type: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + }[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + }>; + updateAccessApprovalPolicy: ({ + policyId, + approvers, + bypassers, + secretPath, + name, + actorId, + actor, + actorOrgId, + actorAuthMethod, + approvals, + enforcementLevel, + allowedSelfApprovals, + approvalsRequired + }: TUpdateAccessApprovalPolicy) => Promise<{ + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + }>; + getAccessApprovalPolicyByProjectSlug: ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectSlug + }: TListAccessApprovalPoliciesDTO) => Promise< + { + approvers: ( + | { + id: string | null | undefined; + type: ApproverType; + name: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + id: string | null | undefined; + type: ApproverType; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + bypassers: ( + | { + id: string | null | undefined; + type: BypasserType; + name: string; + } + | { + id: string | null | undefined; + type: BypasserType; + } + )[]; + }[] + >; + getAccessApprovalPolicyById: ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + policyId + }: TGetAccessApprovalPolicyByIdDTO) => Promise<{ + approvers: ( + | { + id: string | null | undefined; + type: ApproverType.User; + name: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + id: string | null | undefined; + type: ApproverType.Group; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + envId: string; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath?: string | null | undefined; + deletedAt?: Date | null | undefined; + environment: { + id: string; + name: string; + slug: string; + }; + projectId: string; + bypassers: ( + | { + id: string | null | undefined; + type: BypasserType.User; + name: string; + } + | { + id: string | null | undefined; + type: BypasserType.Group; + } + )[]; + }>; +} diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index cb163f0c2..c69c55041 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -9,229 +9,442 @@ import { TUsers } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter, TOrmify } from "@app/lib/knex"; import { ApprovalStatus } from "./access-approval-request-types"; -export type TAccessApprovalRequestDALFactory = ReturnType; +export interface TAccessApprovalRequestDALFactory extends Omit, "findById"> { + findById: ( + id: string, + tx?: Knex + ) => Promise< + | { + policy: { + approvers: ( + | { + userId: string | null | undefined; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + bypassers: ( + | { + userId: string | null | undefined; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + } + | { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + } + )[]; + id: string; + name: string; + approvals: number; + secretPath: string | null | undefined; + enforcementLevel: string; + allowedSelfApprovals: boolean; + deletedAt: Date | null | undefined; + }; + projectId: string; + environment: string; + requestedByUser: { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + }; + status: string; + id: string; + createdAt: Date; + updatedAt: Date; + policyId: string; + isTemporary: boolean; + requestedByUserId: string; + privilegeId?: string | null | undefined; + requestedBy?: string | null | undefined; + temporaryRange?: string | null | undefined; + permissions?: unknown; + note?: string | null | undefined; + privilegeDeletedAt?: Date | null | undefined; + reviewers: { + userId: string; + status: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + }[]; + approvers: ( + | { + userId: string | null | undefined; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + | { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + } + )[]; + bypassers: ( + | { + userId: string | null | undefined; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + } + | { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + } + )[]; + } + | undefined + >; + findRequestsWithPrivilegeByPolicyIds: (policyIds: string[]) => Promise< + { + policy: { + approvers: ( + | { + userId: string | null | undefined; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + | { + userId: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + )[]; + bypassers: string[]; + id: string; + name: string; + approvals: number; + secretPath: string | null | undefined; + enforcementLevel: string; + allowedSelfApprovals: boolean; + envId: string; + deletedAt: Date | null | undefined; + }; + projectId: string; + environment: string; + environmentName: string; + requestedByUser: { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + }; + privilege: { + membershipId: string; + userId: string; + projectId: string; + isTemporary: boolean; + temporaryMode: string | null | undefined; + temporaryRange: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + permissions: unknown; + } | null; + isApproved: boolean; + status: string; + id: string; + createdAt: Date; + updatedAt: Date; + policyId: string; + isTemporary: boolean; + requestedByUserId: string; + privilegeId?: string | null | undefined; + requestedBy?: string | null | undefined; + temporaryRange?: string | null | undefined; + permissions?: unknown; + note?: string | null | undefined; + privilegeDeletedAt?: Date | null | undefined; + reviewers: { + userId: string; + status: string; + }[]; + approvers: ( + | { + userId: string | null | undefined; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + | { + userId: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + )[]; + bypassers: string[]; + }[] + >; + getCount: ({ projectId }: { projectId: string }) => Promise<{ + pendingCount: number; + finalizedCount: number; + }>; + resetReviewByPolicyId: (policyId: string, tx?: Knex) => Promise; +} -export const accessApprovalRequestDALFactory = (db: TDbClient) => { +export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalRequestDALFactory => { const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest); - const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { - try { - const docs = await db - .replicaNode()(TableName.AccessApprovalRequest) - .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) + const findRequestsWithPrivilegeByPolicyIds: TAccessApprovalRequestDALFactory["findRequestsWithPrivilegeByPolicyIds"] = + async (policyIds) => { + try { + const docs = await db + .replicaNode()(TableName.AccessApprovalRequest) + .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) - .leftJoin( - TableName.ProjectUserAdditionalPrivilege, - `${TableName.AccessApprovalRequest}.privilegeId`, - `${TableName.ProjectUserAdditionalPrivilege}.id` - ) - .leftJoin( - TableName.AccessApprovalPolicy, - `${TableName.AccessApprovalRequest}.policyId`, - `${TableName.AccessApprovalPolicy}.id` - ) - .leftJoin( - TableName.AccessApprovalRequestReviewer, - `${TableName.AccessApprovalRequest}.id`, - `${TableName.AccessApprovalRequestReviewer}.requestId` - ) - .leftJoin( - TableName.AccessApprovalPolicyApprover, - `${TableName.AccessApprovalPolicy}.id`, - `${TableName.AccessApprovalPolicyApprover}.policyId` - ) - .leftJoin( - db(TableName.Users).as("accessApprovalPolicyApproverUser"), - `${TableName.AccessApprovalPolicyApprover}.approverUserId`, - "accessApprovalPolicyApproverUser.id" - ) - .leftJoin( - TableName.UserGroupMembership, - `${TableName.AccessApprovalPolicyApprover}.approverGroupId`, - `${TableName.UserGroupMembership}.groupId` - ) - .leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + .leftJoin( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .leftJoin( + db(TableName.Users).as("accessApprovalPolicyApproverUser"), + `${TableName.AccessApprovalPolicyApprover}.approverUserId`, + "accessApprovalPolicyApproverUser.id" + ) + .leftJoin( + TableName.UserGroupMembership, + `${TableName.AccessApprovalPolicyApprover}.approverGroupId`, + `${TableName.UserGroupMembership}.groupId` + ) + .leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin( - TableName.AccessApprovalPolicyBypasser, - `${TableName.AccessApprovalPolicy}.id`, - `${TableName.AccessApprovalPolicyBypasser}.policyId` - ) - .leftJoin( - db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"), - `${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`, - `bypasserUserGroupMembership.groupId` - ) + .leftJoin( + TableName.AccessApprovalPolicyBypasser, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyBypasser}.policyId` + ) + .leftJoin( + db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"), + `${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`, + `bypasserUserGroupMembership.groupId` + ) - .join( - db(TableName.Users).as("requestedByUser"), - `${TableName.AccessApprovalRequest}.requestedByUserId`, - `requestedByUser.id` - ) + .join( + db(TableName.Users).as("requestedByUser"), + `${TableName.AccessApprovalRequest}.requestedByUserId`, + `requestedByUser.id` + ) - .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) - .select(selectAllTableCols(TableName.AccessApprovalRequest)) - .select( - db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), - db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), - db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), - db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), - db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), - db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), - db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"), - db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") - ) - .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) - .select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence")) - .select(db.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover)) - .select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId")) - .select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser)) - .select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId")) - .select( - db.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), - db.ref("email").withSchema(TableName.Users).as("approverGroupEmail"), - db.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"), - db.ref("username").withSchema(TableName.Users).as("approverGroupUsername") - ) - .select( - db.ref("projectId").withSchema(TableName.Environment), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName") - ) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), + db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), + db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"), + db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") + ) + .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) + .select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence")) + .select(db.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover)) + .select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId")) + .select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser)) + .select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId")) + .select( + db.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), + db.ref("email").withSchema(TableName.Users).as("approverGroupEmail"), + db.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"), + db.ref("username").withSchema(TableName.Users).as("approverGroupUsername") + ) + .select( + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) - .select( - db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"), - db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") - ) + .select( + db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"), + db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") + ) - // TODO: ADD SUPPORT FOR GROUPS!!!! - .select( - db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), - db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), - db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), - db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), + // TODO: ADD SUPPORT FOR GROUPS!!!! + .select( + db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), + db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), + db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), + db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"), - db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"), + db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"), + db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"), - db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), - db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("privilegeTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("privilegeTemporaryAccessEndTime"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessEndTime"), - db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") - ) - .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + ) + .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); - const formattedDocs = sqlNestRelationships({ - data: docs, - key: "id", - parentMapper: (doc) => ({ - ...AccessApprovalRequestsSchema.parse(doc), - projectId: doc.projectId, - environment: doc.envSlug, - environmentName: doc.envName, + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc), + projectId: doc.projectId, + environment: doc.envSlug, + environmentName: doc.envName, + policy: { + id: doc.policyId, + name: doc.policyName, + approvals: doc.policyApprovals, + secretPath: doc.policySecretPath, + enforcementLevel: doc.policyEnforcementLevel, + allowedSelfApprovals: doc.policyAllowedSelfApprovals, + envId: doc.policyEnvId, + deletedAt: doc.policyDeletedAt + }, + requestedByUser: { + userId: doc.requestedByUserId, + email: doc.requestedByUserEmail, + firstName: doc.requestedByUserFirstName, + lastName: doc.requestedByUserLastName, + username: doc.requestedByUserUsername + }, + privilege: doc.privilegeId + ? { + membershipId: doc.privilegeMembershipId, + userId: doc.privilegeUserId, + projectId: doc.projectId, + isTemporary: doc.privilegeIsTemporary, + temporaryMode: doc.privilegeTemporaryMode, + temporaryRange: doc.privilegeTemporaryRange, + temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime, + temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime, + permissions: doc.privilegePermissions + } + : null, + isApproved: doc.status === ApprovalStatus.APPROVED + }), + childrenMapper: [ + { + key: "reviewerUserId", + label: "reviewers" as const, + mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) + }, + { + key: "approverUserId", + label: "approvers" as const, + mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({ + userId: approverUserId, + sequence: approverSequence, + approvalsRequired, + email: approverEmail, + username: approverUsername + }) + }, + { + key: "approverGroupUserId", + label: "approvers" as const, + mapper: ({ + approverGroupUserId, + approverSequence, + approvalsRequired, + approverGroupEmail, + approverGroupUsername + }) => ({ + userId: approverGroupUserId, + sequence: approverSequence, + approvalsRequired, + email: approverGroupEmail, + username: approverGroupUsername + }) + }, + { key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId }, + { + key: "bypasserGroupUserId", + label: "bypassers" as const, + mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId + } + ] + }); + + if (!formattedDocs) return []; + + return formattedDocs.map((doc) => ({ + ...doc, policy: { - id: doc.policyId, - name: doc.policyName, - approvals: doc.policyApprovals, - secretPath: doc.policySecretPath, - enforcementLevel: doc.policyEnforcementLevel, - allowedSelfApprovals: doc.policyAllowedSelfApprovals, - envId: doc.policyEnvId, - deletedAt: doc.policyDeletedAt - }, - requestedByUser: { - userId: doc.requestedByUserId, - email: doc.requestedByUserEmail, - firstName: doc.requestedByUserFirstName, - lastName: doc.requestedByUserLastName, - username: doc.requestedByUserUsername - }, - privilege: doc.privilegeId - ? { - membershipId: doc.privilegeMembershipId, - userId: doc.privilegeUserId, - projectId: doc.projectId, - isTemporary: doc.privilegeIsTemporary, - temporaryMode: doc.privilegeTemporaryMode, - temporaryRange: doc.privilegeTemporaryRange, - temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime, - temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime, - permissions: doc.privilegePermissions - } - : null, - isApproved: doc.status === ApprovalStatus.APPROVED - }), - childrenMapper: [ - { - key: "reviewerUserId", - label: "reviewers" as const, - mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) - }, - { - key: "approverUserId", - label: "approvers" as const, - mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({ - userId: approverUserId, - sequence: approverSequence, - approvalsRequired, - email: approverEmail, - username: approverUsername - }) - }, - { - key: "approverGroupUserId", - label: "approvers" as const, - mapper: ({ - approverGroupUserId, - approverSequence, - approvalsRequired, - approverGroupEmail, - approverGroupUsername - }) => ({ - userId: approverGroupUserId, - sequence: approverSequence, - approvalsRequired, - email: approverGroupEmail, - username: approverGroupUsername - }) - }, - { key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId }, - { - key: "bypasserGroupUserId", - label: "bypassers" as const, - mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId + ...doc.policy, + approvers: doc.approvers.filter((el) => el.userId).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)), + bypassers: doc.bypassers } - ] - }); - - if (!formattedDocs) return []; - - return formattedDocs.map((doc) => ({ - ...doc, - policy: { - ...doc.policy, - approvers: doc.approvers.filter((el) => el.userId).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)), - bypassers: doc.bypassers - } - })); - } catch (error) { - throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); - } - }; + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); + } + }; const findQuery = (filter: TFindFilter, tx: Knex) => tx(TableName.AccessApprovalRequest) @@ -354,7 +567,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") ); - const findById = async (id: string, tx?: Knex) => { + const findById: TAccessApprovalRequestDALFactory["findById"] = async (id, tx) => { try { const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const docs = await sql; @@ -489,7 +702,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { } }; - const getCount = async ({ projectId }: { projectId: string }) => { + const getCount: TAccessApprovalRequestDALFactory["getCount"] = async ({ projectId }) => { try { const accessRequests = await db .replicaNode()(TableName.AccessApprovalRequest) @@ -555,7 +768,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { } }; - const resetReviewByPolicyId = async (policyId: string, tx?: Knex) => { + const resetReviewByPolicyId: TAccessApprovalRequestDALFactory["resetReviewByPolicyId"] = async (policyId, tx) => { try { await (tx || db)(TableName.AccessApprovalRequestReviewer) .leftJoin( diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts index 251015b22..0da53d168 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TAccessApprovalRequestReviewerDALFactory = ReturnType; +export type TAccessApprovalRequestReviewerDALFactory = TOrmify; -export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => { +export const accessApprovalRequestReviewerDALFactory = (db: TDbClient): TAccessApprovalRequestReviewerDALFactory => { const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer); return secretApprovalRequestReviewerOrm; }; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 8fafc6b85..70d491bf0 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -23,19 +23,13 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TGroupDALFactory } from "../group/group-dal"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; -import { - ApprovalStatus, - TCreateAccessApprovalRequestDTO, - TGetAccessRequestCountDTO, - TListApprovalRequestsDTO, - TReviewAccessRequestDTO -} from "./access-approval-request-types"; +import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { additionalPrivilegeDAL: Pick; @@ -75,8 +69,6 @@ type TSecretApprovalRequestServiceFactoryDep = { projectMicrosoftTeamsConfigDAL: Pick; }; -export type TAccessApprovalRequestServiceFactory = ReturnType; - export const accessApprovalRequestServiceFactory = ({ groupDAL, projectDAL, @@ -93,8 +85,8 @@ export const accessApprovalRequestServiceFactory = ({ microsoftTeamsService, projectMicrosoftTeamsConfigDAL, projectSlackConfigDAL -}: TSecretApprovalRequestServiceFactoryDep) => { - const createAccessApprovalRequest = async ({ +}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => { + const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({ isTemporary, temporaryRange, actorId, @@ -104,7 +96,7 @@ export const accessApprovalRequestServiceFactory = ({ actorAuthMethod, projectSlug, note - }: TCreateAccessApprovalRequestDTO) => { + }) => { const cfg = getConfig(); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -281,7 +273,7 @@ export const accessApprovalRequestServiceFactory = ({ return { request: approval }; }; - const listApprovalRequests = async ({ + const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({ projectSlug, authorProjectMembershipId, envSlug, @@ -289,7 +281,7 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actorId, actorAuthMethod - }: TListApprovalRequestsDTO) => { + }) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -319,7 +311,7 @@ export const accessApprovalRequestServiceFactory = ({ return { requests }; }; - const reviewAccessRequest = async ({ + const reviewAccessRequest: TAccessApprovalRequestServiceFactory["reviewAccessRequest"] = async ({ requestId, actor, status, @@ -327,7 +319,7 @@ export const accessApprovalRequestServiceFactory = ({ actorAuthMethod, actorOrgId, bypassReason - }: TReviewAccessRequestDTO) => { + }) => { const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); if (!accessApprovalRequest) { throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); @@ -566,7 +558,13 @@ export const accessApprovalRequestServiceFactory = ({ return reviewStatus; }; - const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => { + const getCount: TAccessApprovalRequestServiceFactory["getCount"] = async ({ + projectSlug, + actor, + actorAuthMethod, + actorId, + actorOrgId + }) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index 162f8b3c6..fb3e78de0 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -34,3 +34,124 @@ export type TListApprovalRequestsDTO = { authorProjectMembershipId?: string; envSlug?: string; } & Omit; + +export interface TAccessApprovalRequestServiceFactory { + createAccessApprovalRequest: (arg: TCreateAccessApprovalRequestDTO) => Promise<{ + request: { + status: string; + id: string; + createdAt: Date; + updatedAt: Date; + policyId: string; + isTemporary: boolean; + requestedByUserId: string; + privilegeId?: string | null | undefined; + requestedBy?: string | null | undefined; + temporaryRange?: string | null | undefined; + permissions?: unknown; + note?: string | null | undefined; + privilegeDeletedAt?: Date | null | undefined; + }; + }>; + listApprovalRequests: (arg: TListApprovalRequestsDTO) => Promise<{ + requests: { + policy: { + approvers: ( + | { + userId: string | null | undefined; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + | { + userId: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + )[]; + bypassers: string[]; + id: string; + name: string; + approvals: number; + secretPath: string | null | undefined; + enforcementLevel: string; + allowedSelfApprovals: boolean; + envId: string; + deletedAt: Date | null | undefined; + }; + projectId: string; + environment: string; + environmentName: string; + requestedByUser: { + userId: string; + email: string | null | undefined; + firstName: string | null | undefined; + lastName: string | null | undefined; + username: string; + }; + privilege: { + membershipId: string; + userId: string; + projectId: string; + isTemporary: boolean; + temporaryMode: string | null | undefined; + temporaryRange: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + permissions: unknown; + } | null; + isApproved: boolean; + status: string; + id: string; + createdAt: Date; + updatedAt: Date; + policyId: string; + isTemporary: boolean; + requestedByUserId: string; + privilegeId?: string | null | undefined; + requestedBy?: string | null | undefined; + temporaryRange?: string | null | undefined; + permissions?: unknown; + note?: string | null | undefined; + privilegeDeletedAt?: Date | null | undefined; + reviewers: { + userId: string; + status: string; + }[]; + approvers: ( + | { + userId: string | null | undefined; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + | { + userId: string; + sequence: number | null | undefined; + approvalsRequired: number | null | undefined; + email: string | null | undefined; + username: string; + } + )[]; + bypassers: string[]; + }[]; + }>; + reviewAccessRequest: (arg: TReviewAccessRequestDTO) => Promise<{ + id: string; + requestId: string; + reviewerUserId: string; + status: string; + createdAt: Date; + updatedAt: Date; + }>; + getCount: (arg: TGetAccessRequestCountDTO) => Promise<{ + count: { + pendingCount: number; + finalizedCount: number; + }; + }>; +} diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts index 709ce44b6..d4a643d8b 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-service.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-service.ts @@ -7,29 +7,30 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionMemberActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TAssumeProjectPrivilegeDTO } from "./assume-privilege-types"; +import { TAssumePrivilegeServiceFactory } from "./assume-privilege-types"; type TAssumePrivilegeServiceFactoryDep = { projectDAL: Pick; permissionService: Pick; }; -export type TAssumePrivilegeServiceFactory = ReturnType; - -export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => { - const assumeProjectPrivileges = async ({ +export const assumePrivilegeServiceFactory = ({ + projectDAL, + permissionService +}: TAssumePrivilegeServiceFactoryDep): TAssumePrivilegeServiceFactory => { + const assumeProjectPrivileges: TAssumePrivilegeServiceFactory["assumeProjectPrivileges"] = async ({ targetActorType, targetActorId, projectId, actorPermissionDetails, tokenVersionId - }: TAssumeProjectPrivilegeDTO) => { + }) => { const project = await projectDAL.findById(projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); const { permission } = await permissionService.getProjectPermission({ @@ -79,7 +80,10 @@ export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken }; }; - const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => { + const verifyAssumePrivilegeToken: TAssumePrivilegeServiceFactory["verifyAssumePrivilegeToken"] = ( + token, + tokenVersionId + ) => { const appCfg = getConfig(); const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as { tokenVersionId: string; diff --git a/backend/src/ee/services/assume-privilege/assume-privilege-types.ts b/backend/src/ee/services/assume-privilege/assume-privilege-types.ts index 55b6c8449..999fcce41 100644 --- a/backend/src/ee/services/assume-privilege/assume-privilege-types.ts +++ b/backend/src/ee/services/assume-privilege/assume-privilege-types.ts @@ -8,3 +8,28 @@ export type TAssumeProjectPrivilegeDTO = { tokenVersionId: string; actorPermissionDetails: OrgServiceActor; }; + +export interface TAssumePrivilegeServiceFactory { + assumeProjectPrivileges: ({ + targetActorType, + targetActorId, + projectId, + actorPermissionDetails, + tokenVersionId + }: TAssumeProjectPrivilegeDTO) => Promise<{ + actorType: ActorType.USER | ActorType.IDENTITY; + actorId: string; + projectId: string; + assumePrivilegesToken: string; + }>; + verifyAssumePrivilegeToken: ( + token: string, + tokenVersionId: string + ) => { + tokenVersionId: string; + projectId: string; + requesterId: string; + actorType: ActorType; + actorId: string; + }; +} diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-dal.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-dal.ts index 436821ae9..c957890e4 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-dal.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TAuditLogStreamDALFactory = ReturnType; +export type TAuditLogStreamDALFactory = TOrmify; -export const auditLogStreamDALFactory = (db: TDbClient) => { +export const auditLogStreamDALFactory = (db: TDbClient): TAuditLogStreamDALFactory => { const orm = ormify(db, TableName.AuditLogStream); return orm; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index c5a562a18..72207aca1 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -11,16 +11,9 @@ import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; -import { - LogStreamHeaders, - TCreateAuditLogStreamDTO, - TDeleteAuditLogStreamDTO, - TGetDetailsAuditLogStreamDTO, - TListAuditLogStreamDTO, - TUpdateAuditLogStreamDTO -} from "./audit-log-stream-types"; +import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types"; type TAuditLogStreamServiceFactoryDep = { auditLogStreamDAL: TAuditLogStreamDALFactory; @@ -28,21 +21,19 @@ type TAuditLogStreamServiceFactoryDep = { licenseService: Pick; }; -export type TAuditLogStreamServiceFactory = ReturnType; - export const auditLogStreamServiceFactory = ({ auditLogStreamDAL, permissionService, licenseService -}: TAuditLogStreamServiceFactoryDep) => { - const create = async ({ +}: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => { + const create: TAuditLogStreamServiceFactory["create"] = async ({ url, actor, headers = [], actorId, actorOrgId, actorAuthMethod - }: TCreateAuditLogStreamDTO) => { + }) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); const plan = await licenseService.getPlan(actorOrgId); @@ -110,7 +101,7 @@ export const auditLogStreamServiceFactory = ({ return logStream; }; - const updateById = async ({ + const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({ id, url, actor, @@ -118,7 +109,7 @@ export const auditLogStreamServiceFactory = ({ actorId, actorOrgId, actorAuthMethod - }: TUpdateAuditLogStreamDTO) => { + }) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); const plan = await licenseService.getPlan(actorOrgId); @@ -175,7 +166,13 @@ export const auditLogStreamServiceFactory = ({ return updatedLogStream; }; - const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => { + const deleteById: TAuditLogStreamServiceFactory["deleteById"] = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod + }) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); const logStream = await auditLogStreamDAL.findById(id); @@ -189,7 +186,13 @@ export const auditLogStreamServiceFactory = ({ return deletedLogStream; }; - const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => { + const getById: TAuditLogStreamServiceFactory["getById"] = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod + }) => { const logStream = await auditLogStreamDAL.findById(id); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); @@ -212,7 +215,7 @@ export const auditLogStreamServiceFactory = ({ return { ...logStream, headers }; }; - const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => { + const list: TAuditLogStreamServiceFactory["list"] = async ({ actor, actorId, actorOrgId, actorAuthMethod }) => { const { permission } = await permissionService.getOrgPermission( actor, actorId, diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts index 3c22251d7..4c4a5609e 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts @@ -1,3 +1,4 @@ +import { TAuditLogStreams } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; export type LogStreamHeaders = { @@ -25,3 +26,23 @@ export type TListAuditLogStreamDTO = Omit; export type TGetDetailsAuditLogStreamDTO = Omit & { id: string; }; + +export type TAuditLogStreamServiceFactory = { + create: (arg: TCreateAuditLogStreamDTO) => Promise; + updateById: (arg: TUpdateAuditLogStreamDTO) => Promise; + deleteById: (arg: TDeleteAuditLogStreamDTO) => Promise; + getById: (arg: TGetDetailsAuditLogStreamDTO) => Promise<{ + headers: LogStreamHeaders[] | undefined; + orgId: string; + url: string; + id: string; + createdAt: Date; + updatedAt: Date; + encryptedHeadersCiphertext?: string | null | undefined; + encryptedHeadersIV?: string | null | undefined; + encryptedHeadersTag?: string | null | undefined; + encryptedHeadersAlgorithm?: string | null | undefined; + encryptedHeadersKeyEncoding?: string | null | undefined; + }>; + list: (arg: TListAuditLogStreamDTO) => Promise; +}; diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index ad6b72e33..874460b36 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -2,16 +2,29 @@ import knex from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TAuditLogs } from "@app/db/schemas"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; import { ActorType } from "@app/services/auth/auth-type"; import { EventType, filterableSecretEvents } from "./audit-log-types"; -export type TAuditLogDALFactory = ReturnType; +export interface TAuditLogDALFactory extends Omit, "find"> { + pruneAuditLog: (tx?: knex.Knex) => Promise; + find: ( + arg: Omit & { + actorId?: string | undefined; + actorType?: ActorType | undefined; + secretPath?: string | undefined; + secretKey?: string | undefined; + eventType?: EventType[] | undefined; + eventMetadata?: Record | undefined; + }, + tx?: knex.Knex + ) => Promise; +} type TFindQuery = { actor?: string; @@ -29,7 +42,7 @@ type TFindQuery = { export const auditLogDALFactory = (db: TDbClient) => { const auditLogOrm = ormify(db, TableName.AuditLog); - const find = async ( + const find: TAuditLogDALFactory["find"] = async ( { orgId, projectId, @@ -45,15 +58,8 @@ export const auditLogDALFactory = (db: TDbClient) => { secretKey, eventType, eventMetadata - }: Omit & { - actorId?: string; - actorType?: ActorType; - secretPath?: string; - secretKey?: string; - eventType?: EventType[]; - eventMetadata?: Record; }, - tx?: knex.Knex + tx ) => { if (!orgId && !projectId) { throw new Error("Either orgId or projectId must be provided"); @@ -154,7 +160,7 @@ export const auditLogDALFactory = (db: TDbClient) => { }; // delete all audit log that have expired - const pruneAuditLog = async (tx?: knex.Knex) => { + const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => { const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; const MAX_RETRY_ON_FAILURE = 3; diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index e312c3886..0f774e911 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -21,7 +21,9 @@ type TAuditLogQueueServiceFactoryDep = { licenseService: Pick; }; -export type TAuditLogQueueServiceFactory = Awaited>; +export type TAuditLogQueueServiceFactory = { + pushToLog: (data: TCreateAuditLogDTO) => Promise; +}; // keep this timeout 5s it must be fast because else the queue will take time to finish // audit log is a crowded queue thus needs to be fast @@ -33,7 +35,7 @@ export const auditLogQueueServiceFactory = async ({ projectDAL, licenseService, auditLogStreamDAL -}: TAuditLogQueueServiceFactoryDep) => { +}: TAuditLogQueueServiceFactoryDep): Promise => { const appCfg = getConfig(); const pushToLog = async (data: TCreateAuditLogDTO) => { diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index ce6689fe9..4bea26ac6 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -7,11 +7,11 @@ import { BadRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogQueueServiceFactory } from "./audit-log-queue"; -import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "./audit-log-types"; type TAuditLogServiceFactoryDep = { auditLogDAL: TAuditLogDALFactory; @@ -19,14 +19,18 @@ type TAuditLogServiceFactoryDep = { auditLogQueue: TAuditLogQueueServiceFactory; }; -export type TAuditLogServiceFactory = ReturnType; - export const auditLogServiceFactory = ({ auditLogDAL, auditLogQueue, permissionService -}: TAuditLogServiceFactoryDep) => { - const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => { +}: TAuditLogServiceFactoryDep): TAuditLogServiceFactory => { + const listAuditLogs: TAuditLogServiceFactory["listAuditLogs"] = async ({ + actorAuthMethod, + actorId, + actorOrgId, + actor, + filter + }) => { // Filter logs for specific project if (filter.projectId) { const { permission } = await permissionService.getProjectPermission({ @@ -75,7 +79,7 @@ export const auditLogServiceFactory = ({ })); }; - const createAuditLog = async (data: TCreateAuditLogDTO) => { + const createAuditLog: TAuditLogServiceFactory["createAuditLog"] = async (data) => { const appCfg = getConfig(); if (appCfg.DISABLE_AUDIT_LOG_GENERATION) { return; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 87a98305f..e72b9fa46 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -82,6 +82,32 @@ export type TCreateAuditLogDTO = { projectId?: string; } & BaseAuthData; +export type TAuditLogServiceFactory = { + createAuditLog: (data: TCreateAuditLogDTO) => Promise; + listAuditLogs: (arg: TListProjectAuditLogDTO) => Promise< + { + event: { + type: string; + metadata: unknown; + }; + actor: { + type: string; + metadata: unknown; + }; + id: string; + createdAt: Date; + updatedAt: Date; + orgId?: string | null | undefined; + userAgent?: string | null | undefined; + expiresAt?: Date | null | undefined; + ipAddress?: string | null | undefined; + userAgentType?: string | null | undefined; + projectId?: string | null | undefined; + projectName?: string | null | undefined; + }[] + >; +}; + export type AuditLogInfo = Pick; interface BaseAuthData { @@ -754,6 +780,7 @@ interface CreateIdentityEvent { metadata: { identityId: string; name: string; + hasDeleteProtection: boolean; }; } @@ -762,6 +789,7 @@ interface UpdateIdentityEvent { metadata: { identityId: string; name?: string; + hasDeleteProtection?: boolean; }; } diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts index d367e1616..7524d361c 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TCertificateAuthorityCrlDALFactory = ReturnType; +export type TCertificateAuthorityCrlDALFactory = TOrmify; -export const certificateAuthorityCrlDALFactory = (db: TDbClient) => { +export const certificateAuthorityCrlDALFactory = (db: TDbClient): TCertificateAuthorityCrlDALFactory => { const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl); return caCrlOrm; }; diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index 844bda8ba..5ead798fa 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -3,7 +3,7 @@ import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; @@ -12,7 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types"; +import { TCertificateAuthorityCrlServiceFactory } from "./certificate-authority-crl-types"; type TCertificateAuthorityCrlServiceFactoryDep = { certificateAuthorityDAL: Pick; @@ -22,19 +22,17 @@ type TCertificateAuthorityCrlServiceFactoryDep = { permissionService: Pick; }; -export type TCertificateAuthorityCrlServiceFactory = ReturnType; - export const certificateAuthorityCrlServiceFactory = ({ certificateAuthorityDAL, certificateAuthorityCrlDAL, projectDAL, kmsService, permissionService // licenseService -}: TCertificateAuthorityCrlServiceFactoryDep) => { +}: TCertificateAuthorityCrlServiceFactoryDep): TCertificateAuthorityCrlServiceFactory => { /** * Return CRL with id [crlId] */ - const getCrlById = async (crlId: TGetCrlById) => { + const getCrlById: TCertificateAuthorityCrlServiceFactory["getCrlById"] = async (crlId) => { const caCrl = await certificateAuthorityCrlDAL.findById(crlId); if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` }); @@ -65,7 +63,13 @@ export const certificateAuthorityCrlServiceFactory = ({ /** * Returns a list of CRL ids for CA with id [caId] */ - const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => { + const getCaCrls: TCertificateAuthorityCrlServiceFactory["getCaCrls"] = async ({ + caId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts index 9b82727e9..2864bb3dd 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts @@ -5,3 +5,137 @@ export type TGetCrlById = string; export type TGetCaCrlsDTO = { caId: string; } & Omit; + +export type TCertificateAuthorityCrlServiceFactory = { + getCrlById: (crlId: TGetCrlById) => Promise<{ + ca: { + readonly requireTemplateForIssuance: boolean; + readonly internalCa: + | { + id: string; + parentCaId: string | null | undefined; + type: string; + friendlyName: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; + commonName: string; + dn: string; + serialNumber: string | null | undefined; + maxPathLength: number | null | undefined; + keyAlgorithm: string; + notBefore: string | undefined; + notAfter: string | undefined; + activeCaCertId: string | null | undefined; + } + | undefined; + readonly externalCa: + | { + id: string; + type: string; + configuration: unknown; + dnsAppConnectionId: string | null | undefined; + appConnectionId: string | null | undefined; + credentials: Buffer | null | undefined; + } + | undefined; + readonly name: string; + readonly status: string; + readonly id: string; + readonly createdAt: Date; + readonly updatedAt: Date; + readonly projectId: string; + readonly enableDirectIssuance: boolean; + readonly parentCaId: string | null | undefined; + readonly type: string; + readonly friendlyName: string; + readonly organization: string; + readonly ou: string; + readonly country: string; + readonly province: string; + readonly locality: string; + readonly commonName: string; + readonly dn: string; + readonly serialNumber: string | null | undefined; + readonly maxPathLength: number | null | undefined; + readonly keyAlgorithm: string; + readonly notBefore: string | undefined; + readonly notAfter: string | undefined; + readonly activeCaCertId: string | null | undefined; + }; + caCrl: { + id: string; + createdAt: Date; + updatedAt: Date; + caId: string; + caSecretId: string; + encryptedCrl: Buffer; + }; + crl: ArrayBuffer; + }>; + getCaCrls: ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => Promise<{ + ca: { + readonly requireTemplateForIssuance: boolean; + readonly internalCa: + | { + id: string; + parentCaId: string | null | undefined; + type: string; + friendlyName: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; + commonName: string; + dn: string; + serialNumber: string | null | undefined; + maxPathLength: number | null | undefined; + keyAlgorithm: string; + notBefore: string | undefined; + notAfter: string | undefined; + activeCaCertId: string | null | undefined; + } + | undefined; + readonly externalCa: + | { + id: string; + type: string; + configuration: unknown; + dnsAppConnectionId: string | null | undefined; + appConnectionId: string | null | undefined; + credentials: Buffer | null | undefined; + } + | undefined; + readonly name: string; + readonly status: string; + readonly id: string; + readonly createdAt: Date; + readonly updatedAt: Date; + readonly projectId: string; + readonly enableDirectIssuance: boolean; + readonly parentCaId: string | null | undefined; + readonly type: string; + readonly friendlyName: string; + readonly organization: string; + readonly ou: string; + readonly country: string; + readonly province: string; + readonly locality: string; + readonly commonName: string; + readonly dn: string; + readonly serialNumber: string | null | undefined; + readonly maxPathLength: number | null | undefined; + readonly keyAlgorithm: string; + readonly notBefore: string | undefined; + readonly notAfter: string | undefined; + readonly activeCaCertId: string | null | undefined; + }; + crls: { + id: string; + crl: string; + }[]; + }>; +}; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 106f72334..168b16c5f 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -3,7 +3,7 @@ import RE2 from "re2"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index b502bf9f3..5a7da6a3e 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index 4d7b1a5b5..d515c5973 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -11,7 +11,7 @@ import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TExternalKmsDALFactory } from "./external-kms-dal"; import { TCreateExternalKmsDTO, diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 25f0b384a..ffef3e007 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -21,7 +21,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TGatewayDALFactory } from "./gateway-dal"; import { TExchangeAllocatedRelayAddressDTO, diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index 867feb4a4..37d1bd398 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -14,7 +14,7 @@ import { TGroupDALFactory } from "../group/group-dal"; import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal"; import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types"; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index 801f52fc0..708fbdbd3 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => { } }; + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await (tx || db.replicaNode())(TableName.Groups) + .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .where(`${TableName.Groups}.id`, id) + .select( + selectAllTableCols(TableName.Groups), + db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles) + ) + .first(); + + return doc; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + return { + ...groupOrm, findGroups, findByOrgId, findAllGroupPossibleMembers, findGroupsByProjectId, - ...groupOrm + findById }; }; diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index cc3125918..55665c146 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -15,7 +15,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TGroupDALFactory } from "./group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; import { diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index bf75ce5cd..64da588f8 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -11,7 +11,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident import { TProjectDALFactory } from "@app/services/project/project-dal"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; import { diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index cbfcc4670..828cf43a3 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -11,7 +11,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident import { TProjectDALFactory } from "@app/services/project/project-dal"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionSet, diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 45f201498..55d8c2b42 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -7,7 +7,7 @@ import { KmsKeyUsage } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TKmipClientDALFactory } from "./kmip-client-dal"; import { KmipPermission } from "./kmip-enum"; import { diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 82ff1a9aa..f8c52fe56 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -18,7 +18,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal"; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index c98873879..426ed132e 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -29,7 +29,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TLdapConfigDALFactory } from "./ldap-config-dal"; import { TCreateLdapCfgDTO, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 80e58815e..b841c5bea 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -18,7 +18,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionBillingActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums"; import { TLicenseDALFactory } from "./license-dal"; import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index d933835e4..5cbfbbc97 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -5,14 +5,13 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 7a17108a2..11ee29852 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -6,16 +6,312 @@ import { OrgMembershipRole, OrgMembershipsSchema, TableName, + TIdentityOrgMemberships, TProjectRoles, TProjects } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; -export type TPermissionDALFactory = ReturnType; +export interface TPermissionDALFactory { + getOrgPermission: ( + userId: string, + orgId: string + ) => Promise< + { + status: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + role: string; + isActive: boolean; + shouldUseNewPrivilegeSystem: boolean; + bypassOrgAuthEnabled: boolean; + permissions?: unknown; + userId?: string | null | undefined; + roleId?: string | null | undefined; + inviteEmail?: string | null | undefined; + projectFavorites?: string[] | null | undefined; + customRoleSlug?: string | null | undefined; + orgAuthEnforced?: boolean | null | undefined; + } & { + groups: { + id: string; + updatedAt: Date; + createdAt: Date; + role: string; + roleId: string | null | undefined; + customRolePermission: unknown; + name: string; + slug: string; + orgId: string; + }[]; + } + >; + getOrgIdentityPermission: ( + identityId: string, + orgId: string + ) => Promise< + | (TIdentityOrgMemberships & { + orgAuthEnforced: boolean | null | undefined; + shouldUseNewPrivilegeSystem: boolean; + permissions?: unknown; + }) + | undefined + >; + getProjectPermission: ( + userId: string, + projectId: string + ) => Promise< + | { + roles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + additionalPrivileges: { + id: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + orgId: string; + orgAuthEnforced: boolean | null | undefined; + orgRole: OrgMembershipRole; + userId: string; + projectId: string; + username: string; + projectType: string; + id: string; + createdAt: Date; + updatedAt: Date; + shouldUseNewPrivilegeSystem: boolean; + bypassOrgAuthEnabled: boolean; + metadata: { + id: string; + key: string; + value: string; + }[]; + userGroupRoles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + projecMembershiptRoles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + } + | undefined + >; + getProjectIdentityPermission: ( + identityId: string, + projectId: string + ) => Promise< + | { + roles: { + id: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + role: string; + projectMembershipId: string; + temporaryRange?: string | null | undefined; + permissions?: unknown; + customRoleId?: string | null | undefined; + temporaryMode?: string | null | undefined; + temporaryAccessStartTime?: Date | null | undefined; + temporaryAccessEndTime?: Date | null | undefined; + customRoleSlug?: string | null | undefined; + }[]; + additionalPrivileges: { + id: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + isTemporary: boolean; + }[]; + id: string; + identityId: string; + username: string; + projectId: string; + createdAt: Date; + updatedAt: Date; + orgId: string; + projectType: string; + shouldUseNewPrivilegeSystem: boolean; + orgAuthEnforced: boolean; + metadata: { + id: string; + key: string; + value: string; + }[]; + } + | undefined + >; + getProjectUserPermissions: (projectId: string) => Promise< + { + roles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + additionalPrivileges: { + id: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + orgId: string; + orgAuthEnforced: boolean | null | undefined; + userId: string; + projectId: string; + username: string; + projectType: string; + id: string; + createdAt: Date; + updatedAt: Date; + metadata: { + id: string; + key: string; + value: string; + }[]; + userGroupRoles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + projectMembershipRoles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + }[] + >; + getProjectIdentityPermissions: (projectId: string) => Promise< + { + roles: { + id: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + role: string; + projectMembershipId: string; + temporaryRange?: string | null | undefined; + permissions?: unknown; + customRoleId?: string | null | undefined; + temporaryMode?: string | null | undefined; + temporaryAccessStartTime?: Date | null | undefined; + temporaryAccessEndTime?: Date | null | undefined; + customRoleSlug?: string | null | undefined; + }[]; + additionalPrivileges: { + id: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + isTemporary: boolean; + }[]; + id: string; + identityId: string; + username: string; + projectId: string; + createdAt: Date; + updatedAt: Date; + orgId: string; + projectType: string; + orgAuthEnforced: boolean; + metadata: { + id: string; + key: string; + value: string; + }[]; + }[] + >; + getProjectGroupPermissions: ( + projectId: string, + filterGroupId?: string + ) => Promise< + { + roles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + groupId: string; + username: string; + id: string; + groupRoles: { + id: string; + role: string; + customRoleSlug: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + isTemporary: boolean; + }[]; + }[] + >; +} -export const permissionDALFactory = (db: TDbClient) => { - const getOrgPermission = async (userId: string, orgId: string) => { +export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { + const getOrgPermission: TPermissionDALFactory["getOrgPermission"] = async (userId: string, orgId: string) => { try { const groupSubQuery = db(TableName.Groups) .where(`${TableName.Groups}.orgId`, orgId) @@ -112,7 +408,10 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getOrgIdentityPermission = async (identityId: string, orgId: string) => { + const getOrgIdentityPermission: TPermissionDALFactory["getOrgIdentityPermission"] = async ( + identityId: string, + orgId: string + ) => { try { const membership = await db .replicaNode()(TableName.IdentityOrgMembership) @@ -132,7 +431,10 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectGroupPermissions = async (projectId: string, filterGroupId?: string) => { + const getProjectGroupPermissions: TPermissionDALFactory["getProjectGroupPermissions"] = async ( + projectId: string, + filterGroupId?: string + ) => { try { const docs = await db .replicaNode()(TableName.GroupProjectMembership) @@ -245,7 +547,7 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectUserPermissions = async (projectId: string) => { + const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async (projectId: string) => { try { const docs = await db .replicaNode()(TableName.Users) @@ -535,7 +837,10 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectPermission = async (userId: string, projectId: string) => { + const getProjectPermission: TPermissionDALFactory["getProjectPermission"] = async ( + userId: string, + projectId: string + ) => { try { const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); const docs = await db @@ -838,7 +1143,9 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectIdentityPermissions = async (projectId: string) => { + const getProjectIdentityPermissions: TPermissionDALFactory["getProjectIdentityPermissions"] = async ( + projectId: string + ) => { try { const docs = await db .replicaNode()(TableName.IdentityProjectMembership) @@ -995,7 +1302,10 @@ export const permissionDALFactory = (db: TDbClient) => { } }; - const getProjectIdentityPermission = async (identityId: string, projectId: string) => { + const getProjectIdentityPermission: TPermissionDALFactory["getProjectIdentityPermission"] = async ( + identityId, + projectId + ) => { try { const docs = await db .replicaNode()(TableName.IdentityProjectMembership) diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 570e2b6b1..5e71c65d9 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,6 +1,12 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { MongoQuery } from "@ucast/mongo2js"; + import { ActionProjectType } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; +import { OrgPermissionSet } from "./org-permission"; +import { ProjectPermissionSet } from "./project-permission"; + export type TBuildProjectPermissionDTO = { permissions?: unknown; role: string; @@ -41,3 +47,240 @@ export type TGetProjectPermissionArg = { actorOrgId?: string; actionProjectType: ActionProjectType; }; + +export type TPermissionServiceFactory = { + getUserOrgPermission: ( + userId: string, + orgId: string, + authMethod: ActorAuthMethod, + userOrgId?: string + ) => Promise<{ + permission: MongoAbility; + membership: { + status: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + role: string; + isActive: boolean; + shouldUseNewPrivilegeSystem: boolean; + bypassOrgAuthEnabled: boolean; + permissions?: unknown; + userId?: string | null | undefined; + roleId?: string | null | undefined; + inviteEmail?: string | null | undefined; + projectFavorites?: string[] | null | undefined; + customRoleSlug?: string | null | undefined; + orgAuthEnforced?: boolean | null | undefined; + } & { + groups: { + id: string; + updatedAt: Date; + createdAt: Date; + role: string; + roleId: string | null | undefined; + customRolePermission: unknown; + name: string; + slug: string; + orgId: string; + }[]; + }; + }>; + getOrgPermission: ( + type: ActorType, + id: string, + orgId: string, + authMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => Promise< + | { + permission: MongoAbility; + membership: { + status: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + role: string; + isActive: boolean; + shouldUseNewPrivilegeSystem: boolean; + bypassOrgAuthEnabled: boolean; + permissions?: unknown; + userId?: string | null | undefined; + roleId?: string | null | undefined; + inviteEmail?: string | null | undefined; + projectFavorites?: string[] | null | undefined; + customRoleSlug?: string | null | undefined; + orgAuthEnforced?: boolean | null | undefined; + } & { + groups: { + id: string; + updatedAt: Date; + createdAt: Date; + role: string; + roleId: string | null | undefined; + customRolePermission: unknown; + name: string; + slug: string; + orgId: string; + }[]; + }; + } + | { + permission: MongoAbility; + membership: { + id: string; + role: string; + createdAt: Date; + updatedAt: Date; + orgId: string; + roleId?: string | null | undefined; + permissions?: unknown; + identityId: string; + orgAuthEnforced: boolean | null | undefined; + shouldUseNewPrivilegeSystem: boolean; + }; + } + >; + getUserProjectPermission: ({ + userId, + projectId, + authMethod, + userOrgId, + actionProjectType + }: TGetUserProjectPermissionArg) => Promise<{ + permission: MongoAbility; + membership: { + id: string; + createdAt: Date; + updatedAt: Date; + userId: string; + projectId: string; + } & { + orgAuthEnforced: boolean | null | undefined; + orgId: string; + roles: Array<{ + role: string; + }>; + shouldUseNewPrivilegeSystem: boolean; + }; + hasRole: (role: string) => boolean; + }>; + getProjectPermission: ( + arg: TGetProjectPermissionArg + ) => Promise< + T extends ActorType.SERVICE + ? { + permission: MongoAbility; + membership: { + shouldUseNewPrivilegeSystem: boolean; + }; + hasRole: (arg: string) => boolean; + } + : { + permission: MongoAbility; + membership: (T extends ActorType.USER + ? { + id: string; + createdAt: Date; + updatedAt: Date; + userId: string; + projectId: string; + } + : { + id: string; + createdAt: Date; + updatedAt: Date; + projectId: string; + identityId: string; + }) & { + orgAuthEnforced: boolean | null | undefined; + orgId: string; + roles: Array<{ + role: string; + }>; + shouldUseNewPrivilegeSystem: boolean; + }; + hasRole: (role: string) => boolean; + } + >; + getProjectPermissions: (projectId: string) => Promise<{ + userPermissions: { + permission: MongoAbility; + id: string; + name: string; + membershipId: string; + }[]; + identityPermissions: { + permission: MongoAbility; + id: string; + name: string; + membershipId: string; + }[]; + groupPermissions: { + permission: MongoAbility; + id: string; + name: string; + membershipId: string; + }[]; + }>; + getOrgPermissionByRole: ( + role: string, + orgId: string + ) => Promise< + | { + permission: MongoAbility; + role: { + name: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + } + | { + permission: MongoAbility; + role?: undefined; + } + >; + getProjectPermissionByRole: ( + role: string, + projectId: string + ) => Promise< + | { + permission: MongoAbility; + role: { + name: string; + version: number; + id: string; + createdAt: Date; + updatedAt: Date; + projectId: string; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + } + | { + permission: MongoAbility; + role?: undefined; + } + >; + buildOrgPermission: (orgUserRoles: TBuildOrgPermissionDTO) => MongoAbility; + buildProjectPermissionRules: ( + projectUserRoles: TBuildProjectPermissionDTO + ) => RawRuleOf>[]; + checkGroupProjectPermission: ({ + groupId, + projectId, + checkPermissions + }: { + groupId: string; + projectId: string; + checkPermissions: ProjectPermissionSet; + }) => Promise; +}; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index a1acaeb21..85ee82cca 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -23,7 +23,7 @@ import { import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; -import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; +import { ActorType } from "@app/services/auth/auth-type"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; @@ -38,7 +38,8 @@ import { TGetIdentityProjectPermissionArg, TGetProjectPermissionArg, TGetServiceTokenProjectPermissionArg, - TGetUserProjectPermissionArg + TGetUserProjectPermissionArg, + TPermissionServiceFactory } from "./permission-service-types"; import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; @@ -50,15 +51,13 @@ type TPermissionServiceFactoryDep = { permissionDAL: TPermissionDALFactory; }; -export type TPermissionServiceFactory = ReturnType; - export const permissionServiceFactory = ({ permissionDAL, orgRoleDAL, projectRoleDAL, serviceTokenDAL, projectDAL -}: TPermissionServiceFactoryDep) => { +}: TPermissionServiceFactoryDep): TPermissionServiceFactory => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const rules = orgUserRoles .map(({ role, permissions }) => { @@ -120,11 +119,11 @@ export const permissionServiceFactory = ({ /* * Get user permission in an organization */ - const getUserOrgPermission = async ( - userId: string, - orgId: string, - authMethod: ActorAuthMethod, - userOrgId?: string + const getUserOrgPermission: TPermissionServiceFactory["getUserOrgPermission"] = async ( + userId, + orgId, + authMethod, + userOrgId ) => { // when token is scoped, ensure the passed org id is same as user org id if (userOrgId && userOrgId !== orgId) @@ -172,12 +171,12 @@ export const permissionServiceFactory = ({ }; }; - const getOrgPermission = async ( - type: ActorType, - id: string, - orgId: string, - authMethod: ActorAuthMethod, - actorOrgId: string | undefined + const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( + type, + id, + orgId, + authMethod, + actorOrgId ) => { switch (type) { case ActorType.USER: @@ -194,7 +193,7 @@ export const permissionServiceFactory = ({ // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like // admin member or user defined ones like biller etc - const getOrgPermissionByRole = async (role: string, orgId: string) => { + const getOrgPermissionByRole: TPermissionServiceFactory["getOrgPermissionByRole"] = async (role, orgId) => { const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); if (isCustomRole) { const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); @@ -437,7 +436,7 @@ export const permissionServiceFactory = ({ hasRole: (role: string) => boolean; }; - const getProjectPermissions = async (projectId: string) => { + const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId) => { // fetch user permissions const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { @@ -607,7 +606,10 @@ export const permissionServiceFactory = ({ } }; - const getProjectPermissionByRole = async (role: string, projectId: string) => { + const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( + role, + projectId + ) => { const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); if (isCustomRole) { const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); @@ -630,14 +632,10 @@ export const permissionServiceFactory = ({ return { permission }; }; - const checkGroupProjectPermission = async ({ + const checkGroupProjectPermission: TPermissionServiceFactory["checkGroupProjectPermission"] = async ({ groupId, projectId, checkPermissions - }: { - groupId: string; - projectId: string; - checkPermissions: ProjectPermissionSet; }) => { const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId); const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => { diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index f61c4b1a4..966146c27 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = { secretPath: string; }; +export type SecretSyncSubjectFields = { + environment: string; + secretPath: string; +}; + export type DynamicSecretSubjectFields = { environment: string; secretPath: string; @@ -267,6 +272,10 @@ export type ProjectPermissionSet = | (ForcedSubject & DynamicSecretSubjectFields) ) ] + | [ + ProjectPermissionSecretSyncActions, + ProjectPermissionSub.SecretSyncs | (ForcedSubject & SecretSyncSubjectFields) + ] | [ ProjectPermissionActions, ( @@ -323,7 +332,6 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] - | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] @@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z }) .partial(); +const SecretSyncConditionV2Schema = z + .object({ + environment: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB] + }) + .partial() + ]), + secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA + }) + .partial(); + const SecretImportConditionSchema = z .object({ environment: z.union([ @@ -671,12 +696,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( @@ -836,6 +855,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "When specified, only matching conditions will be allowed to access given resource." ).optional() }), + z.object({ + subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe( + "Describe what action an entity can take." + ), + conditions: SecretSyncConditionV2Schema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), ...GeneralPermissionSchema ]); diff --git a/backend/src/ee/services/pit/pit-service.ts b/backend/src/ee/services/pit/pit-service.ts index 160729123..0eb223fb4 100644 --- a/backend/src/ee/services/pit/pit-service.ts +++ b/backend/src/ee/services/pit/pit-service.ts @@ -16,7 +16,7 @@ import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; type TPitServiceFactoryDep = { folderCommitService: TFolderCommitServiceFactory; diff --git a/backend/src/ee/services/project-template/project-template-dal.ts b/backend/src/ee/services/project-template/project-template-dal.ts index 4dbbea279..b3663d6f4 100644 --- a/backend/src/ee/services/project-template/project-template-dal.ts +++ b/backend/src/ee/services/project-template/project-template-dal.ts @@ -1,7 +1,8 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TProjectTemplateDALFactory = ReturnType; +export type TProjectTemplateDALFactory = TOrmify; -export const projectTemplateDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectTemplates); +export const projectTemplateDALFactory = (db: TDbClient): TProjectTemplateDALFactory => + ormify(db, TableName.ProjectTemplates); diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index 5b6163977..9d585fc9a 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -4,18 +4,16 @@ import { packRules } from "@casl/ability/extra"; import { ProjectType, TProjectTemplates } 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { - TCreateProjectTemplateDTO, TProjectTemplateEnvironment, TProjectTemplateRole, - TUnpackedPermission, - TUpdateProjectTemplateDTO + TProjectTemplateServiceFactory, + TUnpackedPermission } from "@app/ee/services/project-template/project-template-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { OrgServiceActor } from "@app/lib/types"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; @@ -27,8 +25,6 @@ type TProjectTemplatesServiceFactoryDep = { projectTemplateDAL: TProjectTemplateDALFactory; }; -export type TProjectTemplateServiceFactory = ReturnType; - const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({ ...rest, environments: environments as TProjectTemplateEnvironment[], @@ -51,8 +47,11 @@ export const projectTemplateServiceFactory = ({ licenseService, permissionService, projectTemplateDAL -}: TProjectTemplatesServiceFactoryDep) => { - const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => { +}: TProjectTemplatesServiceFactoryDep): TProjectTemplateServiceFactory => { + const listProjectTemplatesByOrg: TProjectTemplateServiceFactory["listProjectTemplatesByOrg"] = async ( + actor, + type + ) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -83,7 +82,10 @@ export const projectTemplateServiceFactory = ({ ]; }; - const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => { + const findProjectTemplateByName: TProjectTemplateServiceFactory["findProjectTemplateByName"] = async ( + name, + actor + ) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -111,7 +113,7 @@ export const projectTemplateServiceFactory = ({ }; }; - const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => { + const findProjectTemplateById: TProjectTemplateServiceFactory["findProjectTemplateById"] = async (id, actor) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -139,9 +141,9 @@ export const projectTemplateServiceFactory = ({ }; }; - const createProjectTemplate = async ( - { roles, environments, type, ...params }: TCreateProjectTemplateDTO, - actor: OrgServiceActor + const createProjectTemplate: TProjectTemplateServiceFactory["createProjectTemplate"] = async ( + { roles, environments, type, ...params }, + actor ) => { const plan = await licenseService.getPlan(actor.orgId); @@ -195,10 +197,10 @@ export const projectTemplateServiceFactory = ({ return $unpackProjectTemplate(projectTemplate); }; - const updateProjectTemplateById = async ( - id: string, - { roles, environments, ...params }: TUpdateProjectTemplateDTO, - actor: OrgServiceActor + const updateProjectTemplateById: TProjectTemplateServiceFactory["updateProjectTemplateById"] = async ( + id, + { roles, environments, ...params }, + actor ) => { const plan = await licenseService.getPlan(actor.orgId); @@ -259,7 +261,7 @@ export const projectTemplateServiceFactory = ({ return $unpackProjectTemplate(updatedProjectTemplate); }; - const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => { + const deleteProjectTemplateById: TProjectTemplateServiceFactory["deleteProjectTemplateById"] = async (id, actor) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index d53b2375e..2684e10e5 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -1,7 +1,8 @@ import { z } from "zod"; -import { ProjectType, TProjectEnvironments } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; +import { OrgServiceActor } from "@app/lib/types"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; @@ -27,3 +28,177 @@ export type TUnpackedPermission = z.infer; export enum InfisicalProjectTemplate { Default = "default" } + +export type TProjectTemplateServiceFactory = { + listProjectTemplatesByOrg: ( + actor: OrgServiceActor, + type?: ProjectType + ) => Promise< + ( + | { + id: string; + type: ProjectType; + name: InfisicalProjectTemplate; + createdAt: Date; + updatedAt: Date; + description: string; + environments: + | { + name: string; + slug: string; + position: number; + }[] + | null; + roles: { + name: string; + slug: ProjectMembershipRole; + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + }[]; + orgId: string; + } + | { + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + } + )[] + >; + createProjectTemplate: ( + arg: TCreateProjectTemplateDTO, + actor: OrgServiceActor + ) => Promise<{ + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + }>; + updateProjectTemplateById: ( + id: string, + { roles, environments, ...params }: TUpdateProjectTemplateDTO, + actor: OrgServiceActor + ) => Promise<{ + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + }>; + deleteProjectTemplateById: ( + id: string, + actor: OrgServiceActor + ) => Promise<{ + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + }>; + findProjectTemplateById: ( + id: string, + actor: OrgServiceActor + ) => Promise<{ + packedRoles: TProjectTemplateRole[]; + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + }>; + findProjectTemplateByName: ( + name: string, + actor: OrgServiceActor + ) => Promise<{ + packedRoles: TProjectTemplateRole[]; + environments: TProjectTemplateEnvironment[]; + roles: { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; + slug: string; + name: string; + }[]; + name: string; + type: string; + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description?: string | null | undefined; + }>; +}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts index 6c15d2d5d..6a3be2631 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TProjectUserAdditionalPrivilegeDALFactory = ReturnType; +export type TProjectUserAdditionalPrivilegeDALFactory = TOrmify; -export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => { +export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient): TProjectUserAdditionalPrivilegeDALFactory => { const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege); return orm; }; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index f4d7f4e6a..944775156 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -11,7 +11,7 @@ import { TProjectMembershipDALFactory } from "@app/services/project-membership/p import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSet, @@ -21,11 +21,7 @@ import { ApprovalStatus } from "../secret-approval-request/secret-approval-reque import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, - TCreateUserPrivilegeDTO, - TDeleteUserPrivilegeDTO, - TGetUserPrivilegeDetailsDTO, - TListUserPrivilegesDTO, - TUpdateUserPrivilegeDTO + TProjectUserAdditionalPrivilegeServiceFactory } from "./project-user-additional-privilege-types"; type TProjectUserAdditionalPrivilegeServiceFactoryDep = { @@ -35,10 +31,6 @@ type TProjectUserAdditionalPrivilegeServiceFactoryDep = { accessApprovalRequestDAL: Pick; }; -export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< - typeof projectUserAdditionalPrivilegeServiceFactory ->; - const unpackPermissions = (permissions: unknown) => UnpackedPermissionSchema.array().parse( unpackRules((permissions || []) as PackRule>>[]) @@ -49,8 +41,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ projectMembershipDAL, permissionService, accessApprovalRequestDAL -}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => { - const create = async ({ +}: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => { + const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({ slug, actor, actorId, @@ -59,7 +51,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, projectMembershipId, ...dto - }: TCreateUserPrivilegeDTO) => { + }) => { const projectMembership = await projectMembershipDAL.findById(projectMembershipId); if (!projectMembership) throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); @@ -147,14 +139,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ }; }; - const updateById = async ({ + const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({ privilegeId, actorOrgId, actor, actorId, actorAuthMethod, ...dto - }: TUpdateUserPrivilegeDTO) => { + }) => { const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); if (!userPrivilege) throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); @@ -259,7 +251,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ }; }; - const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { + const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + privilegeId + }) => { const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); if (!userPrivilege) throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); @@ -299,13 +297,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ }; }; - const getPrivilegeDetailsById = async ({ + const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({ privilegeId, actorOrgId, actor, actorId, actorAuthMethod - }: TGetUserPrivilegeDetailsDTO) => { + }) => { const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); if (!userPrivilege) throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); @@ -335,13 +333,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ }; }; - const listPrivileges = async ({ + const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({ projectMembershipId, actorOrgId, actor, actorId, actorAuthMethod - }: TListUserPrivilegesDTO) => { + }) => { const projectMembership = await projectMembershipDAL.findById(projectMembershipId); if (!projectMembership) throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts index cfcf75872..a700d997d 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts @@ -1,3 +1,4 @@ +import { TProjectUserAdditionalPrivilege } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { TProjectPermissionV2Schema } from "../permission/project-permission"; @@ -40,3 +41,20 @@ export type TDeleteUserPrivilegeDTO = Omit & { export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string }; export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string }; + +interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege { + permissions: { + action: string[]; + subject?: string | undefined; + conditions?: unknown; + inverted?: boolean | undefined; + }[]; +} + +export type TProjectUserAdditionalPrivilegeServiceFactory = { + create: (arg: TCreateUserPrivilegeDTO) => Promise; + updateById: (arg: TUpdateUserPrivilegeDTO) => Promise; + deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise; + getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise; + listPrivileges: (arg: TListUserPrivilegesDTO) => Promise; +}; diff --git a/backend/src/ee/services/rate-limit/rate-limit-dal.ts b/backend/src/ee/services/rate-limit/rate-limit-dal.ts index 7279ff8ea..36b717cc4 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-dal.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-dal.ts @@ -1,7 +1,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TRateLimitDALFactory = ReturnType; +export type TRateLimitDALFactory = TOrmify; -export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {}); +export const rateLimitDALFactory = (db: TDbClient): TRateLimitDALFactory => ormify(db, TableName.RateLimit, {}); diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts index 61b18be91..730bb3b30 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-service.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts @@ -4,7 +4,7 @@ import { logger } from "@app/lib/logger"; import { TLicenseServiceFactory } from "../license/license-service"; import { TRateLimitDALFactory } from "./rate-limit-dal"; -import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; +import { RateLimitConfiguration, TRateLimit, TRateLimitServiceFactory } from "./rate-limit-types"; let rateLimitMaxConfiguration: RateLimitConfiguration = { readLimit: 60, @@ -27,12 +27,13 @@ type TRateLimitServiceFactoryDep = { licenseService: Pick; }; -export type TRateLimitServiceFactory = ReturnType; - -export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateLimitServiceFactoryDep) => { +export const rateLimitServiceFactory = ({ + rateLimitDAL, + licenseService +}: TRateLimitServiceFactoryDep): TRateLimitServiceFactory => { const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000"; - const getRateLimits = async (): Promise => { + const getRateLimits: TRateLimitServiceFactory["getRateLimits"] = async () => { let rateLimit: TRateLimit; try { @@ -51,11 +52,11 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL } }; - const updateRateLimit = async (updates: TRateLimitUpdateDTO): Promise => { + const updateRateLimit: TRateLimitServiceFactory["updateRateLimit"] = async (updates) => { return rateLimitDAL.updateById(DEFAULT_RATE_LIMIT_CONFIG_ID, updates); }; - const syncRateLimitConfiguration = async () => { + const syncRateLimitConfiguration: TRateLimitServiceFactory["syncRateLimitConfiguration"] = async () => { try { const rateLimit = await getRateLimits(); if (rateLimit) { @@ -78,7 +79,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL } }; - const initializeBackgroundSync = async () => { + const initializeBackgroundSync: TRateLimitServiceFactory["initializeBackgroundSync"] = async () => { if (!licenseService.onPremFeatures.customRateLimits) { logger.info("Current license does not support custom rate limit configuration"); return; diff --git a/backend/src/ee/services/rate-limit/rate-limit-types.ts b/backend/src/ee/services/rate-limit/rate-limit-types.ts index d924dce51..0227afe08 100644 --- a/backend/src/ee/services/rate-limit/rate-limit-types.ts +++ b/backend/src/ee/services/rate-limit/rate-limit-types.ts @@ -1,3 +1,5 @@ +import { CronJob } from "cron"; + export type TRateLimitUpdateDTO = { readRateLimit: number; writeRateLimit: number; @@ -23,3 +25,10 @@ export type RateLimitConfiguration = { inviteUserRateLimit: number; mfaRateLimit: number; }; + +export type TRateLimitServiceFactory = { + getRateLimits: () => Promise; + updateRateLimit: (updates: TRateLimitUpdateDTO) => Promise; + initializeBackgroundSync: () => Promise | undefined>; + syncRateLimitConfiguration: () => Promise; +}; diff --git a/backend/src/ee/services/saml-config/saml-config-dal.ts b/backend/src/ee/services/saml-config/saml-config-dal.ts index c82adcb89..917b5c85c 100644 --- a/backend/src/ee/services/saml-config/saml-config-dal.ts +++ b/backend/src/ee/services/saml-config/saml-config-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TSamlConfigDALFactory = ReturnType; +export type TSamlConfigDALFactory = TOrmify; -export const samlConfigDALFactory = (db: TDbClient) => { +export const samlConfigDALFactory = (db: TDbClient): TSamlConfigDALFactory => { const samlCfgOrm = ormify(db, TableName.SamlConfig); return samlCfgOrm; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 601347862..c81fd518b 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -23,9 +23,9 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TSamlConfigDALFactory } from "./saml-config-dal"; -import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types"; +import { TSamlConfigServiceFactory } from "./saml-config-types"; type TSamlConfigServiceFactoryDep = { samlConfigDAL: Pick; @@ -47,8 +47,6 @@ type TSamlConfigServiceFactoryDep = { kmsService: Pick; }; -export type TSamlConfigServiceFactory = ReturnType; - export const samlConfigServiceFactory = ({ samlConfigDAL, orgDAL, @@ -61,8 +59,8 @@ export const samlConfigServiceFactory = ({ smtpService, identityMetadataDAL, kmsService -}: TSamlConfigServiceFactoryDep) => { - const createSamlCfg = async ({ +}: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { + const createSamlCfg: TSamlConfigServiceFactory["createSamlCfg"] = async ({ idpCert, actor, actorAuthMethod, @@ -73,7 +71,7 @@ export const samlConfigServiceFactory = ({ isActive, entryPoint, authProvider - }: TCreateSamlCfgDTO) => { + }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); @@ -101,7 +99,7 @@ export const samlConfigServiceFactory = ({ return samlConfig; }; - const updateSamlCfg = async ({ + const updateSamlCfg: TSamlConfigServiceFactory["updateSamlCfg"] = async ({ orgId, actor, actorOrgId, @@ -112,7 +110,7 @@ export const samlConfigServiceFactory = ({ isActive, entryPoint, authProvider - }: TUpdateSamlCfgDTO) => { + }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); @@ -146,7 +144,7 @@ export const samlConfigServiceFactory = ({ return ssoConfig; }; - const getSaml = async (dto: TGetSamlCfgDTO) => { + const getSaml: TSamlConfigServiceFactory["getSaml"] = async (dto) => { let samlConfig: TSamlConfigs | undefined; if (dto.type === "org") { samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); @@ -221,7 +219,7 @@ export const samlConfigServiceFactory = ({ }; }; - const samlLogin = async ({ + const samlLogin: TSamlConfigServiceFactory["samlLogin"] = async ({ externalId, email, firstName, @@ -230,7 +228,7 @@ export const samlConfigServiceFactory = ({ orgId, relayState, metadata - }: TSamlLoginDTO) => { + }) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index 444839a21..a9bd8f485 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -1,3 +1,4 @@ +import { TSamlConfigs } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; @@ -56,3 +57,26 @@ export type TSamlLoginDTO = { relayState?: string; metadata?: { key: string; value: string }[]; }; + +export type TSamlConfigServiceFactory = { + createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise; + updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise; + getSaml: (arg: TGetSamlCfgDTO) => Promise< + | { + id: string; + organization: string; + orgId: string; + authProvider: string; + isActive: boolean; + entryPoint: string; + issuer: string; + cert: string; + lastUsed: Date | null | undefined; + } + | undefined + >; + samlLogin: (arg: TSamlLoginDTO) => Promise<{ + isUserCompleted: boolean; + providerAuthToken: string; + }>; +}; diff --git a/backend/src/ee/services/scim/scim-dal.ts b/backend/src/ee/services/scim/scim-dal.ts index 05c21b80c..77a19d4d2 100644 --- a/backend/src/ee/services/scim/scim-dal.ts +++ b/backend/src/ee/services/scim/scim-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TScimDALFactory = ReturnType; +export type TScimDALFactory = TOrmify; -export const scimDALFactory = (db: TDbClient) => { +export const scimDALFactory = (db: TDbClient): TScimDALFactory => { const scimTokenOrm = ormify(db, TableName.ScimToken); return scimTokenOrm; }; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 4aad13ab8..6c5465488 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -11,7 +11,6 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TOrgPermission } from "@app/lib/types"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -33,28 +32,10 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; -import { - TCreateScimGroupDTO, - TCreateScimTokenDTO, - TCreateScimUserDTO, - TDeleteScimGroupDTO, - TDeleteScimTokenDTO, - TDeleteScimUserDTO, - TGetScimGroupDTO, - TGetScimUserDTO, - TListScimGroupsDTO, - TListScimUsers, - TListScimUsersDTO, - TReplaceScimUserDTO, - TScimGroup, - TScimTokenJwtPayload, - TUpdateScimGroupNamePatchDTO, - TUpdateScimGroupNamePutDTO, - TUpdateScimUserDTO -} from "./scim-types"; +import { TScimGroup, TScimServiceFactory } from "./scim-types"; type TScimServiceFactoryDep = { scimDAL: Pick; @@ -111,8 +92,6 @@ type TScimServiceFactoryDep = { externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; }; -export type TScimServiceFactory = ReturnType; - export const scimServiceFactory = ({ licenseService, scimDAL, @@ -131,8 +110,8 @@ export const scimServiceFactory = ({ projectUserAdditionalPrivilegeDAL, smtpService, externalGroupOrgRoleMappingDAL -}: TScimServiceFactoryDep) => { - const createScimToken = async ({ +}: TScimServiceFactoryDep): TScimServiceFactory => { + const createScimToken: TScimServiceFactory["createScimToken"] = async ({ actor, actorId, actorOrgId, @@ -140,7 +119,7 @@ export const scimServiceFactory = ({ orgId, description, ttlDays - }: TCreateScimTokenDTO) => { + }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); @@ -169,7 +148,13 @@ export const scimServiceFactory = ({ return { scimToken }; }; - const listScimTokens = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TOrgPermission) => { + const listScimTokens: TScimServiceFactory["listScimTokens"] = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + orgId + }) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); @@ -183,7 +168,13 @@ export const scimServiceFactory = ({ return scimTokens; }; - const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => { + const deleteScimToken: TScimServiceFactory["deleteScimToken"] = async ({ + scimTokenId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }) => { let scimToken = await scimDAL.findById(scimTokenId); if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` }); @@ -208,12 +199,12 @@ export const scimServiceFactory = ({ }; // SCIM server endpoints - const listScimUsers = async ({ + const listScimUsers: TScimServiceFactory["listScimUsers"] = async ({ startIndex = 0, limit = 100, filter, orgId - }: TListScimUsersDTO): Promise => { + }) => { const org = await orgDAL.findById(orgId); if (!org.scimEnabled) @@ -250,7 +241,7 @@ export const scimServiceFactory = ({ }); }; - const getScimUser = async ({ orgMembershipId, orgId }: TGetScimUserDTO) => { + const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL .findMembership({ [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, @@ -287,7 +278,13 @@ export const scimServiceFactory = ({ }); }; - const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => { + const createScimUser: TScimServiceFactory["createScimUser"] = async ({ + externalId, + email, + firstName, + lastName, + orgId + }) => { if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 }); const org = await orgDAL.findOrgById(orgId); @@ -467,7 +464,7 @@ export const scimServiceFactory = ({ }; // partial - const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => { + const updateScimUser: TScimServiceFactory["updateScimUser"] = async ({ orgMembershipId, orgId, operations }) => { const org = await orgDAL.findOrgById(orgId); if (!org.orgAuthMethod) { throw new ScimRequestError({ @@ -540,7 +537,7 @@ export const scimServiceFactory = ({ return scimUser; }; - const replaceScimUser = async ({ + const replaceScimUser: TScimServiceFactory["replaceScimUser"] = async ({ orgMembershipId, active, orgId, @@ -548,7 +545,7 @@ export const scimServiceFactory = ({ firstName, email, externalId - }: TReplaceScimUserDTO) => { + }) => { const org = await orgDAL.findOrgById(orgId); if (!org.orgAuthMethod) { throw new ScimRequestError({ @@ -627,7 +624,7 @@ export const scimServiceFactory = ({ }); }; - const deleteScimUser = async ({ orgMembershipId, orgId }: TDeleteScimUserDTO) => { + const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL.findMembership({ [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId @@ -660,7 +657,13 @@ export const scimServiceFactory = ({ return {}; // intentionally return empty object upon success }; - const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => { + const listScimGroups: TScimServiceFactory["listScimGroups"] = async ({ + orgId, + startIndex, + limit, + filter, + isMembersExcluded + }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -768,7 +771,7 @@ export const scimServiceFactory = ({ ); }; - const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => { + const createScimGroup: TScimServiceFactory["createScimGroup"] = async ({ displayName, orgId, members }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -863,7 +866,7 @@ export const scimServiceFactory = ({ }); }; - const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => { + const getScimGroup: TScimServiceFactory["getScimGroup"] = async ({ groupId, orgId }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -1011,7 +1014,12 @@ export const scimServiceFactory = ({ return updatedGroup; }; - const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => { + const replaceScimGroup: TScimServiceFactory["replaceScimGroup"] = async ({ + groupId, + orgId, + displayName, + members + }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -1043,7 +1051,7 @@ export const scimServiceFactory = ({ }); }; - const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => { + const updateScimGroup: TScimServiceFactory["updateScimGroup"] = async ({ groupId, orgId, operations }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -1101,7 +1109,7 @@ export const scimServiceFactory = ({ }; }; - const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => { + const deleteScimGroup: TScimServiceFactory["deleteScimGroup"] = async ({ groupId, orgId }) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) throw new BadRequestError({ @@ -1137,7 +1145,7 @@ export const scimServiceFactory = ({ return {}; // intentionally return empty object upon success }; - const fnValidateScimToken = async (token: TScimTokenJwtPayload) => { + const fnValidateScimToken: TScimServiceFactory["fnValidateScimToken"] = async (token) => { const scimToken = await scimDAL.findById(token.scimTokenId); if (!scimToken) throw new UnauthorizedError(); diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts index 5099e4ca0..8bdea39e1 100644 --- a/backend/src/ee/services/scim/scim-types.ts +++ b/backend/src/ee/services/scim/scim-types.ts @@ -1,5 +1,6 @@ import { ScimPatchOperation } from "scim-patch"; +import { TScimTokens } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; export type TCreateScimTokenDTO = { @@ -156,3 +157,47 @@ export type TScimGroup = { lastModified: Date; }; }; + +export type TScimServiceFactory = { + createScimToken: (arg: TCreateScimTokenDTO) => Promise<{ + scimToken: string; + }>; + listScimTokens: (arg: TOrgPermission) => Promise; + deleteScimToken: (arg: TDeleteScimTokenDTO) => Promise<{ + orgId: string; + id: string; + createdAt: Date; + updatedAt: Date; + description: string; + ttlDays: number; + }>; + listScimUsers: (arg: TListScimUsersDTO) => Promise; + getScimUser: (arg: TGetScimUserDTO) => Promise; + createScimUser: (arg: TCreateScimUserDTO) => Promise; + updateScimUser: (arg: TUpdateScimUserDTO) => Promise; + replaceScimUser: (arg: TReplaceScimUserDTO) => Promise; + deleteScimUser: (arg: TDeleteScimUserDTO) => Promise; + listScimGroups: (arg: TListScimGroupsDTO) => Promise; + createScimGroup: (arg: TCreateScimGroupDTO) => Promise; + getScimGroup: (arg: TGetScimGroupDTO) => Promise; + deleteScimGroup: (arg: TDeleteScimGroupDTO) => Promise; + replaceScimGroup: (arg: TUpdateScimGroupNamePutDTO) => Promise; + updateScimGroup: (arg: TUpdateScimGroupNamePatchDTO) => Promise<{ + members: { + value: string; + display: string; + }[]; + schemas: string[]; + id: string; + displayName: string; + meta: { + resourceType: string; + created: Date; + lastModified: Date; + }; + }>; + fnValidateScimToken: (token: TScimTokenJwtPayload) => Promise<{ + scimTokenId: string; + orgId: string; + }>; +}; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 696caf311..c8df810ed 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import picomatch from "picomatch"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 9f1ee5307..e70d0af00 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -39,7 +39,8 @@ import { fnSecretBulkDelete, fnSecretBulkInsert, fnSecretBulkUpdate, - getAllNestedSecretReferences + getAllNestedSecretReferences, + INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns"; import { TSecretQueueFactory } from "@app/services/secret/secret-queue"; import { SecretOperations } from "@app/services/secret/secret-types"; @@ -63,7 +64,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; @@ -267,7 +268,6 @@ export const secretApprovalRequestServiceFactory = ({ ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets ); - const hiddenSecretValue = "******"; let secrets; if (shouldUseSecretV2Bridge) { @@ -285,8 +285,9 @@ export const secretApprovalRequestServiceFactory = ({ version: el.version, secretMetadata: el.secretMetadata as ResourceMetadataDTO, isRotatedSecret: el.secret?.isRotatedSecret ?? false, + secretValueHidden: !hasSecretReadAccess, secretValue: !hasSecretReadAccess - ? hiddenSecretValue + ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secret && el.secret.isRotatedSecret ? undefined : el.encryptedValue @@ -300,8 +301,9 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.secret.key, id: el.secret.id, version: el.secret.version, + secretValueHidden: !hasSecretReadAccess, secretValue: !hasSecretReadAccess - ? hiddenSecretValue + ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() : "", @@ -315,8 +317,9 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.secretVersion.key, id: el.secretVersion.id, version: el.secretVersion.version, + secretValueHidden: !hasSecretReadAccess, secretValue: !hasSecretReadAccess - ? hiddenSecretValue + ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secretVersion.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() : "", @@ -333,11 +336,13 @@ export const secretApprovalRequestServiceFactory = ({ const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); secrets = encryptedSecrets.map((el) => ({ ...el, + secretValueHidden: !hasSecretReadAccess, ...decryptSecretWithBot(el, botKey), secret: el.secret ? { id: el.secret.id, version: el.secret.version, + secretValueHidden: false, ...decryptSecretWithBot(el.secret, botKey) } : undefined, @@ -345,6 +350,7 @@ export const secretApprovalRequestServiceFactory = ({ ? { id: el.secretVersion.id, version: el.secretVersion.version, + secretValueHidden: false, ...decryptSecretWithBot(el.secretVersion, botKey) } : undefined @@ -353,6 +359,7 @@ export const secretApprovalRequestServiceFactory = ({ const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [ secretApprovalRequest.folderId ]); + return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets }; }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index 986cb0ee4..c1b5b8c25 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -3,11 +3,10 @@ import { Knex } from "knex"; import isEqual from "lodash.isequal"; import { ActionProjectType, SecretType, TableName } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 2364de79d..1099e17a7 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -14,7 +14,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts index 3747af81f..aa8519027 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts @@ -1,8 +1,7 @@ import { join } from "path"; import { ProjectMembershipRole, TSecretScanningFindings } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { createTempFolder, deleteTempFolder, diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts index 05449bd0d..34a981116 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-service.ts @@ -3,7 +3,7 @@ import { join } from "path"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSecretScanningConfigActions, ProjectPermissionSecretScanningDataSourceActions, diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 7d41091fc..1c12f8b4e 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -5,7 +5,7 @@ import { WebhookEventMap } from "@octokit/webhooks-types"; import { ProbotOctokit } from "probot"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 8cae3dfb5..c9f6dac9d 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -28,7 +28,7 @@ import { hasSecretReadValueOrDescribePermission, throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSecretActions, diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts index 6687efaf9..49d8c1ab6 100644 --- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index 1137660d6..aa6d4f66a 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal"; diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index e41a8b403..64abfebbc 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts index c0a780fbb..a8269ac37 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-types.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal"; import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal"; import { TProjectPermission } from "@app/lib/types"; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index d58644d90..6c35f0ddd 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-dal.ts b/backend/src/ee/services/trusted-ip/trusted-ip-dal.ts index e0c640be6..25998594a 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-dal.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-dal.ts @@ -1,10 +1,10 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { ormify, TOrmify } from "@app/lib/knex"; -export type TTrustedIpDALFactory = ReturnType; +export type TTrustedIpDALFactory = TOrmify; -export const trustedIpDALFactory = (db: TDbClient) => { +export const trustedIpDALFactory = (db: TDbClient): TTrustedIpDALFactory => { const trustedIpOrm = ormify(db, TableName.TrustedIps); return trustedIpOrm; }; diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts index c407bdc82..6b9686e25 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts @@ -3,14 +3,13 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TTrustedIpDALFactory } from "./trusted-ip-dal"; -import { TCreateIpDTO, TDeleteIpDTO, TUpdateIpDTO } from "./trusted-ip-types"; +import { TTrustedIpServiceFactory } from "./trusted-ip-types"; type TTrustedIpServiceFactoryDep = { trustedIpDAL: TTrustedIpDALFactory; @@ -19,15 +18,19 @@ type TTrustedIpServiceFactoryDep = { projectDAL: Pick; }; -export type TTrustedIpServiceFactory = ReturnType; - export const trustedIpServiceFactory = ({ trustedIpDAL, permissionService, licenseService, projectDAL -}: TTrustedIpServiceFactoryDep) => { - const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => { +}: TTrustedIpServiceFactoryDep): TTrustedIpServiceFactory => { + const listIpsByProjectId: TTrustedIpServiceFactory["listIpsByProjectId"] = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -43,7 +46,7 @@ export const trustedIpServiceFactory = ({ return trustedIps; }; - const addProjectIp = async ({ + const addProjectIp: TTrustedIpServiceFactory["addProjectIp"] = async ({ projectId, actorId, actorAuthMethod, @@ -52,7 +55,7 @@ export const trustedIpServiceFactory = ({ ipAddress: ip, comment, isActive - }: TCreateIpDTO) => { + }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -89,7 +92,7 @@ export const trustedIpServiceFactory = ({ return { trustedIp, project }; // for audit log }; - const updateProjectIp = async ({ + const updateProjectIp: TTrustedIpServiceFactory["updateProjectIp"] = async ({ projectId, actorId, actor, @@ -98,7 +101,7 @@ export const trustedIpServiceFactory = ({ ipAddress: ip, comment, trustedIpId - }: TUpdateIpDTO) => { + }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -137,14 +140,14 @@ export const trustedIpServiceFactory = ({ return { trustedIp, project }; // for audit log }; - const deleteProjectIp = async ({ + const deleteProjectIp: TTrustedIpServiceFactory["deleteProjectIp"] = async ({ projectId, actorId, actor, actorOrgId, actorAuthMethod, trustedIpId - }: TDeleteIpDTO) => { + }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-types.ts b/backend/src/ee/services/trusted-ip/trusted-ip-types.ts index abe066d65..2de94d22a 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-types.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-types.ts @@ -1,3 +1,4 @@ +import { TProjects, TTrustedIps } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TCreateIpDTO = TProjectPermission & { @@ -15,3 +16,19 @@ export type TUpdateIpDTO = TProjectPermission & { export type TDeleteIpDTO = TProjectPermission & { trustedIpId: string; }; + +export type TTrustedIpServiceFactory = { + listIpsByProjectId: (arg: TProjectPermission) => Promise; + addProjectIp: (arg: TCreateIpDTO) => Promise<{ + trustedIp: TTrustedIps; + project: TProjects; + }>; + updateProjectIp: (arg: TUpdateIpDTO) => Promise<{ + trustedIp: TTrustedIps; + project: TProjects; + }>; + deleteProjectIp: (arg: TDeleteIpDTO) => Promise<{ + trustedIp: TTrustedIps; + project: TProjects; + }>; +}; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index d3f19168a..6a63af776 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,7 +2,7 @@ import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; -import { Redlock, Settings } from "@app/lib/red-lock"; +import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock"; export const PgSqlLock = { BootUpMigration: 2023, @@ -14,8 +14,6 @@ export const PgSqlLock = { CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`) } as const; -export type TKeyStoreFactory = ReturnType; - // all the key prefixes used must be set here to avoid conflict export const KeyStorePrefixes = { SecretReplication: "secret-replication-import-lock", @@ -71,7 +69,28 @@ type TWaitTillReady = { jitter?: number; }; -export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys) => { +export type TKeyStoreFactory = { + setItem: (key: string, value: string | number | Buffer, prefix?: string) => Promise<"OK">; + getItem: (key: string, prefix?: string) => Promise; + setExpiry: (key: string, expiryInSeconds: number) => Promise; + setItemWithExpiry: ( + key: string, + expiryInSeconds: number | string, + value: string | number | Buffer, + prefix?: string + ) => Promise<"OK">; + deleteItem: (key: string) => Promise; + deleteItems: (arg: TDeleteItems) => Promise; + incrementBy: (key: string, value: number) => Promise; + acquireLock( + resources: string[], + duration: number, + settings?: Partial + ): Promise<{ release: () => Promise }>; + waitTillReady: ({ key, waitingCb, keyCheckCb, waitIteration, delay, jitter }: TWaitTillReady) => Promise; +}; + +export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFactory => { const redis = buildRedisFromConfig(redisConfigKeys); const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 }); @@ -108,7 +127,6 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys) => { // eslint-disable-next-line no-await-in-loop await pipeline.exec(); totalDeleted += batch.length; - console.log("BATCH DONE"); // eslint-disable-next-line no-await-in-loop await delayMs(Math.max(0, applyJitter(delay, jitter))); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 42167bce3..100fe50eb 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -111,12 +111,14 @@ export const IDENTITIES = { CREATE: { name: "The name of the identity to create.", organizationId: "The organization ID to which the identity belongs.", - role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'." + role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.", + hasDeleteProtection: "Prevents deletion of the identity when enabled." }, UPDATE: { identityId: "The ID of the identity to update.", name: "The new name of the identity.", - role: "The new role of the identity." + role: "The new role of the identity.", + hasDeleteProtection: "Prevents deletion of the identity when enabled." }, DELETE: { identityId: "The ID of the identity to delete." @@ -2223,6 +2225,9 @@ export const AppConnections = { ONEPASS: { instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.", apiToken: "The API token used to access the 1Password Connect Server." + }, + FLYIO: { + accessToken: "The Access Token used to access fly.io." } } }; @@ -2389,6 +2394,9 @@ export const SecretSyncs = { serviceId: "The ID of the Render service to sync secrets to.", scope: "The Render scope that secrets should be synced to.", type: "The type of Render resource to sync secrets to." + }, + FLYIO: { + appId: "The ID of the Fly.io app to sync secrets to." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6b9d33c2a..b0b35cc2f 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -262,8 +262,8 @@ const envSchema = z DATADOG_HOSTNAME: zpStr(z.string().optional()), // PIT - PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("2")), - PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("30")), + PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")), + PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("200")), /* CORS ----------------------------------------------------------------------------- */ CORS_ALLOWED_ORIGINS: zpStr( diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 5949afe33..090df561a 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -71,8 +71,8 @@ export const buildFindFilter = return bd; }; -export type TFindReturn = Array< - Awaited[0] & +export type TFindReturn = Array< + Tables[Tname]["base"] & (TCount extends true ? { count: string; @@ -94,40 +94,82 @@ export type TFindOpt< tx?: Knex; }; +export type TOrmify = { + transaction: (cb: (tx: Knex) => Promise) => Promise; + findById: (id: string, tx?: Knex) => Promise; + find: ( + filter: TFindFilter, + { offset, limit, sort, count, tx, countDistinct }?: TFindOpt + ) => Promise>; + findOne: (filter: Partial, tx?: Knex) => Promise; + create: (data: Tables[Tname]["insert"], tx?: Knex) => Promise; + insertMany: (data: readonly Tables[Tname]["insert"][], tx?: Knex) => Promise; + batchInsert: (data: readonly Tables[Tname]["insert"][], tx?: Knex) => Promise; + upsert: ( + data: readonly Tables[Tname]["insert"][], + onConflictField: keyof Tables[Tname]["base"] | Array, + tx?: Knex, + mergeColumns?: (keyof Knex.ResolveTableType, "update">)[] | undefined + ) => Promise; + updateById: ( + id: string, + { + $incr, + $decr, + ...data + }: Tables[Tname]["update"] & { + $incr?: { [x in keyof Partial]: number }; + $decr?: { [x in keyof Partial]: number }; + }, + tx?: Knex + ) => Promise; + update: ( + filter: TFindFilter, + { + $incr, + $decr, + ...data + }: Tables[Tname]["update"] & { + $incr?: { [x in keyof Partial]: number }; + $decr?: { [x in keyof Partial]: number }; + }, + tx?: Knex + ) => Promise; + deleteById: (id: string, tx?: Knex) => Promise; + countDocuments: (tx?: Knex) => Promise; + delete: (filter: TFindFilter, tx?: Knex) => Promise; +}; + // What is ormify // It is to inject typical operations like find, findOne, update, delete, create // This will avoid writing most common ones each time -export const ormify = (db: Knex, tableName: Tname, dal?: DbOps) => ({ +export const ormify = ( + db: Knex, + tableName: Tname, + dal?: DbOps +): TOrmify => ({ transaction: async (cb: (tx: Knex) => Promise) => db.transaction(async (trx) => { const res = await cb(trx); return res; }), - findById: async (id: string, tx?: Knex) => { + findById: async (id, tx): Promise => { try { const result = await (tx || db.replicaNode())(tableName) .where({ id } as never) .first("*"); - return result; + return result as Tables[Tname]["base"]; } catch (error) { throw new DatabaseError({ error, name: "Find by id" }); } }, - findOne: async (filter: Partial, tx?: Knex) => { - try { - const res = await (tx || db.replicaNode())(tableName).where(filter).first("*"); - return res; - } catch (error) { - throw new DatabaseError({ error, name: "Find one" }); - } - }, find: async < TCount extends boolean = false, TCountDistinct extends keyof Tables[Tname]["base"] | undefined = undefined >( filter: TFindFilter, { offset, limit, sort, count, tx, countDistinct }: TFindOpt = {} - ) => { + ): Promise> => { try { const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); if (countDistinct) { @@ -142,35 +184,43 @@ export const ormify = (db: Kne void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); } - const res = (await query) as TFindReturn; - return res; + const res = await query; + return res as TFindReturn; } catch (error) { throw new DatabaseError({ error, name: "Find one" }); } }, - create: async (data: Tables[Tname]["insert"], tx?: Knex) => { + findOne: async (filter, tx): Promise => { + try { + const res = await (tx || db.replicaNode())(tableName).where(filter).first("*"); + return res as Tables[Tname]["base"]; + } catch (error) { + throw new DatabaseError({ error, name: "Find one" }); + } + }, + create: async (data, tx): Promise => { try { const [res] = await (tx || db)(tableName) .insert(data as never) .returning("*"); - return res; + return res as Tables[Tname]["base"]; } catch (error) { throw new DatabaseError({ error, name: "Create" }); } }, - insertMany: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => { + insertMany: async (data, tx?): Promise => { try { if (!data.length) return []; const res = await (tx || db)(tableName) .insert(data as never) .returning("*"); - return res; + return res as Tables[Tname]["base"][]; } catch (error) { throw new DatabaseError({ error, name: "Create" }); } }, // This spilit the insert into multiple chunk - batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => { + batchInsert: async (data, tx): Promise => { try { if (!data.length) return []; const res = await (tx || db).batchInsert(tableName, data as never).returning("*"); @@ -179,12 +229,7 @@ export const ormify = (db: Kne throw new DatabaseError({ error, name: "batchInsert" }); } }, - upsert: async ( - data: readonly Tables[Tname]["insert"][], - onConflictField: keyof Tables[Tname]["base"] | Array, - tx?: Knex, - mergeColumns?: (keyof Knex.ResolveTableType, "update">)[] | undefined - ) => { + upsert: async (data, onConflictField, tx, mergeColumns): Promise => { try { if (!data.length) return []; const res = await (tx || db)(tableName) @@ -192,23 +237,12 @@ export const ormify = (db: Kne .onConflict(onConflictField as never) .merge(mergeColumns) .returning("*"); - return res; + return res as Tables[Tname]["base"][]; } catch (error) { throw new DatabaseError({ error, name: "Create" }); } }, - updateById: async ( - id: string, - { - $incr, - $decr, - ...data - }: Tables[Tname]["update"] & { - $incr?: { [x in keyof Partial]: number }; - $decr?: { [x in keyof Partial]: number }; - }, - tx?: Knex - ) => { + updateById: async (id, { $incr, $decr, ...data }, tx): Promise => { try { const query = (tx || db)(tableName) .where({ id } as never) @@ -225,23 +259,12 @@ export const ormify = (db: Kne }); } const [docs] = await query; - return docs; + return docs as Tables[Tname]["base"]; } catch (error) { throw new DatabaseError({ error, name: "Update by id" }); } }, - update: async ( - filter: TFindFilter, - { - $incr, - $decr, - ...data - }: Tables[Tname]["update"] & { - $incr?: { [x in keyof Partial]: number }; - $decr?: { [x in keyof Partial]: number }; - }, - tx?: Knex - ) => { + update: async (filter, { $incr, $decr, ...data }, tx): Promise => { try { const query = (tx || db)(tableName) .where(buildFindFilter(filter)) @@ -258,26 +281,34 @@ export const ormify = (db: Kne void query.increment(incrementField, incrementValue); }); } - return await query; + return (await query) as Tables[Tname]["base"][]; } catch (error) { throw new DatabaseError({ error, name: "Update" }); } }, - deleteById: async (id: string, tx?: Knex) => { + deleteById: async (id, tx): Promise => { try { const [res] = await (tx || db)(tableName) .where({ id } as never) .delete() .returning("*"); - return res; + return res as Tables[Tname]["base"]; } catch (error) { throw new DatabaseError({ error, name: "Delete by id" }); } }, - delete: async (filter: TFindFilter, tx?: Knex) => { + countDocuments: async (tx): Promise => { + try { + const [res] = await (tx || db)(tableName).count({ count: "*" }).returning("*"); + return Number((res as { count: number }).count || 0); + } catch (error) { + throw new DatabaseError({ error, name: "Delete by id" }); + } + }, + delete: async (filter, tx): Promise => { try { const res = await (tx || db)(tableName).where(buildFindFilter(filter)).delete().returning("*"); - return res; + return res as Tables[Tname]["base"][]; } catch (error) { throw new DatabaseError({ error, name: "Delete" }); } diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 25677841d..16c5bb38f 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -325,11 +325,69 @@ const isQueueEnabled = (name: QueueName) => { } }; -export type TQueueServiceFactory = ReturnType; +export type TQueueServiceFactory = { + initialize: () => Promise; + start: ( + name: T, + jobFn: (job: Job, token?: string) => Promise, + queueSettings?: Omit + ) => void; + startPg: ( + jobName: QueueJobs, + jobsFn: (jobs: PgBoss.JobWithMetadata[]) => Promise, + options: WorkOptions & { + workerCount: number; + } + ) => Promise; + listen: < + T extends QueueName, + U extends keyof WorkerListener + >( + name: T, + event: U, + listener: WorkerListener[U] + ) => void; + queue: ( + name: T, + job: TQueueJobTypes[T]["name"], + data: TQueueJobTypes[T]["payload"], + opts?: JobsOptions & { + jobId?: string; + } + ) => Promise; + queuePg: ( + job: TQueueJobTypes[T]["name"], + data: TQueueJobTypes[T]["payload"], + opts?: PgBoss.SendOptions & { jobId?: string } + ) => Promise; + schedulePg: ( + job: TQueueJobTypes[T]["name"], + cron: string, + data: TQueueJobTypes[T]["payload"], + opts?: PgBoss.ScheduleOptions & { jobId?: string } + ) => Promise; + shutdown: () => Promise; + stopRepeatableJob: ( + name: T, + job: TQueueJobTypes[T]["name"], + repeatOpt: RepeatOptions, + jobId?: string + ) => Promise; + stopRepeatableJobByJobId: (name: T, jobId: string) => Promise; + stopRepeatableJobByKey: (name: T, repeatJobKey: string) => Promise; + clearQueue: (name: QueueName) => Promise; + stopJobById: (name: T, jobId: string) => Promise; + getRepeatableJobs: ( + name: QueueName, + startOffset?: number, + endOffset?: number + ) => Promise<{ key: string; name: string; id: string | null }[]>; +}; + export const queueServiceFactory = ( redisCfg: TRedisConfigKeys, { dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string } -) => { +): TQueueServiceFactory => { const connection = buildRedisFromConfig(redisCfg); const queueContainer = {} as Record< QueueName, @@ -366,36 +424,26 @@ export const queueServiceFactory = ( }); }; - const start = ( - name: T, - jobFn: (job: Job, token?: string) => Promise, - queueSettings: Omit = {} - ) => { + const start: TQueueServiceFactory["start"] = (name, jobFn, queueSettings) => { if (queueContainer[name]) { throw new Error(`${name} queue is already initialized`); } - queueContainer[name] = new Queue(name as string, { + queueContainer[name] = new Queue(name as string, { ...queueSettings, connection }); const appCfg = getConfig(); if (appCfg.QUEUE_WORKERS_ENABLED && isQueueEnabled(name)) { - workerContainer[name] = new Worker(name, jobFn, { + workerContainer[name] = new Worker(name, jobFn, { ...queueSettings, connection }); } }; - const startPg = async ( - jobName: QueueJobs, - jobsFn: (jobs: PgBoss.JobWithMetadata[]) => Promise, - options: WorkOptions & { - workerCount: number; - } - ) => { + const startPg: TQueueServiceFactory["startPg"] = async (jobName, jobsFn, options) => { if (queueContainerPg[jobName]) { throw new Error(`${jobName} queue is already initialized`); } @@ -429,19 +477,12 @@ export const queueServiceFactory = ( await Promise.all( Array.from({ length: options.workerCount }).map(() => - pgBoss.work(jobName, { ...options, includeMetadata: true }, jobsFn) + pgBoss.work(jobName, { ...options, includeMetadata: true }, jobsFn) ) ); }; - const listen = < - T extends QueueName, - U extends keyof WorkerListener - >( - name: T, - event: U, - listener: WorkerListener[U] - ) => { + const listen: TQueueServiceFactory["listen"] = (name, event, listener) => { const appCfg = getConfig(); if (!appCfg.QUEUE_WORKERS_ENABLED || !isQueueEnabled(name)) { return; @@ -451,12 +492,7 @@ export const queueServiceFactory = ( worker.on(event, listener); }; - const queue = async ( - name: T, - job: TQueueJobTypes[T]["name"], - data: TQueueJobTypes[T]["payload"], - opts?: JobsOptions & { jobId?: string } - ) => { + const queue: TQueueServiceFactory["queue"] = async (name, job, data, opts) => { const q = queueContainer[name]; await q.add(job, data, opts); @@ -474,35 +510,25 @@ export const queueServiceFactory = ( }); }; - const schedulePg = async ( - job: TQueueJobTypes[T]["name"], - cron: string, - data: TQueueJobTypes[T]["payload"], - opts?: PgBoss.ScheduleOptions & { jobId?: string } - ) => { + const schedulePg: TQueueServiceFactory["schedulePg"] = async (job, cron, data, opts) => { await pgBoss.schedule(job, cron, data, opts); }; - const stopRepeatableJob = async ( - name: T, - job: TQueueJobTypes[T]["name"], - repeatOpt: RepeatOptions, - jobId?: string - ) => { + const stopRepeatableJob: TQueueServiceFactory["stopRepeatableJob"] = async (name, job, repeatOpt, jobId) => { const q = queueContainer[name]; if (q) { return q.removeRepeatable(job, repeatOpt, jobId); } }; - const getRepeatableJobs = (name: QueueName, startOffset?: number, endOffset?: number) => { + const getRepeatableJobs: TQueueServiceFactory["getRepeatableJobs"] = (name, startOffset, endOffset) => { const q = queueContainer[name]; if (!q) throw new Error(`Queue '${name}' not initialized`); return q.getRepeatableJobs(startOffset, endOffset); }; - const stopRepeatableJobByJobId = async (name: T, jobId: string) => { + const stopRepeatableJobByJobId: TQueueServiceFactory["stopRepeatableJobByJobId"] = async (name, jobId) => { const q = queueContainer[name]; const job = await q.getJob(jobId); if (!job) return true; @@ -511,23 +537,23 @@ export const queueServiceFactory = ( return q.removeRepeatableByKey(job.repeatJobKey); }; - const stopRepeatableJobByKey = async (name: T, repeatJobKey: string) => { + const stopRepeatableJobByKey: TQueueServiceFactory["stopRepeatableJobByKey"] = async (name, repeatJobKey) => { const q = queueContainer[name]; return q.removeRepeatableByKey(repeatJobKey); }; - const stopJobById = async (name: T, jobId: string) => { + const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => { const q = queueContainer[name]; const job = await q.getJob(jobId); return job?.remove().catch(() => undefined); }; - const clearQueue = async (name: QueueName) => { + const clearQueue: TQueueServiceFactory["clearQueue"] = async (name) => { const q = queueContainer[name]; await q.drain(); }; - const shutdown = async () => { + const shutdown: TQueueServiceFactory["shutdown"] = async () => { await Promise.all(Object.values(workerContainer).map((worker) => worker.close())); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 7ca43154f..ab84c014d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -39,6 +39,7 @@ import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema } from "@app/services/app-connection/databricks"; +import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { @@ -105,7 +106,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOCIConnectionSchema.options, ...SanitizedOracleDBConnectionSchema.options, ...SanitizedOnePassConnectionSchema.options, - ...SanitizedRenderConnectionSchema.options + ...SanitizedRenderConnectionSchema.options, + ...SanitizedFlyioConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -133,7 +135,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ OCIConnectionListItemSchema, OracleDBConnectionListItemSchema, OnePassConnectionListItemSchema, - RenderConnectionListItemSchema + RenderConnectionListItemSchema, + FlyioConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts new file mode 100644 index 000000000..c2a1199aa --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/flyio-connection-router.ts @@ -0,0 +1,51 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateFlyioConnectionSchema, + SanitizedFlyioConnectionSchema, + UpdateFlyioConnectionSchema +} from "@app/services/app-connection/flyio"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerFlyioConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Flyio, + server, + sanitizedResponseSchema: SanitizedFlyioConnectionSchema, + createSchema: CreateFlyioConnectionSchema, + updateSchema: UpdateFlyioConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/apps`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + const apps = await server.services.appConnection.flyio.listApps(connectionId, req.permission); + return apps; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index c5dcec537..45af934a7 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router"; @@ -54,5 +55,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { name: z.string().trim().describe(IDENTITIES.CREATE.name), organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId), role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role), + hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection), metadata: z .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .array() @@ -75,6 +76,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { type: EventType.CREATE_IDENTITY, metadata: { name: identity.name, + hasDeleteProtection: identity.hasDeleteProtection, identityId: identity.id } } @@ -86,6 +88,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { properties: { orgId: req.body.organizationId, name: identity.name, + hasDeleteProtection: identity.hasDeleteProtection, identityId: identity.id, ...req.auditLogInfo } @@ -117,6 +120,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { body: z.object({ name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name), role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role), + hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection), metadata: z .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .array() @@ -148,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { type: EventType.UPDATE_IDENTITY, metadata: { name: identity.name, + hasDeleteProtection: identity.hasDeleteProtection, identityId: identity.id } } @@ -243,7 +248,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ authMethods: z.array(z.string()) }) }) @@ -292,7 +297,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ authMethods: z.array(z.string()) }) }).array(), @@ -386,7 +391,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ authMethods: z.array(z.string()) }) }).array(), @@ -451,7 +456,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { temporaryAccessEndTime: z.date().nullable().optional() }) ), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ authMethods: z.array(z.string()) }), project: SanitizedProjectSchema.pick({ name: true, id: true, type: true }) diff --git a/backend/src/server/routes/v1/secret-sync-routers/flyio-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/flyio-sync-router.ts new file mode 100644 index 000000000..30501078e --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/flyio-sync-router.ts @@ -0,0 +1,13 @@ +import { CreateFlyioSyncSchema, FlyioSyncSchema, UpdateFlyioSyncSchema } from "@app/services/secret-sync/flyio"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerFlyioSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Flyio, + server, + responseSchema: FlyioSyncSchema, + createSchema: CreateFlyioSyncSchema, + updateSchema: UpdateFlyioSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 3188c9d97..675b74982 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; +import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; @@ -39,5 +40,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 9c0115b55..7d210897d 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -23,7 +23,8 @@ export enum AppConnection { OCI = "oci", OracleDB = "oracledb", OnePass = "1password", - Render = "render" + Render = "render", + Flyio = "flyio" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index d022e1647..4c97b0392 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -56,6 +56,7 @@ import { getDatabricksConnectionListItem, validateDatabricksConnectionCredentials } from "./databricks"; +import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { @@ -124,7 +125,8 @@ export const listAppConnectionOptions = () => { getOCIConnectionListItem(), getOracleDBConnectionListItem(), getOnePassConnectionListItem(), - getRenderConnectionListItem() + getRenderConnectionListItem(), + getFlyioConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -200,7 +202,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -242,6 +245,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken: + case FlyioConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; @@ -306,7 +310,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.OnePass]: platformManagedCredentialsNotSupported, - [AppConnection.Render]: platformManagedCredentialsNotSupported + [AppConnection.Render]: platformManagedCredentialsNotSupported, + [AppConnection.Flyio]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 795c4ac52..24dc31f99 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -25,7 +25,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.OCI]: "OCI", [AppConnection.OracleDB]: "OracleDB", [AppConnection.OnePass]: "1Password", - [AppConnection.Render]: "Render" + [AppConnection.Render]: "Render", + [AppConnection.Flyio]: "Fly.io" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -53,5 +54,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -200,6 +207,7 @@ export type TAppConnectionInput = { id: string } & ( | TOracleDBConnectionInput | TOnePassConnectionInput | TRenderConnectionInput + | TFlyioConnectionInput ); export type TSqlConnectionInput = @@ -239,7 +247,8 @@ export type TAppConnectionConfig = | TTeamCityConnectionConfig | TOCIConnectionConfig | TOnePassConnectionConfig - | TRenderConnectionConfig; + | TRenderConnectionConfig + | TFlyioConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -266,7 +275,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateOCIConnectionCredentialsSchema | TValidateOracleDBConnectionCredentialsSchema | TValidateOnePassConnectionCredentialsSchema - | TValidateRenderConnectionCredentialsSchema; + | TValidateRenderConnectionCredentialsSchema + | TValidateFlyioConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-enums.ts b/backend/src/services/app-connection/flyio/flyio-connection-enums.ts new file mode 100644 index 000000000..651e46658 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-enums.ts @@ -0,0 +1,3 @@ +export enum FlyioConnectionMethod { + AccessToken = "access-token" +} diff --git a/backend/src/services/app-connection/flyio/flyio-connection-fns.ts b/backend/src/services/app-connection/flyio/flyio-connection-fns.ts new file mode 100644 index 000000000..56d4c0b31 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-fns.ts @@ -0,0 +1,72 @@ +import { AxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { FlyioConnectionMethod } from "./flyio-connection-enums"; +import { TFlyioApp, TFlyioConnection, TFlyioConnectionConfig } from "./flyio-connection-types"; + +export const getFlyioConnectionListItem = () => { + return { + name: "Fly.io" as const, + app: AppConnection.Flyio as const, + methods: Object.values(FlyioConnectionMethod) as [FlyioConnectionMethod.AccessToken] + }; +}; + +export const validateFlyioConnectionCredentials = async (config: TFlyioConnectionConfig) => { + const { accessToken } = config.credentials; + + try { + const resp = await request.post<{ data: { viewer: { id: string | null; email: string } } | null }>( + IntegrationUrls.FLYIO_API_URL, + { query: "query { viewer { id email } }" }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + if (resp.data.data === null) { + throw new BadRequestError({ + message: "Unable to validate connection: Invalid access token provided." + }); + } + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; + +export const listFlyioApps = async (appConnection: TFlyioConnection) => { + const { accessToken } = appConnection.credentials; + + const resp = await request.post<{ data: { apps: { nodes: TFlyioApp[] } } }>( + IntegrationUrls.FLYIO_API_URL, + { + query: + "query GetApps { apps { nodes { id name hostname status organization { id slug } currentRelease { version status createdAt } } } }" + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + + return resp.data.data.apps.nodes; +}; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts b/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts new file mode 100644 index 000000000..4466d24df --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-schemas.ts @@ -0,0 +1,62 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { FlyioConnectionMethod } from "./flyio-connection-enums"; + +export const FlyioConnectionAccessTokenCredentialsSchema = z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .max(1000) + .startsWith("FlyV1", "Token must start with 'FlyV1'") + .describe(AppConnections.CREDENTIALS.FLYIO.accessToken) +}); + +const BaseFlyioConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Flyio) }); + +export const FlyioConnectionSchema = BaseFlyioConnectionSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: FlyioConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [ + BaseFlyioConnectionSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z.literal(FlyioConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.Flyio).method), + credentials: FlyioConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Flyio).credentials + ) + }) +]); + +export const CreateFlyioConnectionSchema = ValidateFlyioConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Flyio) +); + +export const UpdateFlyioConnectionSchema = z + .object({ + credentials: FlyioConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Flyio).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio)); + +export const FlyioConnectionListItemSchema = z.object({ + name: z.literal("Fly.io"), + app: z.literal(AppConnection.Flyio), + methods: z.nativeEnum(FlyioConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/flyio/flyio-connection-service.ts b/backend/src/services/app-connection/flyio/flyio-connection-service.ts new file mode 100644 index 000000000..dd88633a0 --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listFlyioApps } from "./flyio-connection-fns"; +import { TFlyioConnection } from "./flyio-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const flyioConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Flyio, connectionId, actor); + + try { + const apps = await listFlyioApps(appConnection); + return apps; + } catch (error) { + logger.error(error, "Failed to establish connection with fly.io"); + return []; + } + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/flyio/flyio-connection-types.ts b/backend/src/services/app-connection/flyio/flyio-connection-types.ts new file mode 100644 index 000000000..f643caffb --- /dev/null +++ b/backend/src/services/app-connection/flyio/flyio-connection-types.ts @@ -0,0 +1,27 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateFlyioConnectionSchema, + FlyioConnectionSchema, + ValidateFlyioConnectionCredentialsSchema +} from "./flyio-connection-schemas"; + +export type TFlyioConnection = z.infer; + +export type TFlyioConnectionInput = z.infer & { + app: AppConnection.Flyio; +}; + +export type TValidateFlyioConnectionCredentialsSchema = typeof ValidateFlyioConnectionCredentialsSchema; + +export type TFlyioConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TFlyioApp = { + id: string; + name: string; +}; diff --git a/backend/src/services/app-connection/flyio/index.ts b/backend/src/services/app-connection/flyio/index.ts new file mode 100644 index 000000000..f6aefd6b1 --- /dev/null +++ b/backend/src/services/app-connection/flyio/index.ts @@ -0,0 +1,4 @@ +export * from "./flyio-connection-enums"; +export * from "./flyio-connection-fns"; +export * from "./flyio-connection-schemas"; +export * from "./flyio-connection-types"; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 64ba573d5..689228941 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -3,8 +3,7 @@ import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index a57c085ba..8a2aa0a5d 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType, TableName } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 083117241..98d9491f7 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -12,7 +12,7 @@ import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionCertificateActions, diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index be1200503..4fa4e8283 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -4,7 +4,7 @@ import bcrypt from "bcrypt"; import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 3921774fd..5558ccd65 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -4,7 +4,7 @@ import { createPrivateKey, createPublicKey, sign, verify } from "crypto"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, ProjectPermissionSub diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index b968a8951..fd6cbf39d 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { SigningAlgorithm } from "@app/lib/crypto/sign"; import { DatabaseErrorCode } from "@app/lib/error-codes"; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index b38611419..de293609b 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { OrgServiceActor } from "@app/lib/types"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 700819022..1a6d8ea11 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -1,5 +1,5 @@ import { OrgMembershipRole } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { ForbiddenRequestError } from "@app/lib/errors"; diff --git a/backend/src/services/folder-commit/folder-commit-service.ts b/backend/src/services/folder-commit/folder-commit-service.ts index 8c0bc8ebf..35f032312 100644 --- a/backend/src/services/folder-commit/folder-commit-service.ts +++ b/backend/src/services/folder-commit/folder-commit-service.ts @@ -9,7 +9,7 @@ import { TSecretV2TagJunctionInsert, TSecretVersionsV2 } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index a793ecfab..47d8950cc 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -6,7 +6,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index ad357b4e9..77ae64121 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -10,7 +10,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index d6236e4ed..b60366335 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -11,7 +11,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index 1a6e5cbd6..d0ef6fd88 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -9,6 +9,7 @@ const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/ export const validateAccountIds = z .string() .trim() + .max(2048) .default("") // Custom validation to ensure each part is a 12-digit number .refine( @@ -36,6 +37,7 @@ export const validateAccountIds = z export const validatePrincipalArns = z .string() .trim() + .max(2048) .default("") // Custom validation for ARN format .refine( diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 5baa00652..4c0d2164b 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -8,7 +8,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 7420b61cf..1e21d1d3e 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -8,7 +8,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 39fc28ad2..7ebf75a85 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -10,7 +10,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 1f89745a9..3ce28361f 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -17,7 +17,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 7462c9228..89a169b1a 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -10,7 +10,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 00e3884bd..106c30486 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -11,7 +11,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index d54a49f38..211d00163 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -11,7 +11,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 433f5ebd9..e5e59607d 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -101,6 +101,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), + db.ref("hasDeleteProtection").withSchema(TableName.Identity), db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), @@ -130,6 +131,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { data: docs, parentMapper: ({ identityName, + hasDeleteProtection, uaId, awsId, gcpId, @@ -151,6 +153,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { identity: { id: identityId, name: identityName, + hasDeleteProtection, authMethods: buildAuthMethods({ uaId, awsId, diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 14df0cd4a..81387d141 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -5,7 +5,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 549512452..8f1218045 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -8,7 +8,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index dec172e4e..eaae0150b 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -11,7 +11,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 7cf51b9d8..28064c9bb 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -114,16 +114,18 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), - db.ref("name").withSchema(TableName.Identity) + db.ref("name").withSchema(TableName.Identity), + db.ref("hasDeleteProtection").withSchema(TableName.Identity) ); if (data) { - const { name } = data; + const { name, hasDeleteProtection } = data; return { ...data, identity: { id: data.identityId, name, + hasDeleteProtection, authMethods: buildAuthMethods(data) } }; @@ -155,7 +157,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .select( selectAllTableCols(TableName.IdentityOrgMembership), - db.ref("name").withSchema(TableName.Identity).as("identityName") + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity) ) .where(filter) .as("paginatedIdentity"); @@ -245,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), db.ref("identityName").withSchema("paginatedIdentity"), + db.ref("hasDeleteProtection").withSchema("paginatedIdentity"), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -286,6 +290,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { crName, identityId, identityName, + hasDeleteProtection, role, roleId, id, @@ -324,6 +329,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { identity: { id: identityId, name: identityName, + hasDeleteProtection, authMethods: buildAuthMethods({ uaId, alicloudId, @@ -421,6 +427,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityGcpAuth}.identityId` ) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin( TableName.IdentityAwsAuth, `${TableName.IdentityOrgMembership}.identityId`, @@ -471,6 +482,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -513,6 +525,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { crName, identityId, identityName, + hasDeleteProtection, role, roleId, total_count, @@ -551,6 +564,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { identity: { id: identityId, name: identityName, + hasDeleteProtection, authMethods: buildAuthMethods({ uaId, alicloudId, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index fd893713e..4ea382f9e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -7,7 +7,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -47,6 +47,7 @@ export const identityServiceFactory = ({ const createIdentity = async ({ name, role, + hasDeleteProtection, actor, orgId, actorId, @@ -96,7 +97,7 @@ export const identityServiceFactory = ({ } const identity = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name }, tx); + const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); await identityOrgMembershipDAL.create( { identityId: newIdentity.id, @@ -138,6 +139,7 @@ export const identityServiceFactory = ({ const updateIdentity = async ({ id, role, + hasDeleteProtection, name, actor, actorId, @@ -189,7 +191,11 @@ export const identityServiceFactory = ({ } const identity = await identityDAL.transaction(async (tx) => { - const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx); + const newIdentity = + name || hasDeleteProtection + ? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx) + : await identityDAL.findById(id, tx); + if (role) { await identityOrgMembershipDAL.updateById( identityOrgMembership.id, @@ -272,6 +278,9 @@ export const identityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + if (identityOrgMembership.identity.hasDeleteProtection) + throw new BadRequestError({ message: "Identity has delete protection" }); + const deletedIdentity = await identityDAL.deleteById(id); await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 363d42a88..8d23f34fe 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -5,12 +5,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types"; export type TCreateIdentityDTO = { role: string; name: string; + hasDeleteProtection: boolean; metadata?: { key: string; value: string }[]; } & TOrgPermission; export type TUpdateIdentityDTO = { id: string; role?: string; + hasDeleteProtection?: boolean; name?: string; metadata?: { key: string; value: string }[]; isActorSuperAdmin?: boolean; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index eb17c05bb..2517c040c 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -11,7 +11,7 @@ import { TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index ad08e89d1..2ef8615eb 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSecretActions, diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index 1413a3199..23ed61402 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -10,7 +10,7 @@ import { CronJob } from "cron"; import { FastifyReply, FastifyRequest } from "fastify"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 5f9e25f29..cb161c7e5 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index 1243055eb..6ce2b22cd 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -9,7 +9,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 031b906a6..1215b7c00 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -29,7 +29,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 0ddc0f3ae..946740b66 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index 577441bfb..8d758b5e9 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectType, TPkiCollectionItems } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-queue.ts b/backend/src/services/pki-subscriber/pki-subscriber-queue.ts index 28b9353b7..c9f0d7797 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-queue.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-queue.ts @@ -1,5 +1,4 @@ -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 5bedbd60c..a3e6ec78c 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -4,7 +4,7 @@ import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, ProjectPermissionPkiSubscriberActions, diff --git a/backend/src/services/pki-templates/pki-templates-dal.ts b/backend/src/services/pki-templates/pki-templates-dal.ts index 45c632d70..81fffb0e8 100644 --- a/backend/src/services/pki-templates/pki-templates-dal.ts +++ b/backend/src/services/pki-templates/pki-templates-dal.ts @@ -4,7 +4,7 @@ import { Tables } from "knex/types/tables"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, TFindReturn } from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; export type TPkiTemplatesDALFactory = ReturnType; @@ -91,7 +91,7 @@ export const pkiTemplatesDALFactory = (db: TDbClient) => { void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); } - const res = (await query) as TFindReturn; + const res = (await query) as Array[0] & { count: string }>; return res.map((el) => ({ ...el, ca: { id: el.caId, name: el.caName } })); } catch (error) { throw new DatabaseError({ error, name: "Find one" }); diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts index 97f910d6e..e648ab88f 100644 --- a/backend/src/services/pki-templates/pki-templates-service.ts +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -5,7 +5,7 @@ import RE2 from "re2"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiTemplateActions, ProjectPermissionSub diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index dc69cbf99..7dc5b058e 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectVersion } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index f9935df54..9a82a6bbe 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index 8ce2569a0..a884d25bc 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index a68a690d3..b9e502922 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -7,7 +7,7 @@ import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { getConfig } from "@app/lib/config/env"; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index babcf7d9c..dd0eecc68 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSet, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 680b0187c..4650c474d 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -12,7 +12,7 @@ import { import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionCertificateActions, @@ -22,8 +22,10 @@ import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; -import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; +import { + InfisicalProjectTemplate, + TProjectTemplateServiceFactory +} from "@app/ee/services/project-template/project-template-types"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; @@ -263,7 +265,11 @@ export const projectServiceFactory = ({ await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]); const plan = await licenseService.getPlan(organization.id); - if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { + if ( + plan.workspaceLimit !== null && + plan.workspacesUsed >= plan.workspaceLimit && + type === ProjectType.SecretManager + ) { // case: limit imposed on number of workspaces allowed // case: number of workspaces used exceeds the number of workspaces allowed throw new BadRequestError({ diff --git a/backend/src/services/secret-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts index 1cbbbcbb8..a19ce8b88 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts @@ -1,5 +1,5 @@ import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TSecretDALFactory } from "../secret/secret-dal"; diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 5722734f8..d6007957b 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -3,7 +3,7 @@ import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 5078496d6..403484fc2 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -8,7 +8,7 @@ import { hasSecretReadValueOrDescribePermission, throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSecretActions, diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 24739b01b..e879d56f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -3,7 +3,7 @@ import crypto from "node:crypto"; import bcrypt from "bcrypt"; import { TSecretSharing } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-constants.ts b/backend/src/services/secret-sync/flyio/flyio-sync-constants.ts new file mode 100644 index 000000000..7e9e6b287 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const FLYIO_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Fly.io", + destination: SecretSync.Flyio, + connection: AppConnection.Flyio, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts b/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts new file mode 100644 index 000000000..7c006fb64 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-fns.ts @@ -0,0 +1,133 @@ +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { + TDeleteFlyioVariable, + TFlyioListVariables, + TFlyioSecret, + TFlyioSyncWithCredentials, + TPutFlyioVariable +} from "@app/services/secret-sync/flyio/flyio-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; + +const listFlyioSecrets = async ({ accessToken, appId }: TFlyioListVariables) => { + const { data } = await request.post<{ data: { app: { secrets: TFlyioSecret[] } } }>( + IntegrationUrls.FLYIO_API_URL, + { + query: "query GetAppSecrets($appId: String!) { app(id: $appId) { id name secrets { name createdAt } } }", + variables: { appId } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + + return data.data.app.secrets.map((s) => s.name); +}; + +const putFlyioSecrets = async ({ accessToken, appId, secretMap }: TPutFlyioVariable) => { + return request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: + "mutation SetAppSecrets($appId: ID!, $secrets: [SecretInput!]!) { setSecrets(input: { appId: $appId, secrets: $secrets }) { app { name } release { version } } }", + variables: { + appId, + secrets: Object.entries(secretMap).map(([key, { value }]) => ({ key, value })) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +const deleteFlyioSecrets = async ({ accessToken, appId, keys }: TDeleteFlyioVariable) => { + return request.post( + IntegrationUrls.FLYIO_API_URL, + { + query: + "mutation UnsetAppSecrets($appId: ID!, $keys: [String!]!) { unsetSecrets(input: { appId: $appId, keys: $keys }) { app { name } release { version } } }", + variables: { + appId, + keys + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); +}; + +export const FlyioSyncFns = { + syncSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + environment, + destinationConfig: { appId } + } = secretSync; + + const { accessToken } = connection.credentials; + + try { + await putFlyioSecrets({ accessToken, appId, secretMap }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + const secrets = await listFlyioSecrets({ accessToken, appId }); + + const keys = secrets.filter( + (secret) => + matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap) + ); + + try { + await deleteFlyioSecrets({ accessToken, appId, keys }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + removeSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { appId } + } = secretSync; + + const { accessToken } = connection.credentials; + + const secrets = await listFlyioSecrets({ accessToken, appId }); + + const keys = secrets.filter((secret) => secret in secretMap); + + try { + await deleteFlyioSecrets({ accessToken, appId, keys }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + getSecrets: async (secretSync: TFlyioSyncWithCredentials) => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + } +}; diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts b/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts new file mode 100644 index 000000000..b353f94b4 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const FlyioSyncDestinationConfigSchema = z.object({ + appId: z.string().trim().min(1, "App required").max(255).describe(SecretSyncs.DESTINATION_CONFIG.FLYIO.appId) +}); + +const FlyioSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const FlyioSyncSchema = BaseSecretSyncSchema(SecretSync.Flyio, FlyioSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Flyio), + destinationConfig: FlyioSyncDestinationConfigSchema +}); + +export const CreateFlyioSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Flyio, + FlyioSyncOptionsConfig +).extend({ + destinationConfig: FlyioSyncDestinationConfigSchema +}); + +export const UpdateFlyioSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Flyio, + FlyioSyncOptionsConfig +).extend({ + destinationConfig: FlyioSyncDestinationConfigSchema.optional() +}); + +export const FlyioSyncListItemSchema = z.object({ + name: z.literal("Fly.io"), + connection: z.literal(AppConnection.Flyio), + destination: z.literal(SecretSync.Flyio), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/flyio/flyio-sync-types.ts b/backend/src/services/secret-sync/flyio/flyio-sync-types.ts new file mode 100644 index 000000000..336639091 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/flyio-sync-types.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { TFlyioConnection } from "@app/services/app-connection/flyio"; + +import { CreateFlyioSyncSchema, FlyioSyncListItemSchema, FlyioSyncSchema } from "./flyio-sync-schemas"; + +export type TFlyioSync = z.infer; + +export type TFlyioSyncInput = z.infer; + +export type TFlyioSyncListItem = z.infer; + +export type TFlyioSyncWithCredentials = TFlyioSync & { + connection: TFlyioConnection; +}; + +export type TFlyioSecret = { + name: string; +}; + +export type TFlyioListVariables = { + accessToken: string; + appId: string; +}; + +export type TPutFlyioVariable = TFlyioListVariables & { + secretMap: { [key: string]: { value: string } }; +}; + +export type TDeleteFlyioVariable = TFlyioListVariables & { + keys: string[]; +}; diff --git a/backend/src/services/secret-sync/flyio/index.ts b/backend/src/services/secret-sync/flyio/index.ts new file mode 100644 index 000000000..b2fe2b0d5 --- /dev/null +++ b/backend/src/services/secret-sync/flyio/index.ts @@ -0,0 +1,4 @@ +export * from "./flyio-sync-constants"; +export * from "./flyio-sync-fns"; +export * from "./flyio-sync-schemas"; +export * from "./flyio-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 609b5f639..2ac235fe2 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -16,7 +16,8 @@ export enum SecretSync { TeamCity = "teamcity", OCIVault = "oci-vault", OnePass = "1password", - Render = "render" + Render = "render", + Flyio = "flyio" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index c4af17750..71c413c11 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -29,6 +29,7 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; +import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; @@ -59,7 +60,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION, [SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION, - [SecretSync.Render]: RENDER_SYNC_LIST_OPTION + [SecretSync.Render]: RENDER_SYNC_LIST_OPTION, + [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -219,6 +221,8 @@ export const SecretSyncFns = { return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Render: return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Flyio: + return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -299,6 +303,9 @@ export const SecretSyncFns = { case SecretSync.Render: secretMap = await RenderSyncFns.getSecrets(secretSync); break; + case SecretSync.Flyio: + secretMap = await FlyioSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -368,6 +375,8 @@ export const SecretSyncFns = { return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Render: return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Flyio: + return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 9112ab0d2..47cd7c164 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -19,7 +19,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.TeamCity]: "TeamCity", [SecretSync.OCIVault]: "OCI Vault", [SecretSync.OnePass]: "1Password", - [SecretSync.Render]: "Render" + [SecretSync.Render]: "Render", + [SecretSync.Flyio]: "Fly.io" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -40,7 +41,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OnePass]: AppConnection.OnePass, - [SecretSync.Render]: AppConnection.Render + [SecretSync.Render]: AppConnection.Render, + [SecretSync.Flyio]: AppConnection.Flyio }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -61,5 +63,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.TeamCity]: SecretSyncPlanType.Regular, [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise, [SecretSync.OnePass]: SecretSyncPlanType.Regular, - [SecretSync.Render]: SecretSyncPlanType.Regular + [SecretSync.Render]: SecretSyncPlanType.Regular, + [SecretSync.Flyio]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 66e83661f..a8ff94e82 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -3,8 +3,7 @@ import { AxiosError } from "axios"; import { Job } from "bullmq"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index e7751d3f9..3fdb7fea6 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,9 +1,9 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, @@ -89,7 +89,17 @@ export const secretSyncServiceFactory = ({ projectId }); - return secretSyncs as TSecretSync[]; + return secretSyncs.filter((secretSync) => + permission.can( + ProjectPermissionSecretSyncActions.Read, + secretSync.environment && secretSync.folder + ? subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + : ProjectPermissionSub.SecretSyncs + ) + ) as TSecretSync[]; }; const listSecretSyncsBySecretPath = async ( @@ -105,7 +115,15 @@ export const secretSyncServiceFactory = ({ projectId }); - if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) { + if ( + permission.cannot( + ProjectPermissionSecretSyncActions.Read, + subject(ProjectPermissionSub.SecretSyncs, { + environment, + secretPath + }) + ) + ) { return []; } @@ -142,7 +160,12 @@ export const secretSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSub.SecretSyncs + secretSync.environment && secretSync.folder + ? subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + : ProjectPermissionSub.SecretSyncs ); if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) @@ -179,7 +202,12 @@ export const secretSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSub.SecretSyncs + secretSync.environment && secretSync.folder + ? subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + : ProjectPermissionSub.SecretSyncs ); if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) @@ -217,13 +245,17 @@ export const secretSyncServiceFactory = ({ ForbiddenError.from(projectPermission).throwUnlessCan( ProjectPermissionSecretSyncActions.Create, - ProjectPermissionSub.SecretSyncs + subject(ProjectPermissionSub.SecretSyncs, { environment, secretPath }) ); - throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath - }); + throwIfMissingSecretReadValueOrDescribePermission( + projectPermission, + ProjectPermissionSecretActions.DescribeSecret, + { + environment, + secretPath + } + ); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); @@ -286,10 +318,38 @@ export const secretSyncServiceFactory = ({ projectId: secretSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretSyncActions.Edit, - ProjectPermissionSub.SecretSyncs - ); + // we always check the permission against the existing environment / secret path + // if no secret path / environment is present on the secret sync, we need to check without conditions + if (secretSync.environment?.slug && secretSync.folder?.path) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Edit, + subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSub.SecretSyncs + ); + } + + // if the user is updating the secret path or environment, we need to check the permission against the new values + if (secretPath || environment) { + const environmentToCheck = environment || secretSync.environment?.slug || ""; + const secretPathToCheck = secretPath || secretSync.folder?.path || ""; + + if (environmentToCheck && secretPathToCheck) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Edit, + subject(ProjectPermissionSub.SecretSyncs, { + environment: environmentToCheck, + secretPath: secretPathToCheck + }) + ); + } + } if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) throw new BadRequestError({ @@ -315,7 +375,7 @@ export const secretSyncServiceFactory = ({ if (!updatedEnvironment || !updatedSecretPath) throw new BadRequestError({ message: "Must specify both source environment and secret path" }); - throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { environment: updatedEnvironment, secretPath: updatedSecretPath }); @@ -374,10 +434,20 @@ export const secretSyncServiceFactory = ({ projectId: secretSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretSyncActions.Delete, - ProjectPermissionSub.SecretSyncs - ); + if (secretSync.environment?.slug && secretSync.folder?.path) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Delete, + subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSub.SecretSyncs + ); + } if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) throw new BadRequestError({ @@ -441,10 +511,20 @@ export const secretSyncServiceFactory = ({ projectId: secretSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretSyncActions.SyncSecrets, - ProjectPermissionSub.SecretSyncs - ); + if (secretSync.environment?.slug && secretSync.folder?.path) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.SyncSecrets, + subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSub.SecretSyncs + ); + } if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) throw new BadRequestError({ @@ -503,10 +583,20 @@ export const secretSyncServiceFactory = ({ projectId: secretSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretSyncActions.ImportSecrets, - ProjectPermissionSub.SecretSyncs - ); + if (secretSync.environment?.slug && secretSync.folder?.path) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.ImportSecrets, + subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSub.SecretSyncs + ); + } if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) throw new BadRequestError({ @@ -559,10 +649,20 @@ export const secretSyncServiceFactory = ({ projectId: secretSync.projectId }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSecretSyncActions.RemoveSecrets, - ProjectPermissionSub.SecretSyncs - ); + if (secretSync.environment?.slug && secretSync.folder?.path) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.RemoveSecrets, + subject(ProjectPermissionSub.SecretSyncs, { + environment: secretSync.environment.slug, + secretPath: secretSync.folder.path + }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.RemoveSecrets, + ProjectPermissionSub.SecretSyncs + ); + } if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) throw new BadRequestError({ diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 2fdc7c50e..f41d8e27b 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,7 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { THCVaultSync, @@ -123,7 +124,8 @@ export type TSecretSync = | TTeamCitySync | TOCIVaultSync | TOnePassSync - | TRenderSync; + | TRenderSync + | TFlyioSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -143,7 +145,8 @@ export type TSecretSyncWithCredentials = | TTeamCitySyncWithCredentials | TOCIVaultSyncWithCredentials | TOnePassSyncWithCredentials - | TRenderSyncWithCredentials; + | TRenderSyncWithCredentials + | TFlyioSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -163,7 +166,8 @@ export type TSecretSyncInput = | TTeamCitySyncInput | TOCIVaultSyncInput | TOnePassSyncInput - | TRenderSyncInput; + | TRenderSyncInput + | TFlyioSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -183,7 +187,8 @@ export type TSecretSyncListItem = | TTeamCitySyncListItem | TOCIVaultSyncListItem | TOnePassSyncListItem - | TRenderSyncListItem; + | TRenderSyncListItem + | TFlyioSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 0a154a4be..8a08c44dd 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 1adfac22c..01718d570 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -561,7 +561,7 @@ const formatMultiValueEnv = (val?: string) => { return `"${val.replaceAll("\n", "\\n")}"`; }; -type TSecretReferenceTraceNode = { +export type TSecretReferenceTraceNode = { key: string; value?: string; environment: string; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index c4b5835c4..fc758b4f5 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -14,7 +14,7 @@ import { hasSecretReadValueOrDescribePermission, throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionCommitsActions, diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 96f89ab5f..847ef17df 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -13,7 +13,7 @@ import { TSecrets } from "@app/db/schemas"; import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index e9c6f2d88..25b6e6572 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -11,8 +11,7 @@ import { TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { Actor, EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 4fa7404d3..d5c836ff7 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -17,7 +17,7 @@ import { hasSecretReadValueOrDescribePermission, throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSecretActions, diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index bbd306bb5..d68b48d78 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -4,7 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import bcrypt from "bcrypt"; import { ActionProjectType } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSecretActions, diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index 9c1460c37..c8aa8aaf6 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { InstallProvider } from "@slack/oauth"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index a370d0332..ab8fc51c5 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -81,6 +81,7 @@ export type TMachineIdentityCreatedEvent = { event: PostHogEventTypes.MachineIdentityCreated; properties: { name: string; + hasDeleteProtection: boolean; orgId: string; identityId: string; }; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 0f6ae3bf8..07d55f787 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 6729e9a37..d5fc9f5b8 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -4,8 +4,7 @@ import { AxiosError } from "axios"; import picomatch from "picomatch"; import { TWebhooks } from "@app/db/schemas"; -import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; -import { EventType, WebhookTriggeredEvent } from "@app/ee/services/audit-log/audit-log-types"; +import { EventType, TAuditLogServiceFactory, WebhookTriggeredEvent } from "@app/ee/services/audit-log/audit-log-types"; import { request } from "@app/lib/config/request"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index c555dc8d1..eb58ee5bd 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; diff --git a/backend/src/services/workflow-integration/workflow-integration-service.ts b/backend/src/services/workflow-integration/workflow-integration-service.ts index 41419061b..cb7f7a325 100644 --- a/backend/src/services/workflow-integration/workflow-integration-service.ts +++ b/backend/src/services/workflow-integration/workflow-integration-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TWorkflowIntegrationDALFactory } from "./workflow-integration-dal"; import { TGetWorkflowIntegrationsByOrg } from "./workflow-integration-types"; diff --git a/company/documentation/engineering/how-to-write-design-doc.mdx b/company/documentation/engineering/how-to-write-design-doc.mdx index 7f6a49e47..753f884b0 100644 --- a/company/documentation/engineering/how-to-write-design-doc.mdx +++ b/company/documentation/engineering/how-to-write-design-doc.mdx @@ -10,12 +10,12 @@ Writing a design document helps you efficiently solve broad, complex engineering **Writing a design will help you:** -- **Understand the problem space:** Deeply understand the problem you’re solving to make sure it is well scoped. +- **Understand the problem space:** Deeply understand the problem you're solving to make sure it is well scoped. - **Stay on the right path:** Without proper planning, you risk cycling between partial implementation and replanning, encountering roadblocks that force you back to square one. A solid plan minimizes wasted engineering hours. - **An opportunity to collaborate:** Bring relevant engineers into the discussion to develop well-thought-out solutions and catch potential issues you might have overlooked. - **Faster implementation:** A well-thought-out plan will help you catch roadblocks early and ship quickly because you know exactly what needs to get implemented. -**When to write a design document:** +**When to write a design document:** - **Write a design doc**: If the feature is not well defined, high-security, or will take more than **1 full engineering week** to build. - **Skip the design doc**: For small, straightforward features that can be built quickly with informal discussions. @@ -27,21 +27,31 @@ If you are unsure when to create a design doc, chat with @maidul. Every feature/problem is unique, but your design docs should generally include the following sections. If you need to include additional sections, feel free to do so. 1. **Title** - - A descriptive title. - - Name of document owner and name of reviewer(s). + - A descriptive title. + - Name of document owner and name of reviewer(s). 2. **Overview** - - A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs). + - A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs). 3. **Context** - - Explain the problem’s background, why it’s important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.). + - Explain the problem's background, why it's important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.). 4. **Solution** - - Provide a big-picture explanation of the solution, followed by detailed technical architecture. - - Use diagrams/charts where needed. - - Write clearly so that another engineer could implement the solution in your absence. -5. **Milestones** - - Break the project into phases with clear start and end dates estimates. Use a table or bullet points. -6. **FAQ** - - Common questions or concerns someone might have while reading your document that can be quickly addressed. + - Provide a big-picture explanation of the solution, followed by detailed technical architecture. + - Use diagrams/charts where needed. + - Write clearly so that another engineer could implement the solution in your absence. + - Break down the solution into meaningful, logical chunks that can be implemented independently. Consider: + + - Core infrastructure vs feature-specific implementations + - Backend services that can be built and tested in isolation + - Data model changes that can be implemented in phases + - API endpoints that can be versioned and rolled out gradually + + This breakdown will inform your milestones and help reviewers understand the implementation strategy. If you find yourself planning PRs that are 3,000+ lines long, it might be a sign that you need to break down the work further. While PR size isn't the primary factor in determining breakdown, it can be a useful indicator that a chunk of work might be too large to review effectively. + +5. **Milestones** + - Break the project into phases with clear start and end dates estimates. Use a table or bullet points. + - Each milestone should align with the logical chunks identified in the Solution section. +6. **FAQ** + - Common questions or concerns someone might have while reading your document that can be quickly addressed. ## **How to Write a Design Doc** @@ -51,17 +61,18 @@ Every feature/problem is unique, but your design docs should generally include t Before sharing your design docs with others, review your design doc as if you were a teammate seeing it for the first time. Anticipate questions and address them. - ## **Process from start to finish** 1. **Research/Discuss** - - Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are. - Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc? - - Once you’ve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don’t get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning. + + - Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are. + Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc? + + Once you've got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don't get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning. + 2. **Write the Doc** - - Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand. + - Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand. 3. **Assign Reviewers** - - Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up. + - Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up. 4. **Team Review and Feedback** - - Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on. \ No newline at end of file + - Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on. diff --git a/docs/api-reference/endpoints/app-connections/flyio/available.mdx b/docs/api-reference/endpoints/app-connections/flyio/available.mdx new file mode 100644 index 000000000..20a643942 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/flyio/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/create.mdx b/docs/api-reference/endpoints/app-connections/flyio/create.mdx new file mode 100644 index 000000000..afe00ba5e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/flyio" +--- + + + Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/flyio/delete.mdx b/docs/api-reference/endpoints/app-connections/flyio/delete.mdx new file mode 100644 index 000000000..ec5907840 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/flyio/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx new file mode 100644 index 000000000..ad09af5c6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/flyio/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx new file mode 100644 index 000000000..f6053d02a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/flyio/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/list.mdx b/docs/api-reference/endpoints/app-connections/flyio/list.mdx new file mode 100644 index 000000000..73271db2e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/flyio" +--- diff --git a/docs/api-reference/endpoints/app-connections/flyio/update.mdx b/docs/api-reference/endpoints/app-connections/flyio/update.mdx new file mode 100644 index 000000000..0cc6e2496 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/flyio/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/flyio/{connectionId}" +--- + + + Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx new file mode 100644 index 000000000..a080e09c4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/flyio" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx new file mode 100644 index 000000000..5dbd5ff1b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx new file mode 100644 index 000000000..8dfff7915 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx new file mode 100644 index 000000000..0121546b0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/flyio/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx new file mode 100644 index 000000000..39f3475fb --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx new file mode 100644 index 000000000..784c78cac --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/flyio" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx new file mode 100644 index 000000000..3159dd51a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx new file mode 100644 index 000000000..3495b9ae4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/flyio/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx b/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx new file mode 100644 index 000000000..cf448327f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/flyio/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/flyio/{syncId}" +--- diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx index 41706cc6b..824fdd461 100644 --- a/docs/documentation/platform/sso/auth0-saml.mdx +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -63,9 +63,9 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." Click **Save**. - + Make sure the `firstName` claim is mapped to a valid field of your Auth0 users. If your users don't have a `"given_name"` field available, you can replace it with `"name"` or another field that exists in your user profile on the left side of the mapping. - + Enabling SAML SSO allows members in your organization to log into Infisical via Auth0. diff --git a/docs/images/app-connections/flyio/app-connection-created.png b/docs/images/app-connections/flyio/app-connection-created.png new file mode 100644 index 000000000..befffbd02 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-created.png differ diff --git a/docs/images/app-connections/flyio/app-connection-modal.png b/docs/images/app-connections/flyio/app-connection-modal.png new file mode 100644 index 000000000..3bf372241 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-modal.png differ diff --git a/docs/images/app-connections/flyio/app-connection-option.png b/docs/images/app-connections/flyio/app-connection-option.png new file mode 100644 index 000000000..056ca6b76 Binary files /dev/null and b/docs/images/app-connections/flyio/app-connection-option.png differ diff --git a/docs/images/app-connections/flyio/create-token-page.png b/docs/images/app-connections/flyio/create-token-page.png new file mode 100644 index 000000000..910a6af58 Binary files /dev/null and b/docs/images/app-connections/flyio/create-token-page.png differ diff --git a/docs/images/app-connections/flyio/create-token.png b/docs/images/app-connections/flyio/create-token.png new file mode 100644 index 000000000..3dab26411 Binary files /dev/null and b/docs/images/app-connections/flyio/create-token.png differ diff --git a/docs/images/app-connections/flyio/dashboard-page.png b/docs/images/app-connections/flyio/dashboard-page.png new file mode 100644 index 000000000..ff567f1e2 Binary files /dev/null and b/docs/images/app-connections/flyio/dashboard-page.png differ diff --git a/docs/images/secret-syncs/flyio/configure-destination.png b/docs/images/secret-syncs/flyio/configure-destination.png new file mode 100644 index 000000000..d11a194ad Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-destination.png differ diff --git a/docs/images/secret-syncs/flyio/configure-details.png b/docs/images/secret-syncs/flyio/configure-details.png new file mode 100644 index 000000000..d0f70a3f3 Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-details.png differ diff --git a/docs/images/secret-syncs/flyio/configure-source.png b/docs/images/secret-syncs/flyio/configure-source.png new file mode 100644 index 000000000..75507f6fe Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-source.png differ diff --git a/docs/images/secret-syncs/flyio/configure-sync-options.png b/docs/images/secret-syncs/flyio/configure-sync-options.png new file mode 100644 index 000000000..f62e433e4 Binary files /dev/null and b/docs/images/secret-syncs/flyio/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/flyio/review-configuration.png b/docs/images/secret-syncs/flyio/review-configuration.png new file mode 100644 index 000000000..a76ead15b Binary files /dev/null and b/docs/images/secret-syncs/flyio/review-configuration.png differ diff --git a/docs/images/secret-syncs/flyio/select-option.png b/docs/images/secret-syncs/flyio/select-option.png new file mode 100644 index 000000000..ab5be088a Binary files /dev/null and b/docs/images/secret-syncs/flyio/select-option.png differ diff --git a/docs/images/secret-syncs/flyio/sync-created.png b/docs/images/secret-syncs/flyio/sync-created.png new file mode 100644 index 000000000..30a23f0d9 Binary files /dev/null and b/docs/images/secret-syncs/flyio/sync-created.png differ diff --git a/docs/integrations/app-connections/flyio.mdx b/docs/integrations/app-connections/flyio.mdx new file mode 100644 index 000000000..e42756254 --- /dev/null +++ b/docs/integrations/app-connections/flyio.mdx @@ -0,0 +1,96 @@ +--- +title: "Fly.io Connection" +description: "Learn how to configure a Fly.io Connection for Infisical." +--- + +Infisical supports the use of [Access Tokens](https://fly.io/docs/security/tokens/) to connect with Fly.io. + +## Create Fly.io Access Token + + + + ![Dashboard Page](/images/app-connections/flyio/dashboard-page.png) + + + ![Click Create Token](/images/app-connections/flyio/create-token.png) + + + Ensure that you give this token access to the correct app, then click 'Create Token'. + + ![Create Token Page](/images/app-connections/flyio/create-token-page.png) + + + After clicking 'Create Token', a modal containing your access token will appear. Save this token for later steps. + + + +## Create Fly.io Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **Fly.io Connection** option from the available integrations. + + ![Select Fly.io Connection](/images/app-connections/flyio/app-connection-option.png) + + + Complete the Fly.io Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - The Access Token from earlier steps + + ![Fly.io Connection Modal](/images/app-connections/flyio/app-connection-modal.png) + + + After clicking Create, your **Fly.io Connection** is established and ready to use with your Infisical projects. + + ![Fly.io Connection Created](/images/app-connections/flyio/app-connection-created.png) + + + + + To create a Fly.io Connection, make an API request to the [Create Fly.io Connection](/api-reference/endpoints/app-connections/flyio/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/flyio \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-flyio-connection", + "method": "access-token", + "credentials": { + "accessToken": "[PRIVATE TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-flyio-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "flyio", + "method": "access-token", + "credentials": {} + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/flyio.mdx b/docs/integrations/secret-syncs/flyio.mdx new file mode 100644 index 000000000..adc8a32d0 --- /dev/null +++ b/docs/integrations/secret-syncs/flyio.mdx @@ -0,0 +1,155 @@ +--- +title: "Fly.io Sync" +description: "Learn how to configure a Fly.io Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Fly.io Connection](/integrations/app-connections/flyio) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Fly.io](/images/secret-syncs/flyio/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/flyio/configure-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/flyio/configure-destination.png) + + - **Fly.io Connection**: The Fly.io Connection to authenticate with. + - **App**: The Fly.io app to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/flyio/configure-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Fly.io does not support importing secrets. + + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Fly.io Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/flyio/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Fly.io Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/flyio/review-configuration.png) + + + If enabled, your Fly.io Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/flyio/sync-created.png) + + + + + To create a **Fly.io Sync**, make an API request to the [Create Fly.io Sync](/api-reference/endpoints/secret-syncs/flyio/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/flyio \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-flyio-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "appId": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-flyio-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "flyio", + "name": "my-flyio-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "flyio", + "destinationConfig": { + "appId": "..." + } + } + } + ``` + + diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx index da9351188..c60db5afc 100644 --- a/docs/internals/permissions/project-permissions.mdx +++ b/docs/internals/permissions/project-permissions.mdx @@ -12,7 +12,7 @@ Each permission consists of: - **Subject**: The resource the permission applies to (e.g., secrets, members, settings) - **Action**: The operation that can be performed (e.g., read, create, edit, delete) -Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, and `dynamic-secrets`—support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply. +Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, `dynamic-secrets`, and `secret-syncs`, support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply. ## Available Project Permissions @@ -208,6 +208,8 @@ Supports conditions and permission inversion #### Subject: `secret-syncs` +Supports conditions and permission inversion. + | Action | Description | | ---------------- | -------------------------------------------------- | | `read` | View secret synchronization configurations | diff --git a/docs/mint.json b/docs/mint.json index 7322fc2f0..5672494b5 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -396,7 +396,8 @@ "pages": [ "self-hosting/guides/mongo-to-postgres", "self-hosting/guides/custom-certificates", - "self-hosting/guides/automated-bootstrapping" + "self-hosting/guides/automated-bootstrapping", + "self-hosting/guides/production-hardening" ] }, { @@ -503,6 +504,7 @@ "integrations/app-connections/azure-key-vault", "integrations/app-connections/camunda", "integrations/app-connections/databricks", + "integrations/app-connections/flyio", "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/github-radar", @@ -538,6 +540,7 @@ "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/camunda", "integrations/secret-syncs/databricks", + "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", "integrations/secret-syncs/hashicorp-vault", @@ -666,9 +669,9 @@ "sdks/languages/node", "sdks/languages/python", "sdks/languages/java", + "sdks/languages/csharp", "sdks/languages/go", - "sdks/languages/ruby", - "sdks/languages/csharp" + "sdks/languages/ruby" ] }, { @@ -1264,6 +1267,18 @@ "api-reference/endpoints/app-connections/databricks/delete" ] }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/app-connections/flyio/list", + "api-reference/endpoints/app-connections/flyio/available", + "api-reference/endpoints/app-connections/flyio/get-by-id", + "api-reference/endpoints/app-connections/flyio/get-by-name", + "api-reference/endpoints/app-connections/flyio/create", + "api-reference/endpoints/app-connections/flyio/update", + "api-reference/endpoints/app-connections/flyio/delete" + ] + }, { "group": "GCP", "pages": [ @@ -1573,6 +1588,19 @@ "api-reference/endpoints/secret-syncs/databricks/remove-secrets" ] }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/secret-syncs/flyio/list", + "api-reference/endpoints/secret-syncs/flyio/get-by-id", + "api-reference/endpoints/secret-syncs/flyio/get-by-name", + "api-reference/endpoints/secret-syncs/flyio/create", + "api-reference/endpoints/secret-syncs/flyio/update", + "api-reference/endpoints/secret-syncs/flyio/delete", + "api-reference/endpoints/secret-syncs/flyio/sync-secrets", + "api-reference/endpoints/secret-syncs/flyio/remove-secrets" + ] + }, { "group": "GCP Secret Manager", "pages": [ diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx index 4cf75a5c6..71ac7337e 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -1,9 +1,10 @@ --- title: "Infisical .NET SDK" sidebarTitle: ".NET" +url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" icon: "bars" --- - +{/* If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application. - [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk) @@ -590,4 +591,4 @@ var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions); #### Returns (string) `Plaintext` (string): The decrypted plaintext. - + */} diff --git a/docs/self-hosting/guides/production-hardening.mdx b/docs/self-hosting/guides/production-hardening.mdx new file mode 100644 index 000000000..dfd7b575f --- /dev/null +++ b/docs/self-hosting/guides/production-hardening.mdx @@ -0,0 +1,697 @@ +--- +title: "Production Hardening" +description: "Security hardening recommendations for production Infisical deployments" +--- + +This document provides specific security hardening recommendations for production Infisical deployments. These recommendations follow Infisical's security model and focus on defense in depth. + +Choose your deployment method below and follow the recommendations for your specific setup. Start with **Universal Security Fundamentals** that apply to all deployments, then follow your deployment-specific section. + +## Universal Security Fundamentals + +These security configurations apply to **all** Infisical deployments regardless of how you deploy. + +### Cryptographic Security + +#### Generate Secure Keys + +Generate strong cryptographic keys for your deployment: + +```bash +# Required - Generate secure encryption key +ENCRYPTION_KEY=$(openssl rand -hex 16) + +# Required - Generate secure auth secret +AUTH_SECRET=$(openssl rand -base64 32) +``` + +#### Configure Token Lifetimes + +Minimize exposure window for compromised tokens: + +```bash +# JWT token configuration (adjust based on security requirements) +JWT_AUTH_LIFETIME=15m # Authentication tokens +JWT_REFRESH_LIFETIME=24h # Refresh tokens +JWT_SERVICE_LIFETIME=1h # Service tokens +``` + +### Network Security + +#### TLS Configuration + +Configure HTTPS and secure database connections: + +```bash +# Enable HTTPS (recommended for production) +HTTPS_ENABLED=true + +# Secure PostgreSQL connection with SSL +DB_CONNECTION_URI="postgresql://user:pass@host:5432/db?sslmode=require" + +# For base64-encoded SSL certificate +DB_ROOT_CERT="" +``` + +#### Redis Security + +Use authentication and TLS for Redis: + +```bash +# Redis with TLS (if supported by your Redis deployment) +REDIS_URL="rediss://user:password@redis:6380" + +# Redis Sentinel configuration for high availability +REDIS_SENTINEL_HOSTS="192.168.65.254:26379,192.168.65.254:26380" +REDIS_SENTINEL_MASTER_NAME="mymaster" +REDIS_SENTINEL_ENABLE_TLS=true +REDIS_SENTINEL_USERNAME="sentinel_user" +REDIS_SENTINEL_PASSWORD="sentinel_password" +``` + +#### Network Access Controls + +Configure network restrictions and firewall rules: + +```bash +# Limit CORS to specific domains +CORS_ALLOWED_ORIGINS=["https://your-app.example.com"] + +# Prevent connections to internal/private IP addresses +# This blocks access to internal services like metadata endpoints, +# internal APIs, databases, and other sensitive infrastructure +ALLOW_INTERNAL_IP_CONNECTIONS=false +``` + +**Implement network firewalls**. Restrict network access to only necessary services: + +- **Required ports**: Infisical API (8080) and HTTPS (if applicable) +- **Database access**: Restrict PostgreSQL and Redis to authorized sources only +- **Principle**: Default deny incoming, allow only required traffic +- **Implementation**: See your deployment-specific section below for exact configuration + +### Application Security + +#### Site Configuration + +Set proper site URL for your Infisical instance: + +```bash +# Required - Must be absolute URL with protocol +SITE_URL="https://app.infisical.com" +``` + +#### SMTP Security + +Use TLS for email communications: + +```bash +# SMTP with TLS +SMTP_HOST="smtp.example.com" +SMTP_PORT="587" +SMTP_USERNAME="your-smtp-user" +SMTP_PASSWORD="your-smtp-password" +SMTP_REQUIRE_TLS=true +SMTP_IGNORE_TLS=false +SMTP_FROM_ADDRESS="noreply@example.com" +SMTP_FROM_NAME="Infisical" +``` + +#### Privacy Configuration + +Control telemetry and data collection: + +```bash +# Optional - Disable telemetry (enabled by default) +TELEMETRY_ENABLED=false +``` + +### Database Security + +#### High Availability Configuration + +Configure database read replicas for high availability PostgreSQL setups: + +```bash +# Read replica configuration (JSON format) +DB_READ_REPLICAS='[{"DB_CONNECTION_URI":"postgresql://user:pass@replica:5432/db?sslmode=require"}]' +``` + +### Operational Security + +#### User Access Management + +**Establish user off-boarding procedures**. Remove access promptly when users leave: + +1. Remove user from organization +2. Revoke active service tokens +3. Remove from external identity providers +4. Audit access logs for the user's activity +5. Rotate any shared secrets the user had access to + +#### Maintenance and Updates + +**Keep frequent upgrade cadence**. Regularly update to the latest Infisical version for your deployment method. + +## Deployment-Specific Hardening + +### Docker Deployment + +These recommendations are specific to Docker deployments of Infisical. + +#### Container Security + +**Use read-only root filesystems**. Prevent runtime modifications while allowing necessary temporary access: + +```bash +# Run with read-only filesystem but allow /tmp access +docker run --read-only \ + --tmpfs /tmp:rw,exec,size=1G \ + infisical/infisical:latest +``` + +**Note**: Infisical requires temporary directory access for: + +- Secret scanning operations +- SSH certificate generation and validation + +The `--tmpfs` mounts provide secure, isolated temporary storage that is: + +- Automatically cleaned up on container restart +- Limited in size to prevent disk exhaustion +- Isolated from the host system +- Wiped on container removal + +**Drop unnecessary capabilities**. Remove all Linux capabilities: + +```bash +# Drop all capabilities +docker run --cap-drop=ALL infisical/infisical:latest +``` + +**Use specific image tags**. Never use `latest` tags in production: + +```bash +# Use specific version tags +docker run infisical/infisical:v0.93.1-postgres +``` + +#### Resource Management + +**Set resource limits**. Prevent resource exhaustion attacks: + +```bash +# Set memory and CPU limits +docker run --memory=1g --cpus=0.5 infisical/infisical:latest +``` + +#### Health Monitoring + +**Configure health checks**. Set up Docker health checks: + +```dockerfile +# In Dockerfile or docker-compose.yml +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/api/status || exit 1 +``` + +#### Network Security + +**Host firewall configuration**. Configure host-level firewall for Docker deployments: + +```bash +# Docker manages its own iptables rules, but configure host firewall +sudo ufw default deny incoming +sudo ufw default allow outgoing + +# Allow Docker-mapped ports (adjust based on your port mapping) +sudo ufw allow 8080/tcp # If mapping container 8080 to host 8080 +sudo ufw allow 443/tcp # If terminating HTTPS at host level + +# Enable firewall +sudo ufw --force enable + +# Verify Docker iptables integration +sudo iptables -L DOCKER +``` + +#### Maintenance + +**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your image tags regularly. + +### Kubernetes Deployment + +These recommendations are specific to Kubernetes deployments of Infisical. + +#### Pod Security + +**Use Pod Security Standards**. Apply restricted security profile: + +```yaml +# Namespace-level Pod Security Standards +apiVersion: v1 +kind: Namespace +metadata: + name: infisical + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/warn: restricted +``` + +**Configure security context**. Set comprehensive security context: + +```yaml +# Deployment security context +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infisical +spec: + template: + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1001 + fsGroup: 1001 + containers: + - name: infisical + image: infisical/infisical:v0.93.1-postgres + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1001 + capabilities: + drop: + - ALL + resources: + limits: + memory: 1000Mi + cpu: 500m + requests: + cpu: 350m + memory: 512Mi +``` + +#### Network Security + +**Configure network policies**. Restrict pod-to-pod communication: + +```yaml +# Example Kubernetes NetworkPolicy +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: infisical-netpol + namespace: infisical +spec: + podSelector: + matchLabels: + app: infisical + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: ingress-system + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - podSelector: + matchLabels: + app: postgres + ports: + - protocol: TCP + port: 5432 + - to: + - podSelector: + matchLabels: + app: redis + ports: + - protocol: TCP + port: 6379 +``` + +**Infrastructure firewall considerations**. In addition to the universal host firewalls, implement infrastructure-level security: + +For cloud deployments (AWS Security Groups, Azure NSGs, or GCP Firewall Rules): + +- Allow ingress from load balancer to NodePort/ClusterIP service +- Allow egress to managed databases +- Block all other traffic + +For on-premises deployments, ensure node-level firewalls allow: + +- Ingress traffic from ingress controllers +- Egress traffic to external services (databases, SMTP) + +#### Access Control + +**Use dedicated service accounts**. Create service accounts with minimal permissions: + +```yaml +# Service account configuration +apiVersion: v1 +kind: ServiceAccount +metadata: + name: infisical + namespace: infisical +automountServiceAccountToken: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infisical +spec: + template: + spec: + serviceAccountName: infisical +``` + +#### Ingress Security + +**Configure ingress with TLS**. Set up secure ingress: + +```yaml +# Secure ingress configuration +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: infisical-ingress + namespace: infisical + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" +spec: + ingressClassName: nginx + tls: + - secretName: infisical-tls + hosts: + - app.example.com + rules: + - host: app.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: infisical + port: + number: 8080 +``` + +#### Secret Management + +**Use Kubernetes secrets**. Store sensitive configuration securely: + +```yaml +# Kubernetes secret for environment variables +apiVersion: v1 +kind: Secret +metadata: + name: infisical-secrets + namespace: infisical +type: Opaque +stringData: + AUTH_SECRET: "" + ENCRYPTION_KEY: "" + DB_CONNECTION_URI: "" + REDIS_URL: "" + SITE_URL: "" +``` + +**Note:** Kubernetes secrets are only base64-encoded by default and are not encrypted at rest unless you explicitly enable etcd encryption. For production environments, you should: + +- Enable [etcd encryption at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) to protect secrets stored in the cluster +- Limit access to etcd and Kubernetes API to only trusted administrators + +#### Health Monitoring + +**Set up health checks**. Configure readiness and liveness probes: + +```yaml +# Health check configuration +containers: + - name: infisical + readinessProbe: + httpGet: + path: /api/status + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /api/status + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 +``` + +#### Infrastructure Considerations + +**Use managed databases (if possible)**. For production deployments, consider using managed PostgreSQL and Redis services instead of in-cluster instances when feasible, as they typically provide better security, backup, and maintenance capabilities. + +#### Maintenance + +**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your deployment manifests with new image tags regularly. + +### Linux Binary Deployment + +These recommendations are specific to Linux binary deployments of Infisical. + +#### System User Management + +**Create dedicated user account**. Run Infisical under a dedicated service account: + +```bash +# Create dedicated user +sudo useradd --system --shell /bin/false --home-dir /opt/infisical infisical + +# Create application directory +sudo mkdir -p /opt/infisical +sudo chown infisical:infisical /opt/infisical +``` + +#### Service Configuration + +**Configure systemd service**. Create a secure systemd service: + +```ini +# /etc/systemd/system/infisical.service +[Unit] +Description=Infisical Secret Management +After=network.target + +[Service] +Type=simple +# IMPORTANT: Change from default 'root' user to dedicated service account +User=infisical +Group=infisical +WorkingDirectory=/opt/infisical +ExecStart=/opt/infisical/infisical-linux-amd64 +Restart=always +RestartSec=10 + +# Security settings +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/infisical +PrivateTmp=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +LimitCORE=0 +MemorySwapMax=0 + +# Environment file +EnvironmentFile=/etc/infisical/environment + +[Install] +WantedBy=multi-user.target +``` + +#### Configuration Security + +**Secure environment configuration**. Store environment variables securely: + +```bash +# Create secure config directory +sudo mkdir -p /etc/infisical +sudo chmod 750 /etc/infisical +sudo chown root:infisical /etc/infisical + +# Create environment file +sudo touch /etc/infisical/environment +sudo chmod 640 /etc/infisical/environment +sudo chown root:infisical /etc/infisical/environment +``` + +#### System Security + +**Disable memory swapping**. Prevent sensitive data from being written to disk: + +```bash +# Disable swap immediately +sudo swapoff -a + +# Disable swap permanently (comment out swap entries) +sudo sed -i '/swap/d' /etc/fstab +``` + +**Disable core dumps**. Prevent potential exposure of encryption keys: + +```bash +# Set system-wide core dump limits +echo "* hard core 0" | sudo tee -a /etc/security/limits.conf + +# Disable core dumps for current session +ulimit -c 0 +``` + +#### File Permissions + +**Secure file permissions**. Set proper permissions on application files: + +```bash +# Set binary permissions +sudo chmod 755 /opt/infisical/infisical-linux-amd64 +sudo chown infisical:infisical /opt/infisical/infisical-linux-amd64 + +# Set config file permissions +sudo chmod 640 /etc/infisical/environment +sudo chown root:infisical /etc/infisical/environment +``` + +#### Network Security + +**Host firewall configuration**. Configure comprehensive firewall for Linux binary deployments: + +```bash +# Configure UFW firewall +sudo ufw default deny incoming +sudo ufw default allow outgoing + +# Allow Infisical API access +sudo ufw allow 8080/tcp + +# Allow HTTPS (if terminating TLS at Infisical) +sudo ufw allow 443/tcp + +# If running PostgreSQL locally, restrict to localhost +sudo ufw allow from 127.0.0.1 to any port 5432 + +# If running Redis locally, restrict to localhost +sudo ufw allow from 127.0.0.1 to any port 6379 + +# Enable firewall +sudo ufw --force enable +``` + +#### System Maintenance + +**Synchronize system clocks**. Ensure accurate time for JWT tokens and audit logs: + +```bash +# Install and configure NTP +sudo apt-get update +sudo apt-get install -y ntp +sudo systemctl enable ntp +sudo systemctl start ntp + +# Verify time synchronization +timedatectl status +``` + +**Regular updates**. Monitor [Cloudsmith releases](https://cloudsmith.io/~infisical/repos/infisical-core/packages) for new binary versions and update your installation regularly. + +## Enterprise Security Features + +### Hardware Security Module (HSM) Integration + +For the highest level of encryption security, integrate with Hardware Security Modules: + +HSM integration provides hardware-protected encryption keys stored on tamper-proof devices, offering superior security for encryption operations: + +- **Supported HSM Providers**: Thales Luna Cloud HSM, AWS CloudHSM, Fortanix HSM +- **Root Key Protection**: HSM encrypts Infisical's root encryption keys using hardware-protected keys +- **Enterprise Requirements**: Ideal for government, financial, and healthcare organizations + +```bash +# HSM Environment Variables (example for production) +HSM_LIB_PATH="/path/to/hsm/library.so" +HSM_PIN="your-hsm-pin" +HSM_SLOT="0" +HSM_KEY_LABEL="infisical-root-key" +``` + +For complete HSM setup instructions, see the [HSM Integration Guide](/documentation/platform/kms/hsm-integration). + +### External Key Management Service (KMS) Integration + +Leverage cloud-native KMS providers for enhanced security and compliance: + +Infisical can integrate with external KMS providers to encrypt project secrets, providing enterprise-grade key management: + +- **Supported Providers**: AWS KMS, Google Cloud KMS, Azure Key Vault (coming soon) +- **Workspace Key Protection**: Each project's encryption key is protected by your external KMS +- **Envelope Encryption**: Infisical uses your cloud KMS to encrypt/decrypt project workspace keys, which in turn encrypt the actual secret data +- **Compliance**: Leverage your cloud provider's compliance certifications (FedRAMP, SOC2, ISO 27001) + +#### Benefits for Production Deployments + +- **Separation of Concerns**: Keys managed in your cloud infrastructure, separate from Infisical +- **Regulatory Compliance**: Use your existing compliance-certified KMS infrastructure +- **Audit Integration**: KMS operations logged in your cloud provider's audit trails +- **Disaster Recovery**: Keys backed by your cloud provider's HA and backup systems +- **Access Controls**: Leverage your cloud IAM for KMS access management + +#### Configuration Resources + +For external KMS configuration, see: + +- [AWS KMS Integration](/documentation/platform/kms-configuration/aws-kms) +- [GCP KMS Integration](/documentation/platform/kms-configuration/gcp-kms) +- [External KMS Overview](/documentation/platform/kms-configuration/overview) + +## Advanced Security Configurations + +### Backup Security + +**Configure backup encryption**. Encrypt PostgreSQL backups: + +```bash +# PostgreSQL backup with encryption +pg_dump $DB_CONNECTION_URI | gpg --cipher-algo AES256 --compress-algo 1 --symmetric --output backup.sql.gpg +``` + +### Monitoring and Logging + +**Implement log monitoring**. Set up centralized logging for security analysis and audit trails. Configure your SIEM or logging platform to monitor Infisical operations. + +### Security Updates + +**Regular security updates**. Monitor the [Infisical repository](https://github.com/Infisical/infisical) for security updates and apply them promptly. + +## Compliance and Monitoring + +### Enterprise Compliance Requirements + +For enterprise deployments requiring compliance certifications: + +- Implement audit log retention policies +- Set up security event monitoring and alerting +- Configure automated vulnerability scanning +- Establish incident response procedures +- Document security controls for compliance audits + +### Standards Compliance + +**FIPS 140-3 Compliance**. Infisical is actively working on FIPS 140-3 compliance to meet U.S. and Canadian government cryptographic standards. This will provide validated cryptographic modules for organizations requiring certified encryption implementations. diff --git a/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx index 2a6767396..69c469540 100644 --- a/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx +++ b/frontend/src/components/permissions/AccessTree/components/AccessTreeContext.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; import { ViewMode } from "../types"; @@ -23,8 +24,11 @@ interface AccessTreeProviderProps { children: ReactNode; } +export type AccessTreeForm = { metadata: { key: string; value: string }[] }; + export const AccessTreeProvider: React.FC = ({ children }) => { const [secretName, setSecretName] = useState(""); + const formMethods = useForm({ defaultValues: { metadata: [] } }); const [viewMode, setViewMode] = useState(ViewMode.Docked); const value = useMemo( @@ -37,7 +41,11 @@ export const AccessTreeProvider: React.FC = ({ children [secretName, setSecretName, viewMode, setViewMode] ); - return {children}; + return ( + + {children} + + ); }; export const useAccessTreeContext = (): AccessTreeContextProps => { diff --git a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx index 6e3790b3c..ee3ff1ff3 100644 --- a/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx +++ b/frontend/src/components/permissions/AccessTree/components/PermissionSimulation.tsx @@ -1,10 +1,12 @@ import { Dispatch, SetStateAction, useState } from "react"; +import { useFormContext } from "react-hook-form"; import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Panel } from "@xyflow/react"; import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; +import { MetadataForm } from "@app/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/MetadataForm"; import { ViewMode } from "../types"; @@ -32,6 +34,7 @@ export const PermissionSimulation = ({ setSecretName }: TProps) => { const [expand, setExpand] = useState(false); + const { control } = useFormContext(); const handlePermissionSimulation = () => { setExpand(true); @@ -139,6 +142,11 @@ export const PermissionSimulation = ({ /> )} + {subject === ProjectPermissionSub.DynamicSecrets && ( +
+ +
+ )} )} diff --git a/frontend/src/components/permissions/AccessTree/hooks/index.ts b/frontend/src/components/permissions/AccessTree/hooks/index.ts index a03f8fa0f..64e9e04a3 100644 --- a/frontend/src/components/permissions/AccessTree/hooks/index.ts +++ b/frontend/src/components/permissions/AccessTree/hooks/index.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useFormContext, useWatch } from "react-hook-form"; import { MongoAbility, MongoQuery } from "@casl/ability"; import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react"; @@ -7,7 +8,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries"; import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types"; -import { useAccessTreeContext } from "../components"; +import { AccessTreeForm, useAccessTreeContext } from "../components"; import { PermissionAccess } from "../types"; import { createBaseEdge, @@ -36,6 +37,8 @@ export const useAccessTree = ( ) => { const { currentWorkspace } = useWorkspace(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); + const { control } = useFormContext(); + const metadata = useWatch({ control, name: "metadata" }); const [nodes, setNodes] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); const [subject, setSubject] = useState(ProjectPermissionSub.Secrets); @@ -168,7 +171,8 @@ export const useAccessTree = ( environment, subject, secretName, - actionRuleMap + actionRuleMap, + metadata }) ); @@ -266,7 +270,8 @@ export const useAccessTree = ( subject, secretName, setNodes, - setEdges + setEdges, + metadata ]); return { diff --git a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx index f2ca6e878..f0baedc35 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/FolderNode/components/FolderNodeTooltipContent.tsx @@ -17,6 +17,27 @@ type Props = { access: PermissionAccess; } & Pick["data"], "actionRuleMap" | "subject">; +type ConditionDisplayProps = { + _key: string; + operator: string; + value: string | string[]; +}; + +const ConditionDisplay = ({ _key: key, value, operator }: ConditionDisplayProps) => { + return ( +
  • + {camelCaseToSpaces(key)}{" "} + + {formatedConditionsOperatorNames[operator as PermissionConditionOperators]} + {" "} + + {typeof value === "string" ? value : value.join(", ")} + + . +
  • + ); +}; + export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subject }: Props) => { let component: ReactElement; @@ -56,43 +77,58 @@ export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subjec {actionRuleMap.map((ruleMap, index) => { const rule = ruleMap[action]; - if ( - !rule || - !rule.conditions || - (!rule.conditions.secretName && !rule.conditions.secretTags) - ) - return null; + if (!rule || !rule.conditions) return null; - return ( -
  • - - {rule.inverted ? "Forbids" : "Allows"} - - when: - {Object.entries(rule.conditions).map(([key, condition]) => ( -
      - {Object.entries(condition as object).map(([operator, value]) => ( -
    • - - {camelCaseToSpaces(key)} - {" "} - - { - formatedConditionsOperatorNames[ - operator as PermissionConditionOperators - ] + if ( + rule.conditions.secretName || + rule.conditions.secretTags || + rule.conditions.metadata + ) { + return ( +
    • + {rule.inverted ? "Forbids" : "Allows"} + when: + {Object.entries(rule.conditions).map(([key, condition]) => { + if (key.match(/secretPath|environment/)) { + return null; + } + + return ( +
        + {Object.entries(condition as object).map(([operator, value]) => { + if (operator === "$elemMatch") { + return Object.entries(value as object).map( + ([nestedKey, nestedCondition]) => + Object.entries(nestedCondition as object).map( + ([nestedOperator, nestedValue]) => ( + + ) + ) + ); } - {" "} - - {typeof value === "string" ? value : value.join(", ")} - - . - - ))} -
      - ))} -
    • - ); + + return ( + + ); + })} +
    + ); + })} +
  • + ); + } + + return null; })} diff --git a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx index f6ffe212e..c3e9f89e0 100644 --- a/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx +++ b/frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from "react"; -import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons"; +import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Handle, NodeProps, Position } from "@xyflow/react"; @@ -12,15 +12,15 @@ import { createRoleNode } from "../utils"; const getSubjectIcon = (subject: ProjectPermissionSub) => { switch (subject) { case ProjectPermissionSub.Secrets: - return ; + return ; case ProjectPermissionSub.SecretFolders: return ; case ProjectPermissionSub.DynamicSecrets: - return ; + return ; case ProjectPermissionSub.SecretImports: - return ; + return ; default: - return ; + return ; } }; diff --git a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts index 15c64ce8a..a40398ba7 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts @@ -33,6 +33,12 @@ const ACTION_MAP: Record = { ] }; +const SUBJECT_HEIGHT_MAP: Record = { + [ProjectPermissionSub.DynamicSecrets]: 130, + [ProjectPermissionSub.Secrets]: 85, + default: 64 +}; + const evaluateCondition = ( value: string, operator: PermissionConditionOperators, @@ -52,13 +58,113 @@ const evaluateCondition = ( } }; +const doesConditionMatch = ( + conditions: Record | undefined, + value: string +): boolean => { + if (!conditions) return true; + + return Object.entries(conditions).every(([operator, comparisonValue]) => + evaluateCondition(value, operator as PermissionConditionOperators, comparisonValue) + ); +}; + +const doBaseConditionsApply = ( + ruleConditions: any, + environment: string, + folderPath: string +): boolean => { + return ( + doesConditionMatch(ruleConditions?.environment, environment) && + doesConditionMatch(ruleConditions?.secretPath, folderPath) + ); +}; + +const shouldShowConditionalAccess = ( + actionRuleMap: TActionRuleMap, + action: string, + environment: string, + folderPath: string, + conditionalFields: string[] +): boolean => { + return actionRuleMap.some((rule) => { + const ruleConditions = rule[action]?.conditions; + if (!ruleConditions) return false; + + // Check if any of the conditional fields are present + const hasConditionalField = conditionalFields.some((field) => ruleConditions[field]); + if (!hasConditionalField) return false; + + // Check if base conditions (environment and secretPath) apply + return doBaseConditionsApply(ruleConditions, environment, folderPath); + }); +}; + +const determineAccessLevel = ( + hasPermission: boolean, + subject: ProjectPermissionSub, + action: string, + actionRuleMap: TActionRuleMap, + environment: string, + folderPath: string, + secretName: string, + metadata: Array<{ key: string; value: string }> +): PermissionAccess => { + if (!hasPermission) { + return PermissionAccess.None; + } + + if (subject === ProjectPermissionSub.Secrets) { + if ( + !secretName && + shouldShowConditionalAccess(actionRuleMap, action, environment, folderPath, [ + "secretName", + "secretTags" + ]) + ) { + return PermissionAccess.Partial; + } + } else if (subject === ProjectPermissionSub.DynamicSecrets) { + if ( + !metadata.length && + shouldShowConditionalAccess(actionRuleMap, action, environment, folderPath, ["metadata"]) + ) { + return PermissionAccess.Partial; + } + } + + return PermissionAccess.Full; +}; + +const checkPermission = ( + permissions: MongoAbility, + subject: ProjectPermissionSub, + action: string, + subjectFields: any +): boolean => { + if ( + subject === ProjectPermissionSub.Secrets && + (action === ProjectPermissionSecretActions.ReadValue || + action === ProjectPermissionSecretActions.DescribeSecret) + ) { + return hasSecretReadValueOrDescribePermission(permissions, action, subjectFields); + } + + return permissions.can( + // @ts-expect-error we are not specifying which so can't resolve if valid + action, + abilitySubject(subject, subjectFields) + ); +}; + export const createFolderNode = ({ folder, permissions, environment, subject, secretName, - actionRuleMap + actionRuleMap, + metadata }: { folder: TSecretFolderWithPath; permissions: MongoAbility; @@ -66,6 +172,7 @@ export const createFolderNode = ({ subject: ProjectPermissionSub; secretName: string; actionRuleMap: TActionRuleMap; + metadata: Array<{ key: string; value: string }>; }) => { const actions = Object.fromEntries( Object.values(ACTION_MAP[subject] ?? Object.values(ProjectPermissionActions)).map((action) => { @@ -73,74 +180,26 @@ export const createFolderNode = ({ // wrapped in try because while editing certain conditions, if their values are empty it throws an error try { - let hasPermission: boolean; - const subjectFields = { secretPath: folder.path, environment, secretName: secretName || "*", - secretTags: ["*"] + secretTags: ["*"], + metadata: metadata.length ? metadata : ["*"] }; - if ( - subject === ProjectPermissionSub.Secrets && - (action === ProjectPermissionSecretActions.ReadValue || - action === ProjectPermissionSecretActions.DescribeSecret) - ) { - hasPermission = hasSecretReadValueOrDescribePermission( - permissions, - action, - subjectFields - ); - } else { - hasPermission = permissions.can( - // @ts-expect-error we are not specifying which so can't resolve if valid - action, - abilitySubject(subject, subjectFields) - ); - } + const hasPermission = checkPermission(permissions, subject, action, subjectFields); - if (hasPermission) { - // we want to show yellow/conditional access if user hasn't specified secret name to fully resolve access - if ( - !secretName && - actionRuleMap.some((el) => { - // we only show conditional if secretName/secretTags are present - environment and path can be directly determined - if (!el[action]?.conditions?.secretName && !el[action]?.conditions?.secretTags) - return false; - - // make sure condition applies to env - if (el[action]?.conditions?.environment) { - if ( - !Object.entries(el[action]?.conditions?.environment).every(([operator, value]) => - evaluateCondition(environment, operator as PermissionConditionOperators, value) - ) - ) { - return false; - } - } - - // and applies to path - if (el[action]?.conditions?.secretPath) { - if ( - !Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) => - evaluateCondition(folder.path, operator as PermissionConditionOperators, value) - ) - ) { - return false; - } - } - - return true; - }) - ) { - access = PermissionAccess.Partial; - } else { - access = PermissionAccess.Full; - } - } else { - access = PermissionAccess.None; - } + access = determineAccessLevel( + hasPermission, + subject, + action, + actionRuleMap, + environment, + folder.path, + secretName, + metadata + ); } catch (e) { console.error(e); access = PermissionAccess.None; @@ -150,18 +209,7 @@ export const createFolderNode = ({ }) ); - let height: number; - - switch (subject) { - case ProjectPermissionSub.DynamicSecrets: - height = 130; - break; - case ProjectPermissionSub.Secrets: - height = 85; - break; - default: - height = 64; - } + const height = SUBJECT_HEIGHT_MAP[subject] ?? SUBJECT_HEIGHT_MAP.default; return { type: PermissionNode.Folder, diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx new file mode 100644 index 000000000..a2a5bf844 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/FlyioSyncFields.tsx @@ -0,0 +1,51 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { TFlyioApp, useFlyioConnectionListApps } from "@app/hooks/api/appConnections/flyio"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const FlyioSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Flyio } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: apps, isLoading: isAppsLoading } = useFlyioConnectionListApps(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.appId", ""); + }} + /> + + ( + + v.id === value) ?? null} + onChange={(option) => onChange((option as SingleValue)?.id ?? null)} + options={apps} + placeholder="Select an app..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 61b94aec4..5d36ac71f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; +import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; import { HCVaultSyncFields } from "./HCVaultSyncFields"; @@ -64,6 +65,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Render: return ; + case SecretSync.Flyio: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx index 5c328079d..61013483f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/VercelSyncFields.tsx @@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { CreatableSelect } from "@app/components/v2/CreatableSelect"; import { TVercelConnectionApp, useVercelConnectionListOrganizations @@ -174,17 +175,36 @@ export const VercelSyncFields = () => { errorText={error?.message} label="Vercel Preview Branch (Optional)" > - branch.id === value) ?? null} onChange={(option) => onChange((option as SingleValue<{ id: string }>)?.id || "")} - options={previewBranchOptions} - placeholder="Select a branch..." + onCreateOption={(option) => { + onChange(option); + if (!option || option.trim() === "") return; + previewBranchOptions.push({ id: option, name: option }); + }} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getNewOptionData={(inputValue, _optionLabel) => { + return { + id: inputValue, + name: `${inputValue} - press Enter` + }; + }} getOptionLabel={(option) => option.name} getOptionValue={(option) => option?.id || ""} - isClearable + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isValidNewOption={(inputValue, _value, _options, _accessors) => { + return ( + inputValue.trim().length > 0 && + previewBranchOptions.filter((branch) => branch.id === inputValue).length === 0 + ); + }} /> )} diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index a1cec91de..9e60e164e 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -53,6 +53,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.OnePass: case SecretSync.OCIVault: case SecretSync.Render: + case SecretSync.Flyio: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx new file mode 100644 index 000000000..9a1a4bf69 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/FlyioSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const FlyioSyncReviewFields = () => { + const { watch } = useFormContext(); + const appId = watch("destinationConfig.appId"); + + return {appId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 7e678c240..a82400f5b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -20,6 +20,7 @@ import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; +import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; @@ -108,6 +109,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Render: DestinationFieldsComponent = ; break; + case SecretSync.Flyio: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx index 7cc19ae97..850ebbc80 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx @@ -1,17 +1,45 @@ +import { useEffect } from "react"; import { Controller, useFormContext } from "react-hook-form"; +import { subject } from "@casl/ability"; import { FilterableSelect, FormControl } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useWorkspace } from "@app/context"; +import { useProjectPermission, useWorkspace } from "@app/context"; +import { + ProjectPermissionSecretSyncActions, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; import { TSecretSyncForm } from "./schemas"; export const SecretSyncSourceFields = () => { - const { control, watch } = useFormContext(); + const { control, watch, setError, clearErrors } = useFormContext(); + const { permission } = useProjectPermission(); const { currentWorkspace } = useWorkspace(); const selectedEnvironment = watch("environment"); + const selectedSecretPath = watch("secretPath"); + + useEffect(() => { + const hasAccessToSource = + selectedEnvironment && + permission.can( + ProjectPermissionSecretSyncActions.Create, + subject(ProjectPermissionSub.SecretSyncs, { + environment: selectedEnvironment.slug, + secretPath: selectedSecretPath + }) + ); + + if (!hasAccessToSource) { + setError("secretPath", { + message: "You do not have permission to create secret syncs in this environment or path." + }); + } else { + clearErrors("secretPath"); + } + }, [selectedEnvironment, selectedSecretPath]); return ( <> diff --git a/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts new file mode 100644 index 000000000..b18081ccc --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/flyio-sync-destination-schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const FlyioSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Flyio), + destinationConfig: z.object({ + appId: z.string().trim().min(1, "App ID required") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 511075ce8..70cb4767d 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destinatio import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; +import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; @@ -37,7 +38,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ TeamCitySyncDestinationSchema, OCIVaultSyncDestinationSchema, OnePassSyncDestinationSchema, - RenderSyncDestinationSchema + RenderSyncDestinationSchema, + FlyioSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index 417e450bd..c1f9306fa 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -70,7 +70,15 @@ export const Pagination = ({ key={`pagination-per-page-options-${perPageOption}`} icon={perPage === perPageOption && } iconPos="right" - onClick={() => onChangePerPage(perPageOption)} + onClick={() => { + const totalPages = Math.ceil(count / perPageOption); + + if (page > totalPages) { + onChangePage(totalPages); + } + + onChangePerPage(perPageOption); + }} > {perPageOption} rows per page diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 11a5da466..6f3f4283a 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -48,6 +48,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport? type Props = TextareaHTMLAttributes & { value?: string | null; isVisible?: boolean; + valueAlwaysHidden?: boolean; isImport?: boolean; isReadOnly?: boolean; isDisabled?: boolean; @@ -63,6 +64,7 @@ export const SecretInput = forwardRef( value, isVisible, isImport, + valueAlwaysHidden, containerClassName, onBlur, isDisabled, @@ -84,7 +86,11 @@ export const SecretInput = forwardRef(
                 
                   
    -                {syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
    +                {syntaxHighlight(
    +                  value,
    +                  isVisible || (isSecretFocused && !valueAlwaysHidden),
    +                  isImport
    +                )}
                   
                 
               
    diff --git a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx index c82e492e8..a1d4fb2a2 100644 --- a/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx +++ b/frontend/src/components/v2/SecretPathInput/SecretPathInput.tsx @@ -137,7 +137,7 @@ export const SecretPathInput = ({ maxHeight: "var(--radix-select-content-available-height)" }} > -
    +
    {suggestions.map((suggestion, i) => (
    & DynamicSecretSubjectFields) ) ] + | [ + ProjectPermissionSecretSyncActions, + ( + | ProjectPermissionSub.SecretSyncs + | (ForcedSubject & SecretSyncSubjectFields) + ) + ] | [ ProjectPermissionActions, ( @@ -365,7 +377,6 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] - | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index cbc9de156..c27e1e646 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -19,6 +19,7 @@ import { AzureKeyVaultConnectionMethod, CamundaConnectionMethod, DatabricksConnectionMethod, + FlyioConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, GitHubRadarConnectionMethod, @@ -80,7 +81,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, [AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true }, [AppConnection.OnePass]: { name: "1Password", image: "1Password.png" }, - [AppConnection.Render]: { name: "Render", image: "Render.png" } + [AppConnection.Render]: { name: "Render", image: "Render.png" }, + [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -119,6 +121,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case TeamCityConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: + case FlyioConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index ecebec0cf..c33e45159 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -65,6 +65,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OnePass]: AppConnection.OnePass, - [SecretSync.Render]: AppConnection.Render + [SecretSync.Render]: AppConnection.Render, + [SecretSync.Flyio]: AppConnection.Flyio }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 77a9afc8e..1f5cb4a3a 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -23,5 +23,6 @@ export enum AppConnection { TeamCity = "teamcity", OCI = "oci", OnePass = "1password", - Render = "render" + Render = "render", + Flyio = "flyio" } diff --git a/frontend/src/hooks/api/appConnections/flyio/index.ts b/frontend/src/hooks/api/appConnections/flyio/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/flyio/queries.tsx b/frontend/src/hooks/api/appConnections/flyio/queries.tsx new file mode 100644 index 000000000..32c1ffbc2 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TFlyioApp } from "./types"; + +const flyioConnectionKeys = { + all: [...appConnectionKeys.all, "flyio"] as const, + listApps: (connectionId: string) => [...flyioConnectionKeys.all, "apps", connectionId] as const +}; + +export const useFlyioConnectionListApps = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TFlyioApp[], + unknown, + TFlyioApp[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: flyioConnectionKeys.listApps(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/flyio/${connectionId}/apps` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/flyio/types.ts b/frontend/src/hooks/api/appConnections/flyio/types.ts new file mode 100644 index 000000000..73345e628 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/flyio/types.ts @@ -0,0 +1,4 @@ +export type TFlyioApp = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index e56743510..e6e545daf 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -114,6 +114,10 @@ export type TRenderConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Render; }; +export type TFlyioConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Flyio; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -137,7 +141,8 @@ export type TAppConnectionOption = | TTeamCityConnectionOption | TOCIConnectionOption | TOnePassConnectionOption - | TRenderConnectionOption; + | TRenderConnectionOption + | TFlyioConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -165,4 +170,5 @@ export type TAppConnectionOptionMap = { [AppConnection.OCI]: TOCIConnectionOption; [AppConnection.OnePass]: TOnePassConnectionOption; [AppConnection.Render]: TRenderConnectionOption; + [AppConnection.Flyio]: TFlyioConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/flyio-connection.ts b/frontend/src/hooks/api/appConnections/types/flyio-connection.ts new file mode 100644 index 000000000..b1c9123b6 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/flyio-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum FlyioConnectionMethod { + AccessToken = "access-token" +} + +export type TFlyioConnection = TRootAppConnection & { app: AppConnection.Flyio } & { + method: FlyioConnectionMethod.AccessToken; + credentials: { + accessToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index e3fddd612..abaac4fad 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,6 +9,7 @@ import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; +import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; import { TGitHubRadarConnection } from "./github-radar-connection"; @@ -35,6 +36,7 @@ export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./camunda-connection"; export * from "./databricks-connection"; +export * from "./flyio-connection"; export * from "./gcp-connection"; export * from "./github-connection"; export * from "./github-radar-connection"; @@ -77,7 +79,8 @@ export type TAppConnection = | TTeamCityConnection | TOCIConnection | TOnePassConnection - | TRenderConnection; + | TRenderConnection + | TFlyioConnection; export type TAvailableAppConnection = Pick; @@ -130,4 +133,5 @@ export type TAppConnectionMap = { [AppConnection.OCI]: TOCIConnection; [AppConnection.OnePass]: TOnePassConnection; [AppConnection.Render]: TRenderConnection; + [AppConnection.Flyio]: TFlyioConnection; }; diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 006a3d7ca..56dc11aeb 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -242,6 +242,7 @@ interface CreateIdentityEvent { metadata: { identityId: string; name: string; + hasDeleteProtection: boolean; }; } @@ -250,6 +251,7 @@ interface UpdateIdentityEvent { metadata: { identityId: string; name?: string; + hasDeleteProtection?: boolean; }; } diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 52fe52dc2..7abf1a015 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -85,12 +85,13 @@ export const useCreateIdentity = () => { export const useUpdateIdentity = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ identityId, name, role, metadata }) => { + mutationFn: async ({ identityId, name, role, hasDeleteProtection, metadata }) => { const { data: { identity } } = await apiRequest.patch(`/api/v1/identities/${identityId}`, { name, role, + hasDeleteProtection, metadata }); diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 116030131..873338f09 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -14,6 +14,7 @@ export type IdentityTrustedIp = { export type Identity = { id: string; name: string; + hasDeleteProtection: boolean; authMethods: IdentityAuthMethod[]; createdAt: string; updatedAt: string; @@ -83,6 +84,7 @@ export type CreateIdentityDTO = { name: string; organizationId: string; role?: string; + hasDeleteProtection: boolean; metadata?: { key: string; value: string }[]; }; @@ -90,6 +92,7 @@ export type UpdateIdentityDTO = { identityId: string; name?: string; role?: string; + hasDeleteProtection?: boolean; organizationId: string; metadata?: { key: string; value: string }[]; }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 241d3c1e2..18360377f 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -32,6 +32,7 @@ export type TSecretApprovalSecChange = { version: number; secretKey: string; secretValue?: string; + secretValueHidden?: boolean; secretComment?: string; isRotatedSecret?: boolean; tags?: string[]; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index f7829d8e8..323266caa 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -16,7 +16,8 @@ export enum SecretSync { TeamCity = "teamcity", OCIVault = "oci-vault", OnePass = "1password", - Render = "render" + Render = "render", + Flyio = "flyio" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts b/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts new file mode 100644 index 000000000..0717de2c4 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/flyio-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TFlyioSync = TRootSecretSync & { + destination: SecretSync.Flyio; + destinationConfig: { + appId: string; + }; + connection: { + app: AppConnection.Flyio; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 3abc217ca..fc8a0da46 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TCamundaSync } from "./camunda-sync"; import { TDatabricksSync } from "./databricks-sync"; +import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; import { THCVaultSync } from "./hc-vault-sync"; @@ -45,7 +46,8 @@ export type TSecretSync = | TTeamCitySync | TOCIVaultSync | TOnePassSync - | TRenderSync; + | TRenderSync + | TFlyioSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 2b09143dc..829b55674 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -15,7 +15,8 @@ import { IconButton, Input, Modal, - ModalContent + ModalContent, + Switch } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { findOrgMembershipRole } from "@app/helpers/roles"; @@ -27,6 +28,7 @@ const schema = z .object({ name: z.string().min(1, "Required"), role: z.object({ slug: z.string(), name: z.string() }), + hasDeleteProtection: z.boolean(), metadata: z .object({ key: z.string().trim().min(1), @@ -64,7 +66,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { } = useForm({ resolver: zodResolver(schema), defaultValues: { - name: "" + name: "", + hasDeleteProtection: false } }); @@ -78,6 +81,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { identityId: string; name: string; role: string; + hasDeleteProtection: boolean; metadata?: { key: string; value: string }[]; customRole: { name: string; @@ -91,22 +95,25 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { reset({ name: identity.name, role: identity.customRole ?? findOrgMembershipRole(roles, identity.role), + hasDeleteProtection: identity.hasDeleteProtection, metadata: identity.metadata }); } else { reset({ name: "", - role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole) + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole), + hasDeleteProtection: false }); } }, [popUp?.identity?.data, roles]); - const onFormSubmit = async ({ name, role, metadata }: FormData) => { + const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => { try { const identity = popUp?.identity?.data as { identityId: string; name: string; role: string; + hasDeleteProtection: boolean; }; if (identity) { @@ -116,6 +123,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { identityId: identity.identityId, name, role: role.slug || undefined, + hasDeleteProtection, organizationId: orgId, metadata }); @@ -127,6 +135,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const { id: createdId } = await createMutateAsync({ name, role: role.slug || undefined, + hasDeleteProtection, organizationId: orgId, metadata }); @@ -215,6 +224,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + ( + + +

    Delete Protection {value ? "Enabled" : "Disabled"}

    +
    +
    + )} + />
    diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index d499a7b36..f445505e0 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -329,7 +329,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
    - + { return ; case AppConnection.Render: return ; + case AppConnection.Flyio: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -208,6 +211,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Render: return ; + case AppConnection.Flyio: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.tsx new file mode 100644 index 000000000..4b6c5bbc9 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/FlyioConnectionForm.tsx @@ -0,0 +1,136 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { FlyioConnectionMethod, TFlyioConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TFlyioConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Flyio) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(FlyioConnectionMethod.AccessToken), + credentials: z.object({ + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .startsWith("FlyV1", "Token must start with 'FlyV1'") + }) + }) +]); + +type FormData = z.infer; + +export const FlyioConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Flyio, + method: FlyioConnectionMethod.AccessToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
    + {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
    + + + + +
    + +
    + ); +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index 020a1f1c8..328c5505d 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -47,13 +47,19 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) = - + Name

    {data.identity.name}

    +
    +

    Delete Protection

    +

    + {data.identity.hasDeleteProtection ? "On" : "Off"} +

    +

    Organization Role

    {data.role}

    diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx index 05106c658..7ea2c8ba3 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx @@ -35,7 +35,7 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props Options - + { {(isAllowed) => (
    {format(new Date(createdAt), "yyyy-MM-dd")} - - - {(isAllowed) => ( - { - evt.stopPropagation(); - evt.preventDefault(); - handlePopUpOpen("deleteIdentity", { - identityId: id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - - - - + + + + + + + + + + + {(isAllowed) => ( + } + isDisabled={!isAllowed} + onClick={(evt) => { + evt.stopPropagation(); + evt.preventDefault(); + handlePopUpOpen("deleteIdentity", { + identityId: id, + name + }); + }} + > + Remove Identity From Project + + )} + + + + ); diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx index ee1f791d3..a8ecde73a 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersSection.tsx @@ -56,12 +56,12 @@ export const MembersSection = () => { return (
    -
    +

    Users

    {(isAllowed) => (
    + setSearch(e.target.value)} + leftIcon={} + placeholder="Search project roles..." + className="flex-1" + containerClassName="mb-4" + /> - - + + - {isRolesLoading && } - {roles?.map((role) => { + {isRolesLoading && } + {filteredRoles?.slice(offset, perPage * page).map((role) => { const { id, name, slug } = role; const isNonMutatable = Object.values(ProjectMembershipRole).includes( slug as ProjectMembershipRole @@ -109,88 +238,118 @@ export const ProjectRoleList = () => { ); })}
    NameSlug +
    + Name + handleSort(RolesOrderBy.Name)} + > + + +
    +
    +
    + Slug + handleSort(RolesOrderBy.Slug)} + > + + +
    +
    {name} {slug} - - -
    - -
    -
    - - - {(isAllowed) => ( - { - e.stopPropagation(); - navigate({ - to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const, - params: { - projectId: currentWorkspace.id, - roleSlug: slug - } - }); - }} - disabled={!isAllowed} - > - {`${isNonMutatable ? "View" : "Edit"} Role`} - - )} - - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("duplicateRole", role); - }} - disabled={!isAllowed} - > - Duplicate Role - - )} - - {!isNonMutatable && ( + + + + + + + + {(isAllowed) => ( } className={twMerge( - isAllowed - ? "hover:!bg-red-500 hover:!text-white" - : "pointer-events-none cursor-not-allowed opacity-50" + !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} onClick={(e) => { e.stopPropagation(); - handlePopUpOpen("deleteRole", role); + navigate({ + to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const, + params: { + projectId: currentWorkspace.id, + roleSlug: slug + } + }); }} disabled={!isAllowed} > - Delete Role + {`${isNonMutatable ? "View" : "Edit"} Role`} )} - )} - - + + {(isAllowed) => ( + } + className={twMerge( + !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" + )} + onClick={(e) => { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + + {!isNonMutatable && ( + + {(isAllowed) => ( + } + className={twMerge( + isAllowed + ? "hover:!bg-red-500 hover:!text-white" + : "pointer-events-none cursor-not-allowed opacity-50", + "transition-colors duration-100" + )} + onClick={(e) => { + e.stopPropagation(); + handlePopUpOpen("deleteRole", role); + }} + disabled={!isAllowed} + > + Delete Role + + )} + + )} + +
    +
    + {Boolean(filteredRoles?.length) && ( + + )} + {!filteredRoles?.length && !isRolesLoading && ( + + )}
    , state?: boolean) => void; }; -export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { +const ServiceTokenForm = () => { const { t } = useTranslation(); const { currentWorkspace } = useWorkspace(); const { control, - reset, handleSubmit, formState: { isSubmitting } } = useForm({ @@ -152,13 +151,197 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { } }; + return !hasServiceToken ? ( +
    + ( + + + + )} + /> + {tokenScopes.map(({ id }, index) => ( +
    + ( + + + + )} + /> + ( + + + + )} + /> + remove(index)} + > + + +
    + ))} +
    + +
    + ( + + + + )} + /> + { + const options = [ + { + label: "Read (default)", + value: "read" + }, + { + label: "Write (optional)", + value: "write" + } + ] as const; + + return ( + + <> + {options.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} + + + ); + }} + /> +
    + + + + +
    + + ) : ( +
    +

    {newToken}

    + + + + {t("common.click-to-copy")} + + +
    + ); +}; + +export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => { + const { t } = useTranslation(); + + const { currentWorkspace } = useWorkspace(); + return ( { handlePopUpToggle("createAPIToken", open); - reset(); - setToken(""); }} > { } subTitle={t("section.token.add-dialog.description") as string} > - {!hasServiceToken ? ( -
    - ( - - - - )} - /> - {tokenScopes.map(({ id }, index) => ( -
    - ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - > - - -
    - ))} -
    - -
    - ( - - - - )} - /> - { - const options = [ - { - label: "Read (default)", - value: "read" - }, - { - label: "Write (optional)", - value: "write" - } - ] as const; - - return ( - - <> - {options.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} - - - ); - }} - /> -
    - - - - -
    - - ) : ( -
    -

    {newToken}

    - - - - {t("common.click-to-copy")} - - -
    - )} +
    ); diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx index e66ad17cd..712844e18 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; @@ -48,8 +48,29 @@ export const ServiceTokenSection = withProjectPermission( return (
    -
    -

    Service Tokens

    +
    +
    +
    +

    Service Tokens

    + +
    + + Docs + +
    +
    +
    +

    + {t("section.token.service-tokens-description")} +

    +
    - Create token + Create Token )}
    -

    {t("section.token.service-tokens-description")}

    void; }; +enum TokensOrderBy { + Name = "name", + Expiration = "expiration" +} + export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); const { data, isPending } = useGetUserWsServiceTokens({ workspaceID: currentWorkspace?.id || "" }); + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection, + orderBy, + setOrderDirection, + setOrderBy + } = usePagination(TokensOrderBy.Name, { + initPerPage: getUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, newPerPage); + }; + + const filteredTokens = useMemo( + () => + data + ?.filter((token) => { + const { name, scopes } = token; + + const searchValue = search.trim().toLowerCase(); + + if (name.toLowerCase().includes(searchValue)) { + return true; + } + + return scopes.some( + ({ environment, secretPath }) => + environment.toLowerCase().includes(searchValue) || + secretPath.toLowerCase().includes(searchValue) + ); + }) + .sort((a, b) => { + const [tokenOne, tokenTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + switch (orderBy) { + case TokensOrderBy.Expiration: + if (!tokenOne.expiresAt && !tokenTwo.expiresAt) return 0; + if (!tokenOne.expiresAt) return 1; + if (!tokenTwo.expiresAt) return -1; + + return ( + new Date(tokenOne.expiresAt).getTime() - new Date(tokenTwo.expiresAt).getTime() + ); + + case TokensOrderBy.Name: + default: + return tokenOne.name.toLowerCase().localeCompare(tokenTwo.name.toLowerCase()); + } + }) ?? [], + [data, orderDirection, search, orderBy] + ); + + useResetPageHelper({ + totalCount: filteredTokens.length, + offset, + setPage + }); + + const handleSort = (column: TokensOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + const getClassName = (col: TokensOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + + const getColSortIcon = (col: TokensOrderBy) => + orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; + return ( - - - - - - - - - - - {isPending && } - {!isPending && - data && - data.map((row) => ( - - - - - - - ))} - {!isPending && data && data?.length === 0 && ( +
    + setSearch(e.target.value)} + leftIcon={} + placeholder="Search service tokens by name, environment or secret path..." + className="flex-1" + containerClassName="mb-4 mt-2" + /> + +
    Token NameEnvironment - Secret PathValid Until -
    {row.name} -
    - {row?.scopes.map(({ secretPath, environment }) => ( -
    -
    {environment}
    - - {secretPath} -
    - ))} -
    -
    {row.expiresAt && new Date(row.expiresAt).toUTCString()} - - {(isAllowed) => ( - - handlePopUpOpen("deleteAPITokenConfirmation", { - name: row.name, - id: row.id - }) - } - colorSchema="danger" - ariaLabel="delete" - isDisabled={!isAllowed} - > - - - )} - -
    + - + + + + - )} - -
    - - +
    + Name + handleSort(TokensOrderBy.Name)} + > + + +
    +
    Environment / Secret Path +
    + Valid Until + handleSort(TokensOrderBy.Expiration)} + > + + +
    +
    -
    + + + {isPending && } + {!isPending && + filteredTokens.slice(offset, perPage * page).map((row) => ( + + {row.name} + +
    + {row?.scopes.map(({ secretPath, environment }) => ( +
    +
    + {environment} +
    + + {secretPath} +
    + ))} +
    + + + {row.expiresAt ? ( + format(row.expiresAt, "MM/dd/yyyy h:mm:ss aa") + ) : ( + N/A + )} + + + + + + + + + + + + {(isAllowed) => ( + } + isDisabled={!isAllowed} + onClick={(e) => { + e.stopPropagation(); + handlePopUpOpen("deleteAPITokenConfirmation", { + name: row.name, + id: row.id + }); + }} + > + Delete Token + + )} + + + + + + + ))} + + + {Boolean(filteredTokens.length) && ( + + )} + {!isPending && !filteredTokens?.length && ( + + )} + +
    ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index d7872a223..471246d04 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -291,6 +291,13 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), + [ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]), @@ -342,7 +349,6 @@ export const projectRoleFormSchema = z.object({ .default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]), - [ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretScanningDataSources]: SecretScanningDataSourcePolicyActionSchema.array().default([]), @@ -366,7 +372,8 @@ type TConditionalFields = | ProjectPermissionSub.CertificateTemplates | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation - | ProjectPermissionSub.Identity; + | ProjectPermissionSub.Identity + | ProjectPermissionSub.SecretSyncs; export const isConditionalSubjects = ( subject: ProjectPermissionSub @@ -379,7 +386,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.SshHosts || subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.PkiSubscribers || - subject === ProjectPermissionSub.CertificateTemplates; + subject === ProjectPermissionSub.CertificateTemplates || + subject === ProjectPermissionSub.SecretSyncs; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -484,7 +492,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.SshCertificateTemplates, ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificates, - ProjectPermissionSub.SshHostGroups + ProjectPermissionSub.SshHostGroups, + ProjectPermissionSub.SecretSyncs ].includes(subject) ) { // from above statement we are sure it won't be undefined @@ -515,6 +524,36 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } + if (subject === ProjectPermissionSub.SecretSyncs) { + const canRead = action.includes(ProjectPermissionSecretSyncActions.Read); + const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit); + const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete); + const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create); + const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets); + const canImportSecrets = action.includes( + ProjectPermissionSecretSyncActions.ImportSecrets + ); + const canRemoveSecrets = action.includes( + ProjectPermissionSecretSyncActions.RemoveSecrets + ); + + if (!formVal[subject]) formVal[subject] = [{ conditions: [], inverted: false }]; + + // from above statement we are sure it won't be undefined + formVal[subject]!.push({ + [ProjectPermissionSecretSyncActions.Read]: canRead, + [ProjectPermissionSecretSyncActions.Create]: canCreate, + [ProjectPermissionSecretSyncActions.Edit]: canEdit, + [ProjectPermissionSecretSyncActions.Delete]: canDelete, + [ProjectPermissionSecretSyncActions.SyncSecrets]: canSyncSecrets, + [ProjectPermissionSecretSyncActions.ImportSecrets]: canImportSecrets, + [ProjectPermissionSecretSyncActions.RemoveSecrets]: canRemoveSecrets, + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + return; + } + if (subject === ProjectPermissionSub.DynamicSecrets) { const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential); const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential); @@ -777,31 +816,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } - if (subject === ProjectPermissionSub.SecretSyncs) { - const canRead = action.includes(ProjectPermissionSecretSyncActions.Read); - const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit); - const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete); - const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create); - const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets); - const canImportSecrets = action.includes(ProjectPermissionSecretSyncActions.ImportSecrets); - const canRemoveSecrets = action.includes(ProjectPermissionSecretSyncActions.RemoveSecrets); - - if (!formVal[subject]) formVal[subject] = [{}]; - - // from above statement we are sure it won't be undefined - if (canRead) formVal[subject]![0][ProjectPermissionSecretSyncActions.Read] = true; - if (canEdit) formVal[subject]![0][ProjectPermissionSecretSyncActions.Edit] = true; - if (canCreate) formVal[subject]![0][ProjectPermissionSecretSyncActions.Create] = true; - if (canDelete) formVal[subject]![0][ProjectPermissionSecretSyncActions.Delete] = true; - if (canSyncSecrets) - formVal[subject]![0][ProjectPermissionSecretSyncActions.SyncSecrets] = true; - if (canImportSecrets) - formVal[subject]![0][ProjectPermissionSecretSyncActions.ImportSecrets] = true; - if (canRemoveSecrets) - formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true; - return; - } - if (subject === ProjectPermissionSub.SecretScanningDataSources) { const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read); const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 31f63ac51..09f3920fd 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -35,6 +35,7 @@ import { TFormSchema } from "./ProjectRoleModifySection.utils"; import { SecretPermissionConditions } from "./SecretPermissionConditions"; +import { SecretSyncPermissionConditions } from "./SecretSyncPermissionConditions"; import { SshHostPermissionConditions } from "./SshHostPermissionConditions"; type Props = { @@ -69,6 +70,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.SecretSyncs) { + return ; + } + return ; } diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/SecretSyncPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/SecretSyncPermissionConditions.tsx new file mode 100644 index 000000000..b5f8f5c91 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/SecretSyncPermissionConditions.tsx @@ -0,0 +1,186 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { + getConditionOperatorHelperInfo, + renderOperatorSelectItems +} from "./PermissionConditionHelpers"; +import { TFormSchema } from "./ProjectRoleModifySection.utils"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => { + const { + control, + watch, + setValue, + formState: { errors } + } = useFormContext(); + const items = useFieldArray({ + control, + name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions` + }); + + const conditionErrorMessage = + errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message || + errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message; + + return ( +
    +

    Conditions

    +

    + Conditions determine when a policy will be applied (always if no conditions are present). +

    +

    + All conditions must evaluate to true for the policy to take effect. +

    +
    + {items.fields.map((el, index) => { + const condition = watch( + `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}` + ) as { + lhs: string; + rhs: string; + operator: string; + }; + return ( +
    +
    + ( + + + + )} + /> +
    +
    + ( + + + + )} + /> +
    + + + +
    +
    +
    + ( + + + + )} + /> +
    +
    + items.remove(index)} + > + + +
    +
    + ); + })} +
    + {conditionErrorMessage && ( +
    + + {conditionErrorMessage} +
    + )} +
    + +
    +
    + ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/FlyioSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/FlyioSyncDestinationCol.tsx new file mode 100644 index 000000000..4b69e8348 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/FlyioSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TFlyioSync } from "@app/hooks/api/secretSyncs/types/flyio-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TFlyioSync; +}; + +export const FlyioSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 5e0768599..ca7b37682 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; +import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; @@ -61,6 +62,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Render: return ; + case SecretSync.Flyio: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx index ddc74b128..3cec5694c 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncRow.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo } from "react"; +import { subject } from "@casl/ability"; import { faBan, faCalendarCheck, @@ -117,6 +118,14 @@ export const SecretSyncRow = ({ const destinationDetails = SECRET_SYNC_MAP[destination]; + const permissionSubject = + environment && folder + ? subject(ProjectPermissionSub.SecretSyncs, { + environment: environment.slug, + secretPath: folder.path + }) + : ProjectPermissionSub.SecretSyncs; + return ( @@ -264,7 +273,7 @@ export const SecretSyncRow = ({ {(isAllowed: boolean) => ( {(isAllowed: boolean) => ( {(isAllowed: boolean) => ( {(isAllowed: boolean) => ( {(isAllowed: boolean) => ( { primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId; secondaryText = "Service"; break; + case SecretSync.Flyio: + primaryText = destinationConfig.appId; + secondaryText = "App ID"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index c94aef0d5..f34c8695e 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -96,6 +96,10 @@ import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/Overv import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm"; import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm"; +import { + HIDDEN_SECRET_VALUE, + HIDDEN_SECRET_VALUE_API_MASK +} from "../SecretDashboardPage/components/SecretListView/SecretItem"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow"; @@ -509,15 +513,25 @@ export const OverviewPage = () => { env: string, key: string, value: string, + secretValueHidden: boolean, type = SecretType.Shared ) => { + let secretValue: string | undefined = value; + + if ( + secretValueHidden && + (value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE) + ) { + secretValue = undefined; + } + try { const result = await updateSecretV3({ environment: env, workspaceId, secretPath, secretKey: key, - secretValue: value, + secretValue, type }); diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 72eb085c0..4b07e6242 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -50,6 +50,7 @@ type Props = { env: string, key: string, value: string, + secretValueHidden: boolean, type?: SecretType, secretId?: string ) => Promise; @@ -147,6 +148,7 @@ export const SecretEditRow = ({ environment, secretName, value, + secretValueHidden, isOverride ? SecretType.Personal : SecretType.Shared, secretId ); @@ -166,6 +168,7 @@ export const SecretEditRow = ({ environment, secretName, secretValue, + secretValueHidden, isOverride ? SecretType.Personal : SecretType.Shared, secretId ); @@ -257,7 +260,7 @@ export const SecretEditRow = ({ > {(isAllowed) => (
    - + Promise; @@ -96,7 +98,7 @@ export const SecretOverviewTableRow = ({ ); if (secret?.secretValueHidden && !secret?.valueOverride) { - return canEditSecretValue ? "******" : ""; + return canEditSecretValue ? HIDDEN_SECRET_VALUE : ""; } return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || ""; }; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 60e916fdb..3720b63dc 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -11,6 +11,7 @@ import { faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { SecretInput, Tag, Tooltip } from "@app/components/v2"; import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types"; @@ -105,21 +106,41 @@ export const SecretApprovalRequestChangeItem = ({ ) : (
    + {secretVersion?.secretValueHidden && ( +
    + + + +
    + )} -
    setIsOldSecretValueVisible(!isOldSecretValueVisible)} - > - -
    + {!secretVersion?.secretValueHidden && ( +
    setIsOldSecretValueVisible(!isOldSecretValueVisible)} + > + +
    + )}
    )}
    @@ -211,21 +232,41 @@ export const SecretApprovalRequestChangeItem = ({ ) : (
    + {newVersion?.secretValueHidden && ( +
    + + + +
    + )} -
    setIsNewSecretValueVisible(!isNewSecretValueVisible)} - > - -
    + {!newVersion?.secretValueHidden && ( +
    setIsNewSecretValueVisible(!isNewSecretValueVisible)} + > + +
    + )}
    )}
    diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d417821e4..5180383bb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -62,6 +62,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { camelCaseToSpaces } from "@app/lib/fn/string"; import { CreateReminderForm } from "./CreateReminderForm"; +import { HIDDEN_SECRET_VALUE } from "./SecretItem"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; type Props = { @@ -897,7 +898,9 @@ export const SecretDetailSidebar = ({
    - {secretValueHidden ? "******" : secretValue?.replace(/./g, "*")} + {secretValueHidden + ? HIDDEN_SECRET_VALUE + : secretValue?.replace(/./g, "*")}