mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 14:28:20 +00:00
partial factory, endpoint, and service implementation
This commit is contained in:
Vendored
+2
@@ -55,6 +55,7 @@ import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
|||||||
import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service";
|
import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TApprovalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service";
|
||||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
@@ -361,6 +362,7 @@ declare module "fastify" {
|
|||||||
convertor: TConvertorServiceFactory;
|
convertor: TConvertorServiceFactory;
|
||||||
subOrganization: TSubOrgServiceFactory;
|
subOrganization: TSubOrgServiceFactory;
|
||||||
pkiAlertV2: TPkiAlertV2ServiceFactory;
|
pkiAlertV2: TPkiAlertV2ServiceFactory;
|
||||||
|
approvalPolicy: TApprovalPolicyServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+25
-25
@@ -26,6 +26,30 @@ import {
|
|||||||
TAppConnections,
|
TAppConnections,
|
||||||
TAppConnectionsInsert,
|
TAppConnectionsInsert,
|
||||||
TAppConnectionsUpdate,
|
TAppConnectionsUpdate,
|
||||||
|
TApprovalPolicies,
|
||||||
|
TApprovalPoliciesInsert,
|
||||||
|
TApprovalPoliciesUpdate,
|
||||||
|
TApprovalPolicyStepApprovers,
|
||||||
|
TApprovalPolicyStepApproversInsert,
|
||||||
|
TApprovalPolicyStepApproversUpdate,
|
||||||
|
TApprovalPolicySteps,
|
||||||
|
TApprovalPolicyStepsInsert,
|
||||||
|
TApprovalPolicyStepsUpdate,
|
||||||
|
TApprovalRequestApprovals,
|
||||||
|
TApprovalRequestApprovalsInsert,
|
||||||
|
TApprovalRequestApprovalsUpdate,
|
||||||
|
TApprovalRequestGrants,
|
||||||
|
TApprovalRequestGrantsInsert,
|
||||||
|
TApprovalRequestGrantsUpdate,
|
||||||
|
TApprovalRequests,
|
||||||
|
TApprovalRequestsInsert,
|
||||||
|
TApprovalRequestStepEligibleApprovers,
|
||||||
|
TApprovalRequestStepEligibleApproversInsert,
|
||||||
|
TApprovalRequestStepEligibleApproversUpdate,
|
||||||
|
TApprovalRequestSteps,
|
||||||
|
TApprovalRequestStepsInsert,
|
||||||
|
TApprovalRequestStepsUpdate,
|
||||||
|
TApprovalRequestsUpdate,
|
||||||
TAuditLogs,
|
TAuditLogs,
|
||||||
TAuditLogsInsert,
|
TAuditLogsInsert,
|
||||||
TAuditLogStreams,
|
TAuditLogStreams,
|
||||||
@@ -571,31 +595,7 @@ import {
|
|||||||
TWebhooksUpdate,
|
TWebhooksUpdate,
|
||||||
TWorkflowIntegrations,
|
TWorkflowIntegrations,
|
||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate,
|
TWorkflowIntegrationsUpdate
|
||||||
TApprovalPolicies,
|
|
||||||
TApprovalPoliciesInsert,
|
|
||||||
TApprovalPoliciesUpdate,
|
|
||||||
TApprovalPolicyStepApprovers,
|
|
||||||
TApprovalPolicyStepApproversInsert,
|
|
||||||
TApprovalPolicyStepApproversUpdate,
|
|
||||||
TApprovalPolicySteps,
|
|
||||||
TApprovalPolicyStepsInsert,
|
|
||||||
TApprovalPolicyStepsUpdate,
|
|
||||||
TApprovalRequestApprovals,
|
|
||||||
TApprovalRequestApprovalsInsert,
|
|
||||||
TApprovalRequestApprovalsUpdate,
|
|
||||||
TApprovalRequestGrants,
|
|
||||||
TApprovalRequestGrantsInsert,
|
|
||||||
TApprovalRequestGrantsUpdate,
|
|
||||||
TApprovalRequestStepEligibleApprovers,
|
|
||||||
TApprovalRequestStepEligibleApproversInsert,
|
|
||||||
TApprovalRequestStepEligibleApproversUpdate,
|
|
||||||
TApprovalRequestSteps,
|
|
||||||
TApprovalRequestStepsInsert,
|
|
||||||
TApprovalRequestStepsUpdate,
|
|
||||||
TApprovalRequests,
|
|
||||||
TApprovalRequestsInsert,
|
|
||||||
TApprovalRequestsUpdate
|
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
TCertificateRequests,
|
TCertificateRequests,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ export * from "./access-approval-requests-reviewers";
|
|||||||
export * from "./additional-privileges";
|
export * from "./additional-privileges";
|
||||||
export * from "./api-keys";
|
export * from "./api-keys";
|
||||||
export * from "./app-connections";
|
export * from "./app-connections";
|
||||||
|
export * from "./approval-policies";
|
||||||
|
export * from "./approval-policy-step-approvers";
|
||||||
|
export * from "./approval-policy-steps";
|
||||||
|
export * from "./approval-request-approvals";
|
||||||
|
export * from "./approval-request-grants";
|
||||||
|
export * from "./approval-request-step-eligible-approvers";
|
||||||
|
export * from "./approval-request-steps";
|
||||||
|
export * from "./approval-requests";
|
||||||
export * from "./audit-log-streams";
|
export * from "./audit-log-streams";
|
||||||
export * from "./audit-logs";
|
export * from "./audit-logs";
|
||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
@@ -195,11 +203,3 @@ export * from "./users";
|
|||||||
export * from "./vault-external-migration-configs";
|
export * from "./vault-external-migration-configs";
|
||||||
export * from "./webhooks";
|
export * from "./webhooks";
|
||||||
export * from "./workflow-integrations";
|
export * from "./workflow-integrations";
|
||||||
export * from "./approval-policies";
|
|
||||||
export * from "./approval-policy-step-approvers";
|
|
||||||
export * from "./approval-policy-steps";
|
|
||||||
export * from "./approval-request-approvals";
|
|
||||||
export * from "./approval-request-grants";
|
|
||||||
export * from "./approval-request-step-eligible-approvers";
|
|
||||||
export * from "./approval-request-steps";
|
|
||||||
export * from "./approval-requests";
|
|
||||||
|
|||||||
@@ -10,9 +10,21 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
DatabaseError,
|
||||||
|
ForbiddenRequestError,
|
||||||
|
NotFoundError,
|
||||||
|
PolicyViolationError
|
||||||
|
} from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import {
|
||||||
|
TApprovalPolicyDALFactory,
|
||||||
|
TApprovalRequestGrantsDALFactory
|
||||||
|
} from "@app/services/approval-policy/approval-policy-dal";
|
||||||
|
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||||
|
import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
@@ -51,6 +63,8 @@ type TPamAccountServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
|
approvalPolicyDAL: TApprovalPolicyDALFactory;
|
||||||
|
approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory;
|
||||||
};
|
};
|
||||||
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
||||||
|
|
||||||
@@ -67,7 +81,9 @@ export const pamAccountServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
auditLogService
|
auditLogService,
|
||||||
|
approvalPolicyDAL,
|
||||||
|
approvalRequestGrantsDAL
|
||||||
}: TPamAccountServiceFactoryDep) => {
|
}: TPamAccountServiceFactoryDep) => {
|
||||||
const create = async (
|
const create = async (
|
||||||
{
|
{
|
||||||
@@ -531,23 +547,50 @@ export const pamAccountServiceFactory = ({
|
|||||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const fac = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.PamAccess](ApprovalPolicyType.PamAccess);
|
||||||
actor: actor.type,
|
|
||||||
actorAuthMethod: actor.authMethod,
|
|
||||||
actorId: actor.id,
|
|
||||||
actorOrgId: actor.orgId,
|
|
||||||
projectId,
|
|
||||||
actionProjectType: ActionProjectType.PAM
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
const inputs = {
|
||||||
ProjectPermissionPamAccountActions.Access,
|
resourceId: resource.id,
|
||||||
subject(ProjectPermissionSub.PamAccounts, {
|
accountPath: folderPath
|
||||||
resourceName: resource.name,
|
};
|
||||||
accountName: account.name,
|
|
||||||
accountPath: folderPath
|
const canAccess = await fac.canAccess(approvalRequestGrantsDAL, actor.id, resource.projectId, inputs);
|
||||||
})
|
|
||||||
);
|
if (canAccess) {
|
||||||
|
// Grant exists, allow access without checking permission
|
||||||
|
} else {
|
||||||
|
const policy = await fac.matchPolicy(approvalPolicyDAL, resource.projectId, inputs);
|
||||||
|
|
||||||
|
if (policy) {
|
||||||
|
throw new PolicyViolationError({
|
||||||
|
message: "A policy is in place for this resource",
|
||||||
|
details: {
|
||||||
|
policyId: policy.id,
|
||||||
|
policyName: policy.name,
|
||||||
|
policyType: policy.type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there isn't a policy in place, continue with checking permission
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
projectId: account.projectId,
|
||||||
|
actionProjectType: ActionProjectType.PAM
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionPamAccountActions.Access,
|
||||||
|
subject(ProjectPermissionSub.PamAccounts, {
|
||||||
|
resourceName: resource.name,
|
||||||
|
accountName: account.name,
|
||||||
|
accountPath: folderPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const session = await pamSessionDAL.create({
|
const session = await pamSessionDAL.create({
|
||||||
accountName: account.name,
|
accountName: account.name,
|
||||||
|
|||||||
@@ -183,3 +183,23 @@ export class CryptographyError extends Error {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PolicyViolationError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
message,
|
||||||
|
details
|
||||||
|
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
|
||||||
|
super(message || "A policy is in place for this resource");
|
||||||
|
this.name = name || "PolicyViolationError";
|
||||||
|
this.error = error;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
NotFoundError,
|
NotFoundError,
|
||||||
OidcAuthError,
|
OidcAuthError,
|
||||||
PermissionBoundaryError,
|
PermissionBoundaryError,
|
||||||
|
PolicyViolationError,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
ScimRequestError,
|
ScimRequestError,
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
@@ -255,6 +256,14 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
detail: error.message
|
detail: error.message
|
||||||
// TODO: add subproblems if they exist
|
// TODO: add subproblems if they exist
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof PolicyViolationError) {
|
||||||
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
reqId: req.id,
|
||||||
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
|
error: "PolicyViolationError",
|
||||||
|
message: error.message,
|
||||||
|
details: error.details
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
|
|||||||
@@ -159,6 +159,13 @@ import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
|||||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import {
|
||||||
|
approvalPolicyDALFactory,
|
||||||
|
approvalPolicyStepApproversDALFactory,
|
||||||
|
approvalPolicyStepsDALFactory,
|
||||||
|
approvalRequestGrantsDALFactory
|
||||||
|
} from "@app/services/approval-policy/approval-policy-dal";
|
||||||
|
import { approvalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service";
|
||||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
@@ -2412,6 +2419,9 @@ export const registerRoutes = async (
|
|||||||
gatewayV2Service
|
gatewayV2Service
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const approvalRequestGrantsDAL = approvalRequestGrantsDALFactory(db);
|
||||||
|
const approvalPolicyDAL = approvalPolicyDALFactory(db);
|
||||||
|
|
||||||
const pamAccountService = pamAccountServiceFactory({
|
const pamAccountService = pamAccountServiceFactory({
|
||||||
pamAccountDAL,
|
pamAccountDAL,
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
@@ -2423,7 +2433,9 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
auditLogService
|
auditLogService,
|
||||||
|
approvalRequestGrantsDAL,
|
||||||
|
approvalPolicyDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pamAccountRotation = pamAccountRotationServiceFactory({
|
const pamAccountRotation = pamAccountRotationServiceFactory({
|
||||||
@@ -2451,6 +2463,17 @@ export const registerRoutes = async (
|
|||||||
auditLogService
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const approvalPolicyStepsDAL = approvalPolicyStepsDALFactory(db);
|
||||||
|
const approvalPolicyStepApproversDAL = approvalPolicyStepApproversDALFactory(db);
|
||||||
|
|
||||||
|
const approvalPolicyService = approvalPolicyServiceFactory({
|
||||||
|
approvalPolicyDAL,
|
||||||
|
approvalPolicyStepsDAL,
|
||||||
|
approvalPolicyStepApproversDAL,
|
||||||
|
projectDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
await licenseService.init();
|
await licenseService.init();
|
||||||
|
|
||||||
@@ -2630,7 +2653,8 @@ export const registerRoutes = async (
|
|||||||
additionalPrivilege: additionalPrivilegeService,
|
additionalPrivilege: additionalPrivilegeService,
|
||||||
identityProject: identityProjectService,
|
identityProject: identityProjectService,
|
||||||
convertor: convertorService,
|
convertor: convertorService,
|
||||||
pkiAlertV2: pkiAlertV2Service
|
pkiAlertV2: pkiAlertV2Service,
|
||||||
|
approvalPolicy: approvalPolicyService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
|||||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
|
||||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||||
|
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||||
|
import {
|
||||||
|
TApprovalPolicy,
|
||||||
|
TCreatePolicyDTO,
|
||||||
|
TUpdatePolicyDTO
|
||||||
|
} from "@app/services/approval-policy/approval-policy-types";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||||
|
server,
|
||||||
|
policyType,
|
||||||
|
createPolicySchema,
|
||||||
|
updatePolicySchema,
|
||||||
|
policyResponseSchema
|
||||||
|
}: {
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
policyType: ApprovalPolicyType;
|
||||||
|
createPolicySchema: z.ZodType<
|
||||||
|
TCreatePolicyDTO & {
|
||||||
|
conditions: P["conditions"]["conditions"];
|
||||||
|
constraints: P["constraints"]["constraints"];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
updatePolicySchema: z.ZodType<
|
||||||
|
TUpdatePolicyDTO & {
|
||||||
|
conditions?: P["conditions"]["conditions"];
|
||||||
|
constraints?: P["constraints"]["constraints"];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
policyResponseSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Create approval policy",
|
||||||
|
body: createPolicySchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
policy: policyResponseSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const policy = await server.services.approvalPolicy.create(policyType, req.body, req.permission);
|
||||||
|
|
||||||
|
// TODO: Audit log
|
||||||
|
|
||||||
|
return { policy };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:policyId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Update approval policy",
|
||||||
|
params: z.object({
|
||||||
|
policyId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: updatePolicySchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
policy: policyResponseSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const policy = await server.services.approvalPolicy.updateById(req.params.policyId, req.body, req.permission);
|
||||||
|
|
||||||
|
// TODO: Audit log
|
||||||
|
|
||||||
|
return { policy };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:policyId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Delete approval policy",
|
||||||
|
params: z.object({
|
||||||
|
policyId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
policy: policyResponseSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const policy = await server.services.approvalPolicy.deleteById(req.params.policyId, req.permission);
|
||||||
|
|
||||||
|
// TODO: Audit log
|
||||||
|
|
||||||
|
return { policy };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||||
|
import {
|
||||||
|
CreatePamAccessPolicySchema,
|
||||||
|
PamAccessPolicySchema,
|
||||||
|
UpdatePamAccessPolicySchema
|
||||||
|
} from "@app/services/approval-policy/pam-access/pam-access-policy-schemas";
|
||||||
|
|
||||||
|
import { registerApprovalPolicyEndpoints } from "./approval-policy-endpoints";
|
||||||
|
|
||||||
|
export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record<
|
||||||
|
ApprovalPolicyType,
|
||||||
|
(server: FastifyZodProvider) => Promise<void>
|
||||||
|
> = {
|
||||||
|
[ApprovalPolicyType.PamAccess]: async (server: FastifyZodProvider) => {
|
||||||
|
registerApprovalPolicyEndpoints({
|
||||||
|
server,
|
||||||
|
policyType: ApprovalPolicyType.PamAccess,
|
||||||
|
createPolicySchema: CreatePamAccessPolicySchema,
|
||||||
|
updatePolicySchema: UpdatePamAccessPolicySchema,
|
||||||
|
policyResponseSchema: PamAccessPolicySchema
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"
|
|||||||
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
|
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
|
||||||
|
|
||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
|
import { APPROVAL_POLICY_REGISTER_ROUTER_MAP } from "./approval-policy-routers";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
import { registerCaRouter } from "./certificate-authority-router";
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
@@ -275,4 +276,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.register(registerEventRouter, { prefix: "/events" });
|
await server.register(registerEventRouter, { prefix: "/events" });
|
||||||
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
|
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (approvalPolicyRouter) => {
|
||||||
|
// Register policy type-specific endpoints
|
||||||
|
for await (const [type, router] of Object.entries(APPROVAL_POLICY_REGISTER_ROUTER_MAP)) {
|
||||||
|
await approvalPolicyRouter.register(router, { prefix: `/${type}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/approval-policies" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
|||||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
|
||||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
|
||||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||||
|
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||||
|
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||||
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
|
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
|
||||||
import { flyioConnectionService } from "./flyio/flyio-connection-service";
|
import { flyioConnectionService } from "./flyio/flyio-connection-service";
|
||||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
TOracleDBConnectionInput,
|
TOracleDBConnectionInput,
|
||||||
TValidateOracleDBConnectionCredentialsSchema
|
TValidateOracleDBConnectionCredentialsSchema
|
||||||
} from "@app/ee/services/app-connections/oracledb";
|
} from "@app/ee/services/app-connections/oracledb";
|
||||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
|
||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
|
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
// Approval Policy
|
||||||
|
export type TApprovalPolicyDALFactory = ReturnType<typeof approvalPolicyDALFactory>;
|
||||||
|
export const approvalPolicyDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ApprovalPolicies);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...orm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Approval Policy Steps
|
||||||
|
export type TApprovalPolicyStepsDALFactory = ReturnType<typeof approvalPolicyStepsDALFactory>;
|
||||||
|
export const approvalPolicyStepsDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ApprovalPolicySteps);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Approval Policy Step Approvers
|
||||||
|
export type TApprovalPolicyStepApproversDALFactory = ReturnType<typeof approvalPolicyStepApproversDALFactory>;
|
||||||
|
export const approvalPolicyStepApproversDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ApprovalPolicyStepApprovers);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Approval Policy Grants
|
||||||
|
export type TApprovalRequestGrantsDALFactory = ReturnType<typeof approvalRequestGrantsDALFactory>;
|
||||||
|
export const approvalRequestGrantsDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ApprovalRequestGrants);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export enum ApprovalPolicyType {
|
||||||
|
PamAccess = "pam-access"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ApproverType {
|
||||||
|
Group = "group",
|
||||||
|
User = "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ApprovalRequestGrantStatus {
|
||||||
|
Active = "active",
|
||||||
|
Expired = "expired",
|
||||||
|
Revoked = "revoked"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ApprovalPolicyType } from "./approval-policy-enums";
|
||||||
|
import { TApprovalPolicy, TApprovalPolicyInputs, TApprovalResourceFactory } from "./approval-policy-types";
|
||||||
|
import { pamAccessPolicyFactory } from "./pam-access/pam-access-policy-factory";
|
||||||
|
|
||||||
|
type TApprovalPolicyFactoryImplementation = TApprovalResourceFactory<TApprovalPolicyInputs, TApprovalPolicy>;
|
||||||
|
|
||||||
|
export const APPROVAL_POLICY_FACTORY_MAP: Record<ApprovalPolicyType, TApprovalPolicyFactoryImplementation> = {
|
||||||
|
[ApprovalPolicyType.PamAccess]: pamAccessPolicyFactory as TApprovalPolicyFactoryImplementation
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ApprovalPoliciesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { ApproverType } from "./approval-policy-enums";
|
||||||
|
|
||||||
|
export const BaseApprovalPolicySchema = ApprovalPoliciesSchema;
|
||||||
|
|
||||||
|
export const BaseCreateApprovalPolicySchema = z.object({
|
||||||
|
projectId: z.string().uuid(),
|
||||||
|
organizationId: z.string().uuid(),
|
||||||
|
name: z.string().min(1).max(128),
|
||||||
|
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), // 1 hour to 30 days
|
||||||
|
steps: z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).max(128).nullable().optional(),
|
||||||
|
requiredApprovals: z.number().min(1).max(100),
|
||||||
|
notifyApprovers: z.boolean().optional(),
|
||||||
|
approvers: z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(ApproverType),
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseUpdateApprovalPolicySchema = z.object({
|
||||||
|
name: z.string().min(1).max(128).optional(),
|
||||||
|
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), // 1 hour to 30 days
|
||||||
|
steps: z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).max(128).nullable().optional(),
|
||||||
|
requiredApprovals: z.number().min(1).max(100),
|
||||||
|
notifyApprovers: z.boolean().optional(),
|
||||||
|
approvers: z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(ApproverType),
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TApprovalPolicyDALFactory,
|
||||||
|
TApprovalPolicyStepApproversDALFactory,
|
||||||
|
TApprovalPolicyStepsDALFactory
|
||||||
|
} from "./approval-policy-dal";
|
||||||
|
import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums";
|
||||||
|
import { TCreatePolicyDTO, TUpdatePolicyDTO } from "./approval-policy-types";
|
||||||
|
|
||||||
|
type TApprovalPolicyServiceFactoryDep = {
|
||||||
|
approvalPolicyDAL: TApprovalPolicyDALFactory;
|
||||||
|
approvalPolicyStepsDAL: TApprovalPolicyStepsDALFactory;
|
||||||
|
approvalPolicyStepApproversDAL: TApprovalPolicyStepApproversDALFactory;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
};
|
||||||
|
export type TApprovalPolicyServiceFactory = ReturnType<typeof approvalPolicyServiceFactory>;
|
||||||
|
|
||||||
|
export const approvalPolicyServiceFactory = ({
|
||||||
|
approvalPolicyDAL,
|
||||||
|
approvalPolicyStepsDAL,
|
||||||
|
approvalPolicyStepApproversDAL,
|
||||||
|
permissionService
|
||||||
|
}: TApprovalPolicyServiceFactoryDep) => {
|
||||||
|
const create = async (
|
||||||
|
policyType: ApprovalPolicyType,
|
||||||
|
{ projectId, organizationId, name, maxRequestTtlSeconds, conditions, constraints, steps }: TCreatePolicyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const { hasRole } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
projectId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = await approvalPolicyDAL.transaction(async (tx) => {
|
||||||
|
const newPolicy = await approvalPolicyDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
name,
|
||||||
|
maxRequestTtlSeconds,
|
||||||
|
conditions: { version: 1, conditions },
|
||||||
|
constraints: { version: 1, constraints },
|
||||||
|
type: policyType
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create policy steps and their approvers
|
||||||
|
await Promise.all(
|
||||||
|
steps.map(async (step, i) => {
|
||||||
|
const newStep = await approvalPolicyStepsDAL.create(
|
||||||
|
{
|
||||||
|
policyId: newPolicy.id,
|
||||||
|
requiredApprovals: step.requiredApprovals,
|
||||||
|
stepNumber: i + 1,
|
||||||
|
name: step.name,
|
||||||
|
notifyApprovers: step.notifyApprovers
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (step.approvers?.length) {
|
||||||
|
await Promise.all(
|
||||||
|
step.approvers.map((approver) =>
|
||||||
|
approvalPolicyStepApproversDAL.create(
|
||||||
|
{
|
||||||
|
policyStepId: newStep.id,
|
||||||
|
userId: approver.type === ApproverType.User ? approver.id : null,
|
||||||
|
groupId: approver.type === ApproverType.Group ? approver.id : null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return newPolicy;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async (
|
||||||
|
policyId: string,
|
||||||
|
{ name, maxRequestTtlSeconds, conditions, constraints, steps }: TUpdatePolicyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const policy = await approvalPolicyDAL.findById(policyId);
|
||||||
|
if (!policy) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Policy not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasRole } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
projectId: policy.projectId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPolicy = await approvalPolicyDAL.transaction(async (tx) => {
|
||||||
|
const updateDoc: Partial<TApprovalPolicies> = {};
|
||||||
|
|
||||||
|
if (name !== undefined) {
|
||||||
|
updateDoc.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxRequestTtlSeconds !== undefined) {
|
||||||
|
updateDoc.maxRequestTtlSeconds = maxRequestTtlSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions !== undefined) {
|
||||||
|
updateDoc.conditions = { version: 1, conditions };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints !== undefined) {
|
||||||
|
updateDoc.constraints = { version: 1, constraints };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await approvalPolicyDAL.updateById(policyId, updateDoc, tx);
|
||||||
|
|
||||||
|
if (steps !== undefined) {
|
||||||
|
await approvalPolicyStepsDAL.delete({ policyId }, tx);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
steps.map(async (step, i) => {
|
||||||
|
const newStep = await approvalPolicyStepsDAL.create(
|
||||||
|
{
|
||||||
|
policyId,
|
||||||
|
requiredApprovals: step.requiredApprovals,
|
||||||
|
stepNumber: i + 1,
|
||||||
|
name: step.name,
|
||||||
|
notifyApprovers: step.notifyApprovers
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (step.approvers?.length) {
|
||||||
|
await Promise.all(
|
||||||
|
step.approvers.map((approver) =>
|
||||||
|
approvalPolicyStepApproversDAL.create(
|
||||||
|
{
|
||||||
|
policyStepId: newStep.id,
|
||||||
|
userId: approver.type === ApproverType.User ? approver.id : null,
|
||||||
|
groupId: approver.type === ApproverType.Group ? approver.id : null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy: updatedPolicy
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async (policyId: string, actor: OrgServiceActor) => {
|
||||||
|
const policy = await approvalPolicyDAL.findById(policyId);
|
||||||
|
if (!policy) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Policy not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasRole } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
projectId: policy.projectId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||||
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedPolicy = await approvalPolicyDAL.deleteById(policyId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy: deletedPolicy
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
updateById,
|
||||||
|
deleteById
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { TApprovalPolicyDALFactory, TApprovalRequestGrantsDALFactory } from "./approval-policy-dal";
|
||||||
|
import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums";
|
||||||
|
import {
|
||||||
|
TPamAccessPolicy,
|
||||||
|
TPamAccessPolicyConditions,
|
||||||
|
TPamAccessPolicyConstraints,
|
||||||
|
TPamAccessPolicyInputs
|
||||||
|
} from "./pam-access/pam-access-policy-types";
|
||||||
|
|
||||||
|
export type TApprovalPolicy = TPamAccessPolicy;
|
||||||
|
export type TApprovalPolicyInputs = TPamAccessPolicyInputs;
|
||||||
|
export type TApprovalPolicyConditions = TPamAccessPolicyConditions;
|
||||||
|
export type TApprovalPolicyConstraints = TPamAccessPolicyConstraints;
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
export interface TCreatePolicyDTO {
|
||||||
|
projectId: TApprovalPolicy["projectId"];
|
||||||
|
organizationId: TApprovalPolicy["organizationId"];
|
||||||
|
name: TApprovalPolicy["name"];
|
||||||
|
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"];
|
||||||
|
conditions: TApprovalPolicy["conditions"]["conditions"];
|
||||||
|
constraints: TApprovalPolicy["constraints"]["constraints"];
|
||||||
|
steps: {
|
||||||
|
name?: string | null;
|
||||||
|
requiredApprovals: number;
|
||||||
|
notifyApprovers?: boolean;
|
||||||
|
approvers: {
|
||||||
|
type: ApproverType.User | ApproverType.Group;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TUpdatePolicyDTO {
|
||||||
|
name?: TApprovalPolicy["name"];
|
||||||
|
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"];
|
||||||
|
conditions?: TApprovalPolicy["conditions"]["conditions"];
|
||||||
|
constraints?: TApprovalPolicy["constraints"]["constraints"];
|
||||||
|
steps?: {
|
||||||
|
name?: string | null;
|
||||||
|
requiredApprovals: number;
|
||||||
|
notifyApprovers?: boolean;
|
||||||
|
approvers: {
|
||||||
|
type: ApproverType.User | ApproverType.Group;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory
|
||||||
|
export type TApprovalRequestFactoryMatchPolicy<I extends TApprovalPolicyInputs, P extends TApprovalPolicy> = (
|
||||||
|
approvalPolicyDAL: TApprovalPolicyDALFactory,
|
||||||
|
projectId: string,
|
||||||
|
inputs: I
|
||||||
|
) => Promise<P | null>;
|
||||||
|
export type TApprovalRequestFactoryCanAccess<I extends TApprovalPolicyInputs> = (
|
||||||
|
approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory,
|
||||||
|
projectId: string,
|
||||||
|
userId: string,
|
||||||
|
inputs: I
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
export type TApprovalResourceFactory<I extends TApprovalPolicyInputs, P extends TApprovalPolicy> = (
|
||||||
|
policyType: ApprovalPolicyType
|
||||||
|
) => {
|
||||||
|
matchPolicy: TApprovalRequestFactoryMatchPolicy<I, P>;
|
||||||
|
canAccess: TApprovalRequestFactoryCanAccess<I>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
import { ApprovalRequestGrantStatus } from "../approval-policy-enums";
|
||||||
|
import {
|
||||||
|
TApprovalRequestFactoryCanAccess,
|
||||||
|
TApprovalRequestFactoryMatchPolicy,
|
||||||
|
TApprovalResourceFactory
|
||||||
|
} from "../approval-policy-types";
|
||||||
|
import { TPamAccessPolicy, TPamAccessPolicyInputs } from "./pam-access-policy-types";
|
||||||
|
|
||||||
|
export const pamAccessPolicyFactory: TApprovalResourceFactory<TPamAccessPolicyInputs, TPamAccessPolicy> = (
|
||||||
|
policyType
|
||||||
|
) => {
|
||||||
|
const matchPolicy: TApprovalRequestFactoryMatchPolicy<TPamAccessPolicyInputs, TPamAccessPolicy> = async (
|
||||||
|
approvalPolicyDAL,
|
||||||
|
projectId,
|
||||||
|
inputs
|
||||||
|
) => {
|
||||||
|
const policies = await approvalPolicyDAL.find({
|
||||||
|
type: policyType,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
let bestMatch: { policy: TPamAccessPolicy; wildcardCount: number; pathLength: number } | null = null;
|
||||||
|
|
||||||
|
for (const policy of policies) {
|
||||||
|
const p = policy as TPamAccessPolicy;
|
||||||
|
for (const c of p.conditions.conditions) {
|
||||||
|
if (!c.targetResources.some((r) => r === inputs.resourceId)) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most specific path pattern
|
||||||
|
// TODO: Make matching logic more advanced by accounting for wildcard positions
|
||||||
|
for (const pathPattern of c.accountPaths) {
|
||||||
|
if (picomatch(pathPattern)(inputs.accountPath)) {
|
||||||
|
const wildcardCount = (pathPattern.match(/\*/g) || []).length;
|
||||||
|
const pathLength = pathPattern.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!bestMatch ||
|
||||||
|
wildcardCount < bestMatch.wildcardCount ||
|
||||||
|
(wildcardCount === bestMatch.wildcardCount && pathLength > bestMatch.pathLength)
|
||||||
|
) {
|
||||||
|
bestMatch = { policy: p, wildcardCount, pathLength };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch?.policy || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAccess: TApprovalRequestFactoryCanAccess<TPamAccessPolicyInputs> = async (
|
||||||
|
approvalRequestGrantsDAL,
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
inputs
|
||||||
|
) => {
|
||||||
|
const grants = await approvalRequestGrantsDAL.find({
|
||||||
|
granteeUserId: userId,
|
||||||
|
type: policyType,
|
||||||
|
status: ApprovalRequestGrantStatus.Active,
|
||||||
|
projectId,
|
||||||
|
revokedAt: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Move some of this check to be part of SQL query
|
||||||
|
return grants.some((grant) => {
|
||||||
|
const grantAttributes = grant.attributes as TPamAccessPolicyInputs;
|
||||||
|
const isMatch = picomatch(grantAttributes.accountPath);
|
||||||
|
return (
|
||||||
|
grantAttributes.resourceId === inputs.resourceId &&
|
||||||
|
isMatch(inputs.accountPath) &&
|
||||||
|
(!grant.expiresAt || grant.expiresAt > new Date())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchPolicy,
|
||||||
|
canAccess
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BaseApprovalPolicySchema,
|
||||||
|
BaseCreateApprovalPolicySchema,
|
||||||
|
BaseUpdateApprovalPolicySchema
|
||||||
|
} from "../approval-policy-schemas";
|
||||||
|
|
||||||
|
// Inputs
|
||||||
|
export const PamAccessPolicyInputsSchema = z.object({
|
||||||
|
resourceId: z.string().uuid(),
|
||||||
|
accountPath: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
export const PamAccessPolicyConditionsSchema = z
|
||||||
|
.object({
|
||||||
|
targetResources: z.string().uuid().array(),
|
||||||
|
accountPaths: z.string().array() // TODO: Add path & wildcard validation
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
|
||||||
|
// Constraints
|
||||||
|
export const PamAccessPolicyConstraintsSchema = z.object({
|
||||||
|
requestDurationHours: z.object({
|
||||||
|
// 168 hours = 7 days
|
||||||
|
min: z.number().min(0).max(168),
|
||||||
|
max: z.number().min(1).max(168)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Policy
|
||||||
|
export const PamAccessPolicySchema = BaseApprovalPolicySchema.extend({
|
||||||
|
conditions: z.object({
|
||||||
|
version: z.literal(1),
|
||||||
|
conditions: PamAccessPolicyConditionsSchema
|
||||||
|
}),
|
||||||
|
constraints: z.object({
|
||||||
|
version: z.literal(1),
|
||||||
|
constraints: PamAccessPolicyConstraintsSchema
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreatePamAccessPolicySchema = BaseCreateApprovalPolicySchema.extend({
|
||||||
|
conditions: PamAccessPolicyConditionsSchema,
|
||||||
|
constraints: PamAccessPolicyConstraintsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdatePamAccessPolicySchema = BaseUpdateApprovalPolicySchema.extend({
|
||||||
|
conditions: PamAccessPolicyConditionsSchema.optional(),
|
||||||
|
constraints: PamAccessPolicyConstraintsSchema.optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PamAccessPolicyConditionsSchema,
|
||||||
|
PamAccessPolicyConstraintsSchema,
|
||||||
|
PamAccessPolicyInputsSchema,
|
||||||
|
PamAccessPolicySchema
|
||||||
|
} from "./pam-access-policy-schemas";
|
||||||
|
|
||||||
|
export type TPamAccessPolicy = z.infer<typeof PamAccessPolicySchema>;
|
||||||
|
export type TPamAccessPolicyInputs = z.infer<typeof PamAccessPolicyInputsSchema>;
|
||||||
|
export type TPamAccessPolicyConditions = z.infer<typeof PamAccessPolicyConditionsSchema>;
|
||||||
|
export type TPamAccessPolicyConstraints = z.infer<typeof PamAccessPolicyConstraintsSchema>;
|
||||||
Reference in New Issue
Block a user