Merge branch 'main' of https://github.com/Infisical/infisical into feat/adds-GETtokenAuthTokenById-api-endpoint

This commit is contained in:
Piyush Gupta
2025-11-17 20:22:09 +05:30
422 changed files with 15873 additions and 4805 deletions
+3 -1
View File
@@ -91,6 +91,7 @@ import { TIdentityProjectServiceFactory } from "@app/services/identity-project/i
import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TScopedIdentityV2ServiceFactory } from "@app/services/identity-v2/identity-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service";
@@ -258,7 +259,8 @@ declare module "fastify" {
integrationAuth: TIntegrationAuthServiceFactory;
webhook: TWebhookServiceFactory;
serviceToken: TServiceTokenServiceFactory;
identity: TIdentityServiceFactory;
identityV1: TIdentityServiceFactory;
identityV2: TScopedIdentityV2ServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityTokenAuth: TIdentityTokenAuthServiceFactory;
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId");
if (!hasProjectIdCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.string("projectId");
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId");
if (hasProjectIdCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.dropColumn("projectId");
});
}
}
+2 -1
View File
@@ -14,7 +14,8 @@ export const IdentitiesSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false),
orgId: z.string().uuid()
orgId: z.string().uuid(),
projectId: z.string().nullable().optional()
});
export type TIdentities = z.infer<typeof IdentitiesSchema>;
+1 -5
View File
@@ -1,5 +1,4 @@
import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router";
import { getConfig } from "@app/lib/config/env";
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
@@ -109,10 +108,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
// Notice: current this feature is still in development and is not yet ready for production.
if (getConfig().isAcmeFeatureEnabled === true) {
await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
}
await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
},
{ prefix: "/pki" }
);
+3 -10
View File
@@ -435,14 +435,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
message: z
.string()
.trim()
.min(1)
.max(255)
.refine((message) => message.trim() !== "", {
message: "Commit message cannot be empty"
}),
message: z.string().trim().max(255).optional(),
changes: z.object({
secrets: z.object({
create: z
@@ -546,7 +539,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
message: req.body.message,
message: req.body.message || "",
changes: {
secrets: req.body.changes.secrets,
folders: req.body.changes.folders
@@ -564,7 +557,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
message: req.body.message
message: req.body.message || ""
}
}
});
+3 -13
View File
@@ -2,7 +2,6 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
import {
AcmeOrderResourceSchema,
CreateAcmeAccountResponseSchema,
@@ -257,12 +256,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({
const { profileId, accountId } = await validateExistingAccount({
req
});
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
return sendAcmeResponse(
res,
profileId,
@@ -369,12 +365,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({
const { profileId, accountId } = await validateExistingAccount({
req
});
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
res.type("application/pem-certificate-chain");
return sendAcmeResponse(
res,
@@ -405,10 +398,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ req });
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
const { profileId, accountId } = await validateExistingAccount({ req });
return sendAcmeResponse(
res,
profileId,
+3 -3
View File
@@ -42,7 +42,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organization } = await server.services.subOrganization.createSubOrg({
name: req.body.name,
@@ -95,7 +95,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organizations } = await server.services.subOrganization.listSubOrgs({
permissionActor: req.permission,
@@ -137,7 +137,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { organization } = await server.services.subOrganization.updateSubOrg({
subOrgId: req.params.subOrgId,
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
@@ -48,7 +49,7 @@ export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [
BaseChefConnectionSchema.extend({
method: z.literal(ChefConnectionMethod.UserKey),
credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true })
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Chef]} (User Key)` }))
]);
export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -70,8 +71,10 @@ export const UpdateChefConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef));
export const ChefConnectionListItemSchema = z.object({
name: z.literal("Chef"),
app: z.literal(AppConnection.Chef),
methods: z.nativeEnum(ChefConnectionMethod).array()
});
export const ChefConnectionListItemSchema = z
.object({
name: z.literal("Chef"),
app: z.literal(AppConnection.Chef),
methods: z.nativeEnum(ChefConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Chef] }));
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
@@ -34,7 +35,7 @@ export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
region: true,
fingerprint: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OCI]} (Access Key)` }))
]);
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -58,8 +59,10 @@ export const UpdateOCIConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
export const OCIConnectionListItemSchema = z.object({
name: z.literal("OCI"),
app: z.literal(AppConnection.OCI),
methods: z.nativeEnum(OCIConnectionMethod).array()
});
export const OCIConnectionListItemSchema = z
.object({
name: z.literal("OCI"),
app: z.literal(AppConnection.OCI),
methods: z.nativeEnum(OCIConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OCI] }));
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
@@ -32,7 +33,7 @@ export const SanitizedOracleDBConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OracleDB]} (Username and Password)` }))
]);
export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -64,9 +65,11 @@ export const UpdateOracleDBConnectionSchema = z
})
);
export const OracleDBConnectionListItemSchema = z.object({
name: z.literal("OracleDB"),
app: z.literal(AppConnection.OracleDB),
methods: z.nativeEnum(OracleDBConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});
export const OracleDBConnectionListItemSchema = z
.object({
name: z.literal("OracleDB"),
app: z.literal(AppConnection.OracleDB),
methods: z.nativeEnum(OracleDBConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OracleDB] }));
@@ -159,9 +159,22 @@ export enum EventType {
DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_SUB_ORGANIZATION = "create-sub-organization",
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity",
DELETE_IDENTITY = "delete-identity",
CREATE_IDENTITY_ORG_MEMBERSHIP = "create-identity-org-membership",
UPDATE_IDENTITY_ORG_MEMBERSHIP = "update-identity-org-membership",
DELETE_IDENTITY_ORG_MEMBERSHIP = "delete-identity-org-membership",
CREATE_IDENTITY_PROJECT_MEMBERSHIP = "create-identity-project-membership",
UPDATE_IDENTITY_PROJECT_MEMBERSHIP = "update-identity-project-membership",
DELETE_IDENTITY_PROJECT_MEMBERSHIP = "delete-identity-project-membership",
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
@@ -175,9 +188,6 @@ export enum EventType {
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
GET_TOKEN_IDENTITY_TOKEN_AUTH = "get-token-identity-token-auth",
CREATE_SUB_ORGANIZATION = "create-sub-organization",
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
@@ -324,6 +334,7 @@ export enum EventType {
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
EXPORT_CERT_PKCS12 = "export-cert-pkcs12",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
@@ -355,6 +366,8 @@ export enum EventType {
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -560,6 +573,7 @@ interface UserActorMetadata {
email?: string | null;
username: string;
permission?: Record<string, unknown>;
authMethod?: string;
}
interface ServiceActorMetadata {
@@ -891,6 +905,7 @@ interface CreateIdentityEvent {
identityId: string;
name: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[];
};
}
@@ -900,6 +915,7 @@ interface UpdateIdentityEvent {
identityId: string;
name?: string;
hasDeleteProtection?: boolean;
metadata?: { key: string; value: string }[];
};
}
@@ -1509,6 +1525,52 @@ interface ClearIdentityLdapAuthLockoutsEvent {
};
}
interface CreateIdentityOrgMembershipEvent {
type: EventType.CREATE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
roles: unknown;
};
}
interface UpdateIdentityOrgMembershipEvent {
type: EventType.UPDATE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
roles?: unknown;
};
}
interface DeleteIdentityOrgMembershipEvent {
type: EventType.DELETE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
};
}
interface CreateIdentityProjectMembershipEvent {
type: EventType.CREATE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
roles: unknown;
};
}
interface UpdateIdentityProjectMembershipEvent {
type: EventType.UPDATE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
roles?: unknown;
};
}
interface DeleteIdentityProjectMembershipEvent {
type: EventType.DELETE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
};
}
interface LoginIdentityOidcAuthEvent {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
metadata: {
@@ -2324,6 +2386,14 @@ interface GetCertBundle {
serialNumber: string;
};
}
interface GetCertPkcs12 {
type: EventType.EXPORT_CERT_PKCS12;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
@@ -2599,6 +2669,22 @@ interface OrgAdminBypassSSOEvent {
metadata: Record<string, string>; // no metadata yet
}
interface UserLoginEvent {
type: EventType.USER_LOGIN;
metadata: {
organizationId?: string;
authProvider?: string;
};
}
interface SelectOrganizationEvent {
type: EventType.SELECT_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
};
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
@@ -4198,6 +4284,12 @@ export type Event =
| GetIdentityLdapAuthEvent
| RevokeIdentityLdapAuthEvent
| ClearIdentityLdapAuthLockoutsEvent
| CreateIdentityOrgMembershipEvent
| UpdateIdentityOrgMembershipEvent
| DeleteIdentityOrgMembershipEvent
| CreateIdentityProjectMembershipEvent
| UpdateIdentityProjectMembershipEvent
| DeleteIdentityProjectMembershipEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
@@ -4262,6 +4354,7 @@ export type Event =
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| GetCertPkcs12
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert
@@ -4471,4 +4564,6 @@ export type Event =
| UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed;
| AutomatedRenewCertificateFailed
| UserLoginEvent
| SelectOrganizationEvent;
@@ -56,6 +56,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretsLimit: 40
},
pkiEst: false,
pkiAcme: false,
enforceMfa: false,
projectTemplates: false,
kmip: false,
@@ -78,6 +78,7 @@ export type TFeatureSet = {
secretsLimit: number;
};
pkiEst: boolean;
pkiAcme: false;
enforceMfa: boolean;
projectTemplates: false;
kmip: false;
@@ -104,8 +104,7 @@ const makeSqlConnection = (
// (like being able to do an auth handshake regardless pass or not)
if (
connectOnly &&
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
error.message.includes("no pg_hba.conf entry for host"))
error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`
) {
return;
}
@@ -171,7 +171,11 @@ const buildAdminPermissionRules = () => {
ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionIdentityActions.AssumePrivileges
ProjectPermissionIdentityActions.AssumePrivileges,
ProjectPermissionIdentityActions.GetToken,
ProjectPermissionIdentityActions.CreateToken,
ProjectPermissionIdentityActions.DeleteToken,
ProjectPermissionIdentityActions.RevokeAuth
],
ProjectPermissionSub.Identity
);
@@ -204,9 +204,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
.on(`${TableName.IdentityMetadata}.userId`, db.raw("?", [actorId]))
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
} else if (actorType === ActorType.IDENTITY) {
void queryBuilder
.on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId]))
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
void queryBuilder.on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId]));
}
})
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
@@ -667,9 +665,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
})
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
void queryBuilder.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`);
})
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
@@ -196,7 +196,7 @@ export const permissionServiceFactory = ({
}
if (orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
throw new ForbiddenRequestError({ name: "You are not allowed to access organization resource" });
}
const permissionData = await permissionDAL.getPermission({
@@ -344,7 +344,7 @@ export const permissionServiceFactory = ({
});
if (projectDetails.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
throw new ForbiddenRequestError({ name: "This project does not belong to your selected organization." });
}
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) {
@@ -362,7 +362,7 @@ export const permissionServiceFactory = ({
actorId,
actorType: actor
});
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" });
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this project" });
const permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles
@@ -65,7 +65,11 @@ export enum ProjectPermissionIdentityActions {
Edit = "edit",
Delete = "delete",
GrantPrivileges = "grant-privileges",
AssumePrivileges = "assume-privileges"
AssumePrivileges = "assume-privileges",
RevokeAuth = "revoke-auth",
CreateToken = "create-token",
GetToken = "get-token",
DeleteToken = "delete-token"
}
export enum ProjectPermissionMemberActions {
@@ -76,7 +76,9 @@ export const pkiAcmeChallengeServiceFactory = ({
// challenge validation at the same time, it should be fine.
const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) });
if (challengeResponse.status !== 200) {
throw new BadRequestError({ message: "ACME challenge response is not 200" });
throw new AcmeIncorrectResponseError({
message: `ACME challenge response is not 200: ${challengeResponse.status}`
});
}
const challengeResponseBody = await challengeResponse.text();
const thumbprint = challenge.auth.account.publicKeyThumbprint;
@@ -107,6 +109,7 @@ export const pkiAcmeChallengeServiceFactory = ({
if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
} else if (exp instanceof DOMException) {
@@ -35,7 +35,7 @@ export enum AcmeErrorType {
export interface IAcmeError {
type: AcmeErrorType;
detail: string;
message: string;
status: number;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
}
@@ -43,7 +43,7 @@ export interface IAcmeError {
export class AcmeError extends Error implements IAcmeError {
type: AcmeErrorType;
detail: string;
message: string;
status: number;
@@ -53,22 +53,20 @@ export class AcmeError extends Error implements IAcmeError {
constructor({
type,
detail,
message,
status,
subproblems,
error,
message
error
}: {
type: AcmeErrorType;
detail: string;
message: string;
status: number;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
error?: unknown;
message?: string;
}) {
super(message || detail);
super(message);
this.type = type;
this.detail = detail;
this.message = message;
this.status = status;
this.subproblems = subproblems;
this.error = error;
@@ -78,7 +76,7 @@ export class AcmeError extends Error implements IAcmeError {
toAcmeResponse(): IAcmeError {
return {
type: this.type,
detail: this.detail,
message: this.message,
status: this.status,
subproblems: this.subproblems
};
@@ -90,20 +88,17 @@ export class AcmeError extends Error implements IAcmeError {
*/
export class AcmeMalformedError extends AcmeError {
constructor({
detail = "The request message was malformed",
error,
message
message = "The request message was malformed",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.Malformed,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeMalformedError";
}
@@ -114,20 +109,17 @@ export class AcmeMalformedError extends AcmeError {
*/
export class AcmeUnauthorizedError extends AcmeError {
constructor({
detail = "The client lacks sufficient authorization",
error,
message
message = "The client lacks sufficient authorization",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.Unauthorized,
detail,
message,
status: 403,
error,
message
error
});
this.name = "AcmeUnauthorizedError";
}
@@ -139,20 +131,17 @@ export class AcmeUnauthorizedError extends AcmeError {
*/
export class AcmeAccountDoesNotExistError extends AcmeError {
constructor({
detail = "The request specified an account that does not exist",
error,
message
message = "The request specified an account that does not exist",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.AccountDoesNotExist,
detail,
status: 400,
error,
message
message,
status: 404,
error
});
this.name = "AcmeAccountDoesNotExistError";
}
@@ -163,20 +152,17 @@ export class AcmeAccountDoesNotExistError extends AcmeError {
*/
export class AcmeBadNonceError extends AcmeError {
constructor({
detail = "The client sent an unacceptable anti-replay nonce",
error,
message
message = "The client sent an unacceptable anti-replay nonce",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadNonce,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeBadNonceError";
}
@@ -187,20 +173,17 @@ export class AcmeBadNonceError extends AcmeError {
*/
export class AcmeBadSignatureAlgorithmError extends AcmeError {
constructor({
detail = "The signature algorithm is invalid",
error,
message
message = "The signature algorithm is invalid",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadSignatureAlgorithm,
detail,
message,
status: 401,
error,
message
error
});
this.name = "AcmeBadSignatureAlgorithmError";
}
@@ -211,20 +194,17 @@ export class AcmeBadSignatureAlgorithmError extends AcmeError {
*/
export class AcmeBadPublicKeyError extends AcmeError {
constructor({
detail = "The public key is not acceptable",
error,
message
message = "The public key is not acceptable",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadPublicKey,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeBadPublicKeyError";
}
@@ -235,20 +215,17 @@ export class AcmeBadPublicKeyError extends AcmeError {
*/
export class AcmeBadCsrError extends AcmeError {
constructor({
detail = "The CSR is unacceptable",
error,
message
message = "The CSR is unacceptable",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadCsr,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeBadCsrError";
}
@@ -260,20 +237,17 @@ export class AcmeBadCsrError extends AcmeError {
*/
export class AcmeBadRevocationReasonError extends AcmeError {
constructor({
detail = "The revocation reason provided is not allowed",
error,
message
message = "The revocation reason provided is not allowed",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadRevocationReason,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeBadRevocationReasonError";
}
@@ -284,20 +258,17 @@ export class AcmeBadRevocationReasonError extends AcmeError {
*/
export class AcmeRateLimitedError extends AcmeError {
constructor({
detail = "The client has exceeded a rate limit",
error,
message
message = "The client has exceeded a rate limit",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.RateLimited,
detail,
message,
status: 429,
error,
message
error
});
this.name = "AcmeRateLimitedError";
}
@@ -309,23 +280,20 @@ export class AcmeRateLimitedError extends AcmeError {
*/
export class AcmeRejectedIdentifierError extends AcmeError {
constructor({
detail = "The server will not issue certificates for the identifier",
message = "The server will not issue certificates for the identifier",
subproblems,
error,
message
error
}: {
detail?: string;
message?: string;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
error?: unknown;
message?: string;
} = {}) {
super({
type: AcmeErrorType.RejectedIdentifier,
detail,
message,
status: 400,
subproblems,
error,
message
error
});
this.name = "AcmeRejectedIdentifierError";
}
@@ -336,20 +304,17 @@ export class AcmeRejectedIdentifierError extends AcmeError {
*/
export class AcmeServerInternalError extends AcmeError {
constructor({
detail = "An internal error occurred",
error,
message
message = "An internal error occurred",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.ServerInternal,
detail,
message,
status: 500,
error,
message
error
});
this.name = "AcmeServerInternalError";
}
@@ -360,20 +325,17 @@ export class AcmeServerInternalError extends AcmeError {
*/
export class AcmeUnsupportedContactError extends AcmeError {
constructor({
detail = "A contact URL is of an unsupported type",
error,
message
message = "A contact URL is of an unsupported type",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.UnsupportedContact,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeUnsupportedContactError";
}
@@ -385,20 +347,17 @@ export class AcmeUnsupportedContactError extends AcmeError {
*/
export class AcmeUnsupportedIdentifierError extends AcmeError {
constructor({
detail = "An identifier is of an unsupported type",
error,
message
message = "An identifier is of an unsupported type",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.UnsupportedIdentifier,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeUnsupportedIdentifierError";
}
@@ -412,22 +371,19 @@ export class AcmeUserActionRequiredError extends AcmeError {
instance?: string;
constructor({
detail = "Visit the instance URL and take actions specified there",
message = "Visit the instance URL and take actions specified there",
instance,
error,
message
error
}: {
detail?: string;
message?: string;
instance?: string;
error?: unknown;
message?: string;
} = {}) {
super({
type: AcmeErrorType.UserActionRequired,
detail,
message,
status: 403,
error,
message
error
});
this.instance = instance;
this.name = "AcmeUserActionRequiredError";
@@ -446,20 +402,17 @@ export class AcmeUserActionRequiredError extends AcmeError {
*/
export class AcmeIncorrectResponseError extends AcmeError {
constructor({
detail = "The response is incorrect",
error,
message
message = "The response is incorrect",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.IncorrectResponse,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeIncorrectResponseError";
}
@@ -470,20 +423,17 @@ export class AcmeIncorrectResponseError extends AcmeError {
*/
export class AcmeConnectionError extends AcmeError {
constructor({
detail = "A connection error occurred",
error,
message
message = "A connection error occurred",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.Connection,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeConnectionError";
}
@@ -491,20 +441,17 @@ export class AcmeConnectionError extends AcmeError {
export class AcmeDnsFailureError extends AcmeError {
constructor({
detail = "Hostname could not be resolved (DNS failure)",
error,
message
message = "Hostname could not be resolved (DNS failure)",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.DNS,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeDnsFailureError";
}
@@ -512,20 +459,17 @@ export class AcmeDnsFailureError extends AcmeError {
export class AcmeOrderNotReadyError extends AcmeError {
constructor({
detail = "The order is not ready",
error,
message
message = "The order is not ready",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.OrderNotReady,
detail,
status: 403,
error,
message
message,
status: 400,
error
});
this.name = "AcmeOrderNotReadyError";
}
@@ -533,20 +477,17 @@ export class AcmeOrderNotReadyError extends AcmeError {
export class AcmeBadCSRError extends AcmeError {
constructor({
detail = "The CSR is unacceptable",
error,
message
message = "The CSR is unacceptable",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.BadCsr,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeBadCSRError";
}
@@ -554,20 +495,17 @@ export class AcmeBadCSRError extends AcmeError {
export class AcmeExternalAccountRequiredError extends AcmeError {
constructor({
detail = "External account binding is required",
error,
message
message = "External account binding is required",
error
}: {
detail?: string;
error?: unknown;
message?: string;
error?: unknown;
} = {}) {
super({
type: AcmeErrorType.ExternalAccountRequired,
detail,
message,
status: 400,
error,
message
error
});
this.name = "AcmeExternalAccountRequiredError";
}
@@ -1,8 +1,9 @@
import RE2 from "re2";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { AcmeMalformedError } from "./pki-acme-errors";
import { AcmeAccountDoesNotExistError } from "./pki-acme-errors";
export const buildUrl = (profileId: string, path: string): string => {
const appCfg = getConfig();
@@ -13,7 +14,14 @@ export const buildUrl = (profileId: string, path: string): string => {
export const extractAccountIdFromKid = (kid: string, profileId: string): string => {
const kidPrefix = buildUrl(profileId, "/accounts/");
if (!kid.startsWith(kidPrefix)) {
throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" });
throw new AcmeAccountDoesNotExistError({ message: "KID must start with the profile account URL" });
}
return z.string().uuid().parse(kid.slice(kidPrefix.length));
};
export const validateDnsIdentifier = (identifier: string): boolean => {
// DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen
const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/);
const labels = identifier.split(".");
return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label));
};
@@ -1,4 +1,3 @@
import RE2 from "re2";
import { z } from "zod";
export enum AcmeIdentifierType {
@@ -88,13 +87,8 @@ export const CreateAcmeAccountResponseSchema = z.object({
export const CreateAcmeOrderBodySchema = z.object({
identifiers: z.array(
z.object({
type: z.enum(Object.values(AcmeIdentifierType) as [string, ...string[]]),
value: z.string().refine((val) => {
// DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen
const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/);
const labels = val.split(".");
return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label));
}, "Invalid DNS identifier")
type: z.string(),
value: z.string()
})
),
notBefore: z.string().optional(),
@@ -12,12 +12,26 @@ import { z, ZodError } from "zod";
import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts";
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { ActorType } from "@app/services/auth/auth-type";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyUsage,
CertSubjectAlternativeNameType
} from "@app/services/certificate/certificate-types";
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import {
EnrollmentType,
@@ -28,6 +42,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 { TLicenseServiceFactory } from "../license/license-service";
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
@@ -44,7 +59,7 @@ import {
AcmeUnauthorizedError,
AcmeUnsupportedIdentifierError
} from "./pki-acme-errors";
import { buildUrl, extractAccountIdFromKid } from "./pki-acme-fns";
import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import {
@@ -77,9 +92,14 @@ import {
} from "./pki-acme-types";
type TPkiAcmeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
@@ -100,15 +120,24 @@ type TPkiAcmeServiceFactoryDep = {
"create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation"
>;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"decryptWithKmsKey" | "generateKmsKey" | "encryptWithKmsKey" | "createCipherPairWithDataKey"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeChallengeService: TPkiAcmeChallengeServiceFactory;
};
export const pkiAcmeServiceFactory = ({
projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL,
certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
@@ -116,6 +145,7 @@ export const pkiAcmeServiceFactory = ({
acmeChallengeDAL,
keyStore,
kmsService,
licenseService,
certificateV3Service,
acmeChallengeService
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
@@ -127,6 +157,12 @@ export const pkiAcmeServiceFactory = ({
if (profile.enrollmentType !== EnrollmentType.ACME) {
throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" });
}
const orgLicensePlan = await licenseService.getPlan(profile.project!.orgId);
if (!orgLicensePlan.pkiAcme) {
throw new AcmeUnauthorizedError({
message: "Failed to validate ACME profile: Plan restriction. Upgrade plan to continue"
});
}
return profile;
};
@@ -148,7 +184,7 @@ export const pkiAcmeServiceFactory = ({
try {
result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => {
if (protectedHeader === undefined) {
throw new AcmeMalformedError({ detail: "Protected header is required" });
throw new AcmeMalformedError({ message: "Protected header is required" });
}
const jwk = await getJWK(protectedHeader);
const key = await importJWK(jwk, protectedHeader.alg);
@@ -159,28 +195,35 @@ export const pkiAcmeServiceFactory = ({
throw error;
}
if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` });
throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` });
}
if (error instanceof errors.JWSSignatureVerificationFailed) {
throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" });
throw new AcmeBadPublicKeyError({ message: "Invalid JWS payload" });
}
logger.error(error, "Unexpected error while verifying JWS payload");
throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" });
throw new AcmeMalformedError({ message: "Failed to verify JWS payload" });
}
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
const parsedUrl = (() => {
try {
return new URL(protectedHeader.url);
} catch (error) {
throw new AcmeMalformedError({ message: "Invalid URL in the protected header" });
}
})();
// Validate the URL
if (new URL(protectedHeader.url).href !== url.href) {
throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" });
if (parsedUrl.href !== url.href) {
throw new AcmeMalformedError({ message: "URL mismatch in the protected header" });
}
// Consume the nonce
if (!protectedHeader.nonce) {
throw new AcmeMalformedError({ detail: "Nonce is required in the protected header" });
throw new AcmeMalformedError({ message: "Nonce is required in the protected header" });
}
const deleted = await keyStore.deleteItem(KeyStorePrefixes.PkiAcmeNonce(protectedHeader.nonce));
if (deleted !== 1) {
throw new AcmeBadNonceError({ detail: "Invalid nonce" });
throw new AcmeBadNonceError({ message: "Invalid nonce" });
}
// Parse the payload
@@ -196,10 +239,10 @@ export const pkiAcmeServiceFactory = ({
throw error;
}
if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` });
throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` });
}
logger.error(error, "Unexpected error while parsing JWS payload");
throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" });
throw new AcmeMalformedError({ message: "Failed to verify JWS payload" });
}
};
@@ -215,7 +258,7 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload,
getJWK: async (protectedHeader) => {
if (!protectedHeader.jwk) {
throw new AcmeMalformedError({ detail: "JWK is required in the protected header" });
throw new AcmeMalformedError({ message: "JWK is required in the protected header" });
}
return protectedHeader.jwk as unknown as JsonWebKey;
},
@@ -246,18 +289,18 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload,
getJWK: async (protectedHeader) => {
if (!protectedHeader.kid) {
throw new AcmeMalformedError({ detail: "KID is required in the protected header" });
throw new AcmeMalformedError({ message: "KID is required in the protected header" });
}
const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId);
if (expectedAccountId && accountId !== expectedAccountId) {
throw new NotFoundError({ message: "ACME resource not found" });
throw new AcmeAccountDoesNotExistError({ message: "ACME resource not found" });
}
const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId);
if (!account) {
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
}
if (account.alg !== protectedHeader.alg) {
throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" });
throw new AcmeMalformedError({ message: "ACME account algorithm mismatch" });
}
return account.publicKey as JsonWebKey;
},
@@ -344,7 +387,7 @@ export const pkiAcmeServiceFactory = ({
}): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
const profile = await validateAcmeProfile(profileId);
if (!externalAccountBinding) {
throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" });
throw new AcmeExternalAccountRequiredError({ message: "External account binding is required" });
}
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
@@ -363,26 +406,28 @@ export const pkiAcmeServiceFactory = ({
return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader };
} catch (error) {
if (error instanceof errors.JWSSignatureVerificationFailed) {
throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" });
throw new AcmeExternalAccountRequiredError({ message: "Invalid external account binding JWS signature" });
}
logger.error(error, "Unexpected error while verifying EAB JWS signature");
throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS signature" });
throw new AcmeServerInternalError({ message: "Failed to verify EAB JWS signature" });
}
})();
const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!;
if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) {
throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" });
throw new AcmeExternalAccountRequiredError({
message: "Invalid algorithm for external account binding JWS payload"
});
}
// Make sure the KID in the EAB payload matches the profile ID
if (eabKid !== profile.id) {
throw new UnauthorizedError({ message: "External account binding KID mismatch" });
throw new AcmeExternalAccountRequiredError({ message: "External account binding KID mismatch" });
}
// Make sure the URL matches the expected URL
const url = eabProtectedHeader!.url!;
if (url !== buildUrl(profile.id, "/new-account")) {
throw new UnauthorizedError({ message: "External account binding URL mismatch" });
throw new AcmeExternalAccountRequiredError({ message: "External account binding URL mismatch" });
}
// Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload
@@ -481,6 +526,21 @@ export const pkiAcmeServiceFactory = ({
// TODO: check the identifiers and see if are they even allowed for this profile.
// if not, we may be able to reject it early with an unsupportedIdentifier error.
// TODO: ideally, we should return an error with subproblems if we have multiple unsupported identifiers
if (payload.identifiers.some((identifier) => identifier.type !== AcmeIdentifierType.DNS)) {
throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" });
}
if (
payload.identifiers.some(
(identifier) =>
!validateDnsIdentifier(identifier.value) ||
isPrivateIp(identifier.value) ||
(!getConfig().isDevelopmentMode && identifier.value.toLowerCase() === "localhost")
)
) {
throw new AcmeUnsupportedIdentifierError({ message: "Invalid DNS identifier" });
}
const order = await acmeOrderDAL.transaction(async (tx) => {
const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!;
const createdOrder = await acmeOrderDAL.create(
@@ -497,10 +557,10 @@ export const pkiAcmeServiceFactory = ({
const authorizations: TPkiAcmeAuths[] = await Promise.all(
payload.identifiers.map(async (identifier) => {
if (identifier.type !== AcmeIdentifierType.DNS) {
throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" });
throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" });
}
if (isPrivateIp(identifier.value)) {
throw new AcmeUnsupportedIdentifierError({ detail: "Private IP addresses are not allowed" });
throw new AcmeUnsupportedIdentifierError({ message: "Private IP addresses are not allowed" });
}
const auth = await acmeAuthDAL.create(
{
@@ -588,6 +648,7 @@ export const pkiAcmeServiceFactory = ({
orderId: string;
payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
if (!order) {
throw new NotFoundError({ message: "ACME order not found" });
@@ -603,28 +664,100 @@ export const pkiAcmeServiceFactory = ({
if (finalizingOrder.expiresAt < new Date()) {
throw new AcmeOrderNotReadyError({ message: "ACME order has expired" });
}
const { csr } = payload;
// Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if (
certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Only DNS subject alternative names are supported" });
}
const orderWithAuthorizations = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(
accountId,
orderId,
tx
))!;
const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()])
);
if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length ||
!orderWithAuthorizations.authorizations.every((auth) =>
csrIdentifierValues.has(auth.identifierValue.toLowerCase())
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
let errorToReturn: Error | undefined;
try {
const { certificateId } = await certificateV3Service.signCertificateFromProfile({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
validity: !finalizingOrder.notAfter
? {
// TODO: read config from the profile to get the expiration time instead
ttl: (24 * 60 * 60 * 1000).toString()
}
: // ttl is not used if notAfter is provided
({ ttl: "0" } as const),
enrollmentType: EnrollmentType.ACME
});
// TODO: associate the certificate with the order
const { certificateId } = await (async () => {
if (caType === CaType.INTERNAL) {
const result = await certificateV3Service.signCertificateFromProfile({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
validity: !finalizingOrder.notAfter
? {
// 47 days, the default TTL comes with Let's Encrypt
// TODO: read config from the profile to get the expiration time instead
ttl: `${47}d`
}
: // ttl is not used if notAfter is provided
({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
return { certificateId: result.certificateId };
}
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem");
// TODO: for internal CA, we rely on the internal certificate authority service to check CSR against the template
// we should check the CSR against the template here
// TODO: this is pretty slow, and we are holding the transaction open for a long time,
// we should queue the certificate issuance to a background job instead
const cert = await orderCertificate(
{
caId: certificateAuthority!.id,
commonName: certificateRequest.commonName!,
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
csr: Buffer.from(csrPem),
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
return { certificateId: cert.id };
})();
await acmeOrderDAL.updateById(
orderId,
{
@@ -647,9 +780,9 @@ export const pkiAcmeServiceFactory = ({
logger.error(exp, "Failed to sign certificate");
// TODO: audit log the error
if (exp instanceof BadRequestError) {
errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${exp.message}` });
errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` });
} else {
errorToReturn = new AcmeServerInternalError({ detail: "Failed to sign certificate with internal error" });
errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" });
}
}
return {
@@ -10,7 +10,7 @@ import {
import { logger } from "@app/lib/logger";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { executeWithPotentialGateway, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils";
import {
@@ -71,17 +71,18 @@ export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput["temporaryParameters"]
> = (secretRotation, appConnectionDAL, kmsService) => {
> = (secretRotation, appConnectionDAL, kmsService, gatewayService, gatewayV2Service) => {
const { connection, parameters, secretsMapping, activeIndex } = secretRotation;
const { dn, passwordRequirements } = parameters;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try {
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
client.unbind();
client.destroy();
await executeWithPotentialGateway(
{ ...connection, credentials: { ...connection.credentials, ...credentials } },
gatewayV2Service,
async () => {}
);
} catch (error) {
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
}
@@ -92,17 +93,7 @@ export const ldapPasswordRotationFactory: TRotationFactory<
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(
currentPassword
? {
...credentials,
password: currentPassword,
dn
}
: credentials
);
const isConnectionRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements);
let changes: ldap.Change[] | ldap.Change;
@@ -147,22 +138,32 @@ export const ldapPasswordRotationFactory: TRotationFactory<
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
}
try {
const userDn = await getDN(dn, client);
await new Promise((resolve, reject) => {
client.modify(userDn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve(true);
}
await executeWithPotentialGateway(
{
...connection,
credentials: currentPassword
? {
...credentials,
password: currentPassword,
dn
}
: credentials
},
gatewayV2Service,
async (client) => {
const userDn = await getDN(dn, client);
await new Promise<void>((resolve, reject) => {
client.modify(userDn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve();
}
});
});
});
} finally {
client.unbind();
client.destroy();
}
}
);
await $verifyCredentials({ dn, password });
@@ -3,6 +3,7 @@ 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 { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
@@ -25,10 +26,12 @@ const ChefSyncDestinationConfigSchema = z.object({
const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Chef),
destinationConfig: ChefSyncDestinationConfigSchema
});
export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig)
.extend({
destination: z.literal(SecretSync.Chef),
destinationConfig: ChefSyncDestinationConfigSchema
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Chef] }));
export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
destinationConfig: ChefSyncDestinationConfigSchema
@@ -38,10 +41,12 @@ export const UpdateChefSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSy
destinationConfig: ChefSyncDestinationConfigSchema.optional()
});
export const ChefSyncListItemSchema = z.object({
name: z.literal("Chef"),
connection: z.literal(AppConnection.Chef),
destination: z.literal(SecretSync.Chef),
canImportSecrets: z.literal(true),
enterprise: z.boolean()
});
export const ChefSyncListItemSchema = z
.object({
name: z.literal("Chef"),
connection: z.literal(AppConnection.Chef),
destination: z.literal(SecretSync.Chef),
canImportSecrets: z.literal(true),
enterprise: z.boolean()
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Chef] }));
@@ -4,6 +4,7 @@ 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 { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
@@ -43,10 +44,12 @@ const OCIVaultSyncDestinationConfigSchema = z.object({
const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({
destination: z.literal(SecretSync.OCIVault),
destinationConfig: OCIVaultSyncDestinationConfigSchema
});
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig)
.extend({
destination: z.literal(SecretSync.OCIVault),
destinationConfig: OCIVaultSyncDestinationConfigSchema
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OCIVault] }));
export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OCIVault,
@@ -62,10 +65,12 @@ export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
destinationConfig: OCIVaultSyncDestinationConfigSchema.optional()
});
export const OCIVaultSyncListItemSchema = z.object({
name: z.literal("OCI Vault"),
connection: z.literal(AppConnection.OCI),
destination: z.literal(SecretSync.OCIVault),
canImportSecrets: z.literal(true),
enterprise: z.boolean()
});
export const OCIVaultSyncListItemSchema = z
.object({
name: z.literal("OCI Vault"),
connection: z.literal(AppConnection.OCI),
destination: z.literal(SecretSync.OCIVault),
canImportSecrets: z.literal(true),
enterprise: z.boolean()
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OCIVault] }));
+104 -3
View File
@@ -33,11 +33,13 @@ export enum ApiDocsTags {
LdapAuth = "LDAP Auth",
Groups = "Groups",
Organizations = "Organizations",
OrgIdentityMembership = "Organization Identity Membership",
SubOrganizations = "Sub Organizations",
Projects = "Projects",
ProjectUsers = "Project Users",
ProjectGroups = "Project Groups",
ProjectIdentities = "Project Identities",
IdentityProjectMembership = "Project Identity Membership",
ProjectRoles = "Project Roles",
ProjectTemplates = "Project Templates",
Environments = "Environments",
@@ -122,13 +124,15 @@ export const IDENTITIES = {
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'.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
hasDeleteProtection: "Prevents deletion of the identity when enabled.",
metadata: "An optional array of key-value pairs to attach to the identity."
},
UPDATE: {
identityId: "The ID of the machine identity to update.",
name: "The new name of the identity.",
role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
hasDeleteProtection: "Prevents deletion of the identity when enabled.",
metadata: "An optional array of key-value pairs to attach to the identity."
},
DELETE: {
identityId: "The ID of the machine identity to delete."
@@ -138,7 +142,10 @@ export const IDENTITIES = {
orgId: "The ID of the org of the identity"
},
LIST: {
orgId: "The ID of the organization to list identities."
orgId: "The ID of the organization to list identities.",
search: "The text string that identity names will be filtered by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return."
},
SEARCH: {
search: {
@@ -723,6 +730,50 @@ export const ORGANIZATIONS = {
}
} as const;
export const ORG_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the newly created identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return.",
identityName: "The text string that identity membership names will be filtered by."
}
} as const;
export const SUB_ORGANIZATIONS = {
CREATE: {
name: "The name of the sub organization to create."
@@ -911,6 +962,56 @@ export const PROJECT_IDENTITIES = {
}
};
export const PROJECT_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership for.",
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to list identity memberships from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "The text string that identity membership names will be filtered by.",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
projectId: "The ID of the project to get the identity membership for.",
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
projectId: "The ID of the project to list available identities for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return.",
identityName: "The text string that identity membership names will be filtered by."
}
} as const;
export const ENVIRONMENTS = {
CREATE: {
projectId: "The ID of the project to create the environment in.",
+3 -5
View File
@@ -106,11 +106,9 @@ const envSchema = z
HTTPS_ENABLED: zodStrBool,
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
// Note: The ACME feature is still in development and is not yet ready for production.
// This is the feature flag to enable/disable the ACME feature.
// It's not intended to be used by users outside of the development team yet.
ACME_FEATURE_ENABLED: zodStrBool.default("false").optional(),
BDD_NOCK_API_ENABLED: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
ACME_SKIP_UPSTREAM_VALIDATION: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr(
z
.string()
@@ -399,10 +397,10 @@ const envSchema = z
(data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test",
isDailyResourceCleanUpDevelopmentMode:
data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE,
isAcmeFeatureEnabled: data.NODE_ENV === "development" && data.ACME_FEATURE_ENABLED === true,
isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
isBddNockApiEnabled: data.NODE_ENV === "development" && data.BDD_NOCK_API_ENABLED,
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",")
.map((el) => {
+2 -2
View File
@@ -8,10 +8,10 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "../errors";
import { isPrivateIp } from "../ip/ipRange";
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
export const blockLocalAndPrivateIpAddresses = async (url: string, isGateway = false) => {
const appCfg = getConfig();
if (appCfg.isDevelopmentMode) return;
if (appCfg.isDevelopmentMode || isGateway) return;
const validUrl = new URL(url);
@@ -182,8 +182,8 @@ export const injectIdentity = fp(
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(
token,
subOrganizationSelector,
req.realIp
req.realIp,
subOrganizationSelector
);
const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId);
+1 -2
View File
@@ -252,8 +252,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
error: error.name,
status: error.status,
type: `urn:ietf:params:acme:error:${error.type}`,
detail: error.detail,
message: error.message
detail: error.message
// TODO: add subproblems if they exist
});
} else {
+1 -4
View File
@@ -31,10 +31,7 @@ export const registerServeUI = async (
CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY,
POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY,
INTERCOM_ID: appCfg.INTERCOM_ID,
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED,
// The feature flag to enable/disable the ACME feature.
// Will be removed once the feature is ready for production.
ACME_FEATURE_ENABLED: appCfg.isAcmeFeatureEnabled
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
};
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
return res.send(js);
+25 -2
View File
@@ -241,6 +241,8 @@ import { identityTokenAuthServiceFactory } from "@app/services/identity-token-au
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { identityV2DALFactory } from "@app/services/identity-v2/identity-dal";
import { identityV2ServiceFactory } from "@app/services/identity-v2/identity-service";
import { integrationDALFactory } from "@app/services/integration/integration-dal";
import { integrationServiceFactory } from "@app/services/integration/integration-service";
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
@@ -445,6 +447,7 @@ export const registerRoutes = async (
const serviceTokenDAL = serviceTokenDALFactory(db);
const identityDAL = identityDALFactory(db);
const identityV2DAL = identityV2DALFactory(db);
const identityMetadataDAL = identityMetadataDALFactory(db);
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
const identityOrgMembershipDAL = identityOrgDALFactory(db);
@@ -640,7 +643,8 @@ export const registerRoutes = async (
projectDAL,
identityDAL,
userDAL,
externalGroupOrgRoleMappingDAL
externalGroupOrgRoleMappingDAL,
membershipRoleDAL
});
const additionalPrivilegeService = additionalPrivilegeServiceFactory({
additionalPrivilegeDAL,
@@ -1184,6 +1188,7 @@ export const registerRoutes = async (
certificateAuthorityDAL,
certificateAuthorityCertDAL,
permissionService,
licenseService,
kmsService,
projectDAL
});
@@ -1655,6 +1660,17 @@ export const registerRoutes = async (
membershipIdentityDAL,
membershipRoleDAL
});
const identityV2Service = identityV2ServiceFactory({
membershipIdentityDAL,
membershipRoleDAL,
identityMetadataDAL,
licenseService,
permissionService,
identityDAL: identityV2DAL,
keyStore
});
const identityProjectService = identityProjectServiceFactory({
identityProjectDAL,
membershipIdentityDAL,
@@ -2229,8 +2245,13 @@ export const registerRoutes = async (
});
const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL,
certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
@@ -2238,6 +2259,7 @@ export const registerRoutes = async (
acmeChallengeDAL,
keyStore,
kmsService,
licenseService,
certificateV3Service,
acmeChallengeService
});
@@ -2457,7 +2479,8 @@ export const registerRoutes = async (
integrationAuth: integrationAuthService,
webhook: webhookService,
serviceToken: serviceTokenService,
identity: identityService,
identityV1: identityService,
identityV2: identityV2Service,
identityAuthTemplate: identityAuthTemplateService,
identityAccessToken: identityAccessTokenService,
identityTokenAuth: identityTokenAuthService,
@@ -0,0 +1,87 @@
// import { z } from "zod";
// import { getConfig } from "@app/lib/config/env";
// import { ForbiddenRequestError } from "@app/lib/errors";
// import { logger } from "@app/lib/logger";
// import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
// import { AuthMode } from "@app/services/auth/auth-type";
// export const registerBddNockRouter = async (server: FastifyZodProvider) => {
// const checkIfBddNockApiEnabled = () => {
// const appCfg = getConfig();
// // Note: Please note that this API is only available in development mode and only for BDD tests.
// // This endpoint should NEVER BE ENABLED IN PRODUCTION!
// if (appCfg.NODE_ENV !== "development" || !appCfg.isBddNockApiEnabled) {
// throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
// }
// };
// server.route({
// method: "POST",
// url: "/define",
// schema: {
// body: z.object({ definitions: z.unknown().array() }),
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// checkIfBddNockApiEnabled();
// const { body } = req;
// const { definitions } = body;
// logger.info(definitions, "Defining nock");
// const processedDefinitions = definitions.map((definition: unknown) => {
// const { path, ...rest } = definition as Definition;
// return {
// ...rest,
// path:
// path !== undefined && typeof path === "string"
// ? path
// : new RegExp((path as unknown as { regex: string }).regex ?? "")
// } as Definition;
// });
// nock.define(processedDefinitions);
// // Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
// if (!nock.isActive()) {
// nock.activate();
// }
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/clean-all",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Cleaning all nocks");
// nock.cleanAll();
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/restore",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Restore network requests from nock");
// nock.restore();
// return { status: "ok" };
// }
// });
// };
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import RE2 from "re2";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
@@ -616,4 +617,64 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:serialNumber/pkcs12",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.PkiCertificates],
description: "Download certificate in PKCS12 format",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}),
body: z.object({
password: z
.string()
.min(6, "Password must be at least 6 characters long")
.describe("Password for the keystore (minimum 6 characters)"),
alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore")
}),
response: {
200: z.any().describe("PKCS12 keystore as binary data")
}
},
handler: async (req, reply) => {
const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({
serialNumber: req.params.serialNumber,
password: req.body.password,
alias: req.body.alias,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.EXPORT_CERT_PKCS12,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
reply.header("Content-Type", "application/octet-stream");
reply.header(
"Content-Disposition",
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
);
return pkcs12Data;
}
});
};
@@ -19,7 +19,7 @@ import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
export const registerDeprecatedIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identity-memberships/:identityId",
@@ -293,7 +293,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -362,7 +362,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -1,9 +1,10 @@
import { z } from "zod";
import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas";
import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { AccessScope, IdentitiesSchema, MembershipRolesSchema, TemporaryPermissionMode } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, ORG_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -15,7 +16,7 @@ const sanitizedOrgIdentityMembershipSchema = z.object({
updatedAt: z.date()
});
export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => {
export const registerIdentityOrgMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identity-memberships/:identityId",
@@ -25,8 +26,7 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
// this is hidden so not updating tags
tags: [ApiDocsTags.ProjectIdentities],
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Create org identity membership",
security: [
{
@@ -34,38 +34,40 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
],
params: z.object({
identityId: z.string().trim()
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.max(1)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.min(1)
}),
response: {
200: z.object({
@@ -86,12 +88,115 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
server.route({
method: "PATCH",
url: "/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Update org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
roles: MembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
roles: membership.roles.map((el) => ({ ...el, membershipId: membership.id }))
};
}
});
server.route({
method: "DELETE",
url: "/identity-memberships/:identityId",
@@ -101,15 +206,15 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete org identity memberships",
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Delete org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
@@ -129,9 +234,226 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
server.route({
method: "GET",
url: "/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List org identity memberships",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Get org identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string())
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List available identities for org membership",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z.string().describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName).optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
};
@@ -0,0 +1,493 @@
import { z } from "zod";
import {
AccessScope,
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
TemporaryPermissionMode
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_IDENTITIES, PROJECT_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
// @depreciated
role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess),
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.optional()
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const { membership } = await server.services.membershipIdentity.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
identityId: req.params.identityId,
roles: roles || [{ role, isTemporary: false }]
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.CREATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Update project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.UPDATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.deleteMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.DELETE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "Get project identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string()),
metadata: z
.object({
id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()
.optional()
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List available identities for project membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName)
.optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
};
@@ -60,7 +60,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.createIdentity({
const identity = await server.services.identityV1.createIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -136,7 +136,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.updateIdentity({
const identity = await server.services.identityV1.updateIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -189,7 +189,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.deleteIdentity({
const identity = await server.services.identityV1.deleteIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -258,7 +258,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.getIdentityById({
const identity = await server.services.identityV1.getIdentityById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -308,7 +308,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
const { identityMemberships, totalCount } = await server.services.identityV1.listOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -402,7 +402,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
const { identityMemberships, totalCount } = await server.services.identityV1.searchOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -468,7 +468,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({
const identityMemberships = await server.services.identityV1.listProjectIdentitiesByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
+26 -5
View File
@@ -8,12 +8,14 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
// import { registerBddNockRouter } from "./bdd-nock-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
@@ -33,8 +35,8 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -45,6 +47,7 @@ import { registerInviteOrgRouter } from "./invite-org-router";
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
import { registerNotificationRouter } from "./notification-router";
import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgIdentityRouter } from "./org-identity-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router";
@@ -52,6 +55,7 @@ import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectIdentityRouter } from "./project-identity-router";
import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
@@ -90,8 +94,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ prefix: "/auth" }
);
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
await server.register(
async (orgRouter) => {
await orgRouter.register(registerOrgRouter);
await orgRouter.register(registerOrgIdentityRouter);
await orgRouter.register(registerIdentityOrgMembershipRouter);
},
{ prefix: "/organization" }
);
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" });
@@ -126,14 +136,19 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (projectRouter) => {
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerProjectIdentityRouter);
await projectRouter.register(registerProjectEnvRouter);
await projectRouter.register(registerSecretTagRouter);
await projectRouter.register(registerGroupProjectRouter);
await projectRouter.register(registerIdentityProjectRouter);
await projectRouter.register(registerDeprecatedIdentityProjectMembershipRouter);
},
{ prefix: "/projects" }
);
await server.register(registerIdentityProjectMembershipRouter, {
prefix: "/projects/:projectId/memberships"
});
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -223,4 +238,10 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
// if (getConfig().isBddNockApiEnabled) {
// await server.register(registerBddNockRouter, { prefix: "/bdd-nock" });
// }
};
@@ -0,0 +1,286 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
authMethods: z.array(z.string()).optional(),
metadata: z.array(metadataSchema).optional()
});
export const registerOrgIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Create an identity",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Update an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Delete an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "List identities",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};
@@ -0,0 +1,313 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
activeLockoutAuthMethods: z.string().array().optional(),
authMethods: z.string().array().optional(),
metadata: z
.object({
key: z.string(),
value: z.string(),
id: z.string()
})
.array()
.optional()
});
export const registerProjectIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Create an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project to create the identity in")
}),
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Update an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
projectId: req.params.projectId,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Delete an identity from a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "List identities in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project")
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};
@@ -1,6 +1,6 @@
import { z } from "zod";
import { AccessScope, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { AccessScope, OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -51,6 +51,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }],
usernames: usernamesAndEmails
}
});
const { memberships } = await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
@@ -70,7 +70,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
const { identityMemberships, totalCount } = await server.services.identityV1.listOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { OnePassConnectionMethod } from "./1password-connection-enums";
export const OnePassConnectionAccessTokenCredentialsSchema = z.object({
@@ -33,7 +34,7 @@ export const SanitizedOnePassConnectionSchema = z.discriminatedUnion("method", [
credentials: OnePassConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OnePass]} (API Token)` }))
]);
export const ValidateOnePassConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -57,8 +58,10 @@ export const UpdateOnePassConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OnePass));
export const OnePassConnectionListItemSchema = z.object({
name: z.literal("1Password"),
app: z.literal(AppConnection.OnePass),
methods: z.nativeEnum(OnePassConnectionMethod).array()
});
export const OnePassConnectionListItemSchema = z
.object({
name: z.literal("1Password"),
app: z.literal(AppConnection.OnePass),
methods: z.nativeEnum(OnePassConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OnePass] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
@@ -59,7 +60,7 @@ export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
clientId: true,
audience: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Auth0]} (Client Credentials)` }))
]);
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -85,10 +86,12 @@ export const UpdateAuth0ConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
export const Auth0ConnectionListItemSchema = z.object({
name: z.literal("Auth0"),
app: z.literal(AppConnection.Auth0),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(Auth0ConnectionMethod).array()
});
export const Auth0ConnectionListItemSchema = z
.object({
name: z.literal("Auth0"),
app: z.literal(AppConnection.Auth0),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(Auth0ConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Auth0] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AwsConnectionMethod } from "./aws-connection-enums";
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
@@ -39,11 +40,11 @@ export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema.pick({})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AWS]} (Assume Role)` })),
BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKeyId: true })
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AWS]} (Access Key)` }))
]);
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -72,11 +73,13 @@ export const UpdateAwsConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
export const AwsConnectionListItemSchema = z.object({
name: z.literal("AWS"),
app: z.literal(AppConnection.AWS),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(AwsConnectionMethod).array(),
accessKeyId: z.string().optional()
});
export const AwsConnectionListItemSchema = z
.object({
name: z.literal("AWS"),
app: z.literal(AppConnection.AWS),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(AwsConnectionMethod).array(),
accessKeyId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AWS] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
export const AzureADCSUsernamePasswordCredentialsSchema = z.object({
@@ -55,7 +56,7 @@ export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureADCS]} (Username and Password)` }))
]);
export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -81,8 +82,10 @@ export const UpdateAzureADCSConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS));
export const AzureADCSConnectionListItemSchema = z.object({
name: z.literal("Azure ADCS"),
app: z.literal(AppConnection.AzureADCS),
methods: z.nativeEnum(AzureADCSConnectionMethod).array()
});
export const AzureADCSConnectionListItemSchema = z
.object({
name: z.literal("Azure ADCS"),
app: z.literal(AppConnection.AzureADCS),
methods: z.nativeEnum(AzureADCSConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureADCS] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums";
export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({
@@ -104,19 +105,23 @@ export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUni
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration]} (OAuth)` })),
BaseAzureAppConfigurationConnectionSchema.extend({
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true,
tenantId: true
})
})
}).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration]} (Client Secret)` })
)
]);
export const AzureAppConfigurationConnectionListItemSchema = z.object({
name: z.literal("Azure App Configuration"),
app: z.literal(AppConnection.AzureAppConfiguration),
methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const AzureAppConfigurationConnectionListItemSchema = z
.object({
name: z.literal("Azure App Configuration"),
app: z.literal(AppConnection.AzureAppConfiguration),
methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({
@@ -162,26 +163,30 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (OAuth)` })),
BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true,
tenantId: true
})
}),
}).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (Client Secret)` })
),
BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.Certificate),
credentials: AzureClientSecretsConnectionCertificateOutputCredentialsSchema.pick({
tenantId: true,
clientId: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (Certificate)` }))
]);
export const AzureClientSecretsConnectionListItemSchema = z.object({
name: z.literal("Azure Client Secrets"),
app: z.literal(AppConnection.AzureClientSecrets),
methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const AzureClientSecretsConnectionListItemSchema = z
.object({
name: z.literal("Azure Client Secrets"),
app: z.literal(AppConnection.AzureClientSecrets),
methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({
@@ -147,13 +148,13 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
tenantId: true,
orgName: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (OAuth)` })),
BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
orgName: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (Access Token)` })),
BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema.pick({
@@ -161,12 +162,14 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
tenantId: true,
orgName: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (Client Secret)` }))
]);
export const AzureDevOpsConnectionListItemSchema = z.object({
name: z.literal("Azure DevOps"),
app: z.literal(AppConnection.AzureDevOps),
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const AzureDevOpsConnectionListItemSchema = z
.object({
name: z.literal("Azure DevOps"),
app: z.literal(AppConnection.AzureDevOps),
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({
@@ -104,19 +105,21 @@ export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("meth
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault]} (OAuth)` })),
BaseAzureKeyVaultConnectionSchema.extend({
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true,
tenantId: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault]} (Client Secret)` }))
]);
export const AzureKeyVaultConnectionListItemSchema = z.object({
name: z.literal("Azure Key Vault"),
app: z.literal(AppConnection.AzureKeyVault),
methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const AzureKeyVaultConnectionListItemSchema = z
.object({
name: z.literal("Azure Key Vault"),
app: z.literal(AppConnection.AzureKeyVault),
methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({
@@ -39,7 +40,7 @@ export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method",
credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({
email: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Bitbucket]} (API Token)` }))
]);
export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -65,8 +66,10 @@ export const UpdateBitbucketConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket));
export const BitbucketConnectionListItemSchema = z.object({
name: z.literal("Bitbucket"),
app: z.literal(AppConnection.Bitbucket),
methods: z.nativeEnum(BitbucketConnectionMethod).array()
});
export const BitbucketConnectionListItemSchema = z
.object({
name: z.literal("Bitbucket"),
app: z.literal(AppConnection.Bitbucket),
methods: z.nativeEnum(BitbucketConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Bitbucket] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { CamundaConnectionMethod } from "./camunda-connection-enums";
const BaseCamundaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Camunda) });
@@ -44,7 +45,7 @@ export const SanitizedCamundaConnectionSchema = z.discriminatedUnion("method", [
credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema.pick({
clientId: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Camunda]} (Client Credentials)` }))
]);
export const ValidateCamundaConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -70,8 +71,10 @@ export const UpdateCamundaConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Camunda));
export const CamundaConnectionListItemSchema = z.object({
name: z.literal("Camunda"),
app: z.literal(AppConnection.Camunda),
methods: z.nativeEnum(CamundaConnectionMethod).array()
});
export const CamundaConnectionListItemSchema = z
.object({
name: z.literal("Camunda"),
app: z.literal(AppConnection.Camunda),
methods: z.nativeEnum(CamundaConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Camunda] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
export const ChecklyConnectionMethodSchema = z
@@ -31,7 +32,7 @@ export const SanitizedChecklyConnectionSchema = z.discriminatedUnion("method", [
BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Checkly]} (Access Token)` }))
]);
export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -55,8 +56,10 @@ export const UpdateChecklyConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly));
export const ChecklyConnectionListItemSchema = z.object({
name: z.literal("Checkly"),
app: z.literal(AppConnection.Checkly),
methods: z.nativeEnum(ChecklyConnectionMethod).array()
});
export const ChecklyConnectionListItemSchema = z
.object({
name: z.literal("Checkly"),
app: z.literal(AppConnection.Checkly),
methods: z.nativeEnum(ChecklyConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Checkly] }));
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
const accountIdCharacterValidator = characterValidator([
@@ -41,7 +42,7 @@ export const SanitizedCloudflareConnectionSchema = z.discriminatedUnion("method"
BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true })
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Cloudflare]} (API Token)` }))
]);
export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -67,8 +68,10 @@ export const UpdateCloudflareConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare));
export const CloudflareConnectionListItemSchema = z.object({
name: z.literal("Cloudflare"),
app: z.literal(AppConnection.Cloudflare),
methods: z.nativeEnum(CloudflareConnectionMethod).array()
});
export const CloudflareConnectionListItemSchema = z
.object({
name: z.literal("Cloudflare"),
app: z.literal(AppConnection.Cloudflare),
methods: z.nativeEnum(CloudflareConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Cloudflare] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { DatabricksConnectionMethod } from "./databricks-connection-enums";
export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({
@@ -42,7 +43,7 @@ export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method"
clientId: true,
workspaceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Databricks]} (Service Principal)` }))
]);
export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -68,10 +69,12 @@ export const UpdateDatabricksConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks));
export const DatabricksConnectionListItemSchema = z.object({
name: z.literal("Databricks"),
app: z.literal(AppConnection.Databricks),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(DatabricksConnectionMethod).array()
});
export const DatabricksConnectionListItemSchema = z
.object({
name: z.literal("Databricks"),
app: z.literal(AppConnection.Databricks),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(DatabricksConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Databricks] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
export const DigitalOceanConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("metho
BaseDigitalOceanConnectionSchema.extend({
method: DigitalOceanConnectionMethodSchema,
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DigitalOcean]} (Access Token)` }))
]);
export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,8 +61,10 @@ export const UpdateDigitalOceanConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean));
export const DigitalOceanConnectionListItemSchema = z.object({
name: z.literal("Digital Ocean"),
app: z.literal(AppConnection.DigitalOcean),
methods: z.nativeEnum(DigitalOceanConnectionMethod).array()
});
export const DigitalOceanConnectionListItemSchema = z
.object({
name: z.literal("Digital Ocean"),
app: z.literal(AppConnection.DigitalOcean),
methods: z.nativeEnum(DigitalOceanConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DigitalOcean] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { FlyioConnectionMethod } from "./flyio-connection-enums";
export const FlyioConnectionAccessTokenCredentialsSchema = z.object({
@@ -31,7 +32,7 @@ export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [
BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Flyio]} (Access Token)` }))
]);
export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -55,8 +56,10 @@ export const UpdateFlyioConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio));
export const FlyioConnectionListItemSchema = z.object({
name: z.literal("Fly.io"),
app: z.literal(AppConnection.Flyio),
methods: z.nativeEnum(FlyioConnectionMethod).array()
});
export const FlyioConnectionListItemSchema = z
.object({
name: z.literal("Fly.io"),
app: z.literal(AppConnection.Flyio),
methods: z.nativeEnum(FlyioConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Flyio] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GcpConnectionMethod } from "./gcp-connection-enums";
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
@@ -30,7 +31,9 @@ export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
BaseGcpConnectionSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
})
}).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GCP]} (Service Account Impersonation)` })
)
]);
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -56,10 +59,12 @@ export const UpdateGcpConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
export const GcpConnectionListItemSchema = z.object({
name: z.literal("GCP"),
app: z.literal(AppConnection.GCP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GcpConnectionMethod).array()
});
export const GcpConnectionListItemSchema = z
.object({
name: z.literal("GCP"),
app: z.literal(AppConnection.GCP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GcpConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GCP] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
export const GitHubRadarConnectionInputCredentialsSchema = z.object({
@@ -53,14 +54,16 @@ export const SanitizedGitHubRadarConnectionSchema = z.discriminatedUnion("method
BaseGitHubRadarConnectionSchema.extend({
method: z.literal(GitHubRadarConnectionMethod.App),
credentials: GitHubRadarConnectionOutputCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHubRadar]} (GitHub App)` }))
]);
export const GitHubRadarConnectionListItemSchema = z.object({
name: z.literal("GitHub Radar"),
app: z.literal(AppConnection.GitHubRadar),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubRadarConnectionMethod).array(),
appClientSlug: z.string().optional()
});
export const GitHubRadarConnectionListItemSchema = z
.object({
name: z.literal("GitHub Radar"),
app: z.literal(AppConnection.GitHubRadar),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubRadarConnectionMethod).array(),
appClientSlug: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitHubRadar] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.union([
@@ -161,29 +162,31 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (GitHub App)` })),
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (OAuth)` })),
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.Pat),
credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (Personal Access Token)` }))
]);
export const GitHubConnectionListItemSchema = z.object({
name: z.literal("GitHub"),
app: z.literal(AppConnection.GitHub),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubConnectionMethod).array(),
oauthClientId: z.string().optional(),
appClientSlug: z.string().optional()
});
export const GitHubConnectionListItemSchema = z
.object({
name: z.literal("GitHub"),
app: z.literal(AppConnection.GitHub),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GitHubConnectionMethod).array(),
oauthClientId: z.string().optional(),
appClientSlug: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitHub] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums";
export const GitLabConnectionAccessTokenCredentialsSchema = z.object({
@@ -84,13 +85,13 @@ export const SanitizedGitLabConnectionSchema = z.discriminatedUnion("method", [
instanceUrl: true,
accessTokenType: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitLab]} (Access Token)` })),
BaseGitLabConnectionSchema.extend({
method: z.literal(GitLabConnectionMethod.OAuth),
credentials: GitLabConnectionOAuthOutputCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitLab]} (OAuth)` }))
]);
export const ValidateGitLabConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -130,9 +131,11 @@ export const UpdateGitLabConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitLab));
export const GitLabConnectionListItemSchema = z.object({
name: z.literal("GitLab"),
app: z.literal(AppConnection.GitLab),
methods: z.nativeEnum(GitLabConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const GitLabConnectionListItemSchema = z
.object({
name: z.literal("GitLab"),
app: z.literal(AppConnection.GitLab),
methods: z.nativeEnum(GitLabConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitLab] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
const InstanceUrlSchema = z
@@ -59,7 +60,7 @@ export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
namespace: true,
instanceUrl: true
})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.HCVault]} (Access Token)` })),
BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AppRole),
credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({
@@ -67,7 +68,7 @@ export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
instanceUrl: true,
roleId: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.HCVault]} (App Role)` }))
]);
export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -100,8 +101,10 @@ export const UpdateHCVaultConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true }));
export const HCVaultConnectionListItemSchema = z.object({
name: z.literal("HCVault"),
app: z.literal(AppConnection.HCVault),
methods: z.nativeEnum(HCVaultConnectionMethod).array()
});
export const HCVaultConnectionListItemSchema = z
.object({
name: z.literal("HCVault"),
app: z.literal(AppConnection.HCVault),
methods: z.nativeEnum(HCVaultConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.HCVault] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HerokuConnectionMethod } from "./heroku-connection-enums";
export const HerokuConnectionAuthTokenCredentialsSchema = z.object({
@@ -51,11 +52,11 @@ export const SanitizedHerokuConnectionSchema = z.discriminatedUnion("method", [
BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.AuthToken),
credentials: HerokuConnectionAuthTokenCredentialsSchema.pick({})
}),
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Heroku]} (Auth Token)` })),
BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.OAuth),
credentials: HerokuConnectionOAuthOutputCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Heroku]} (OAuth)` }))
]);
export const ValidateHerokuConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -95,9 +96,11 @@ export const UpdateHerokuConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Heroku));
export const HerokuConnectionListItemSchema = z.object({
name: z.literal("Heroku"),
app: z.literal(AppConnection.Heroku),
methods: z.nativeEnum(HerokuConnectionMethod).array(),
oauthClientId: z.string().optional()
});
export const HerokuConnectionListItemSchema = z
.object({
name: z.literal("Heroku"),
app: z.literal(AppConnection.Heroku),
methods: z.nativeEnum(HerokuConnectionMethod).array(),
oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Heroku] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HumanitecConnectionMethod } from "./humanitec-connection-enums";
export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method",
BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.ApiToken),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Humanitec]} (API Token)` }))
]);
export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -51,8 +52,10 @@ export const UpdateHumanitecConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec));
export const HumanitecConnectionListItemSchema = z.object({
name: z.literal("Humanitec"),
app: z.literal(AppConnection.Humanitec),
methods: z.nativeEnum(HumanitecConnectionMethod).array()
});
export const HumanitecConnectionListItemSchema = z
.object({
name: z.literal("Humanitec"),
app: z.literal(AppConnection.Humanitec),
methods: z.nativeEnum(HumanitecConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Humanitec] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { LaravelForgeConnectionMethod } from "./laravel-forge-connection-enums";
export const LaravelForgeConnectionApiTokenCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedLaravelForgeConnectionSchema = z.discriminatedUnion("metho
BaseLaravelForgeConnectionSchema.extend({
method: z.literal(LaravelForgeConnectionMethod.ApiToken),
credentials: LaravelForgeConnectionApiTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.LaravelForge]} (API Token)` }))
]);
export const ValidateLaravelForgeConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -51,8 +52,10 @@ export const UpdateLaravelForgeConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LaravelForge));
export const LaravelForgeConnectionListItemSchema = z.object({
name: z.literal("Laravel Forge"),
app: z.literal(AppConnection.LaravelForge),
methods: z.nativeEnum(LaravelForgeConnectionMethod).array()
});
export const LaravelForgeConnectionListItemSchema = z
.object({
name: z.literal("Laravel Forge"),
app: z.literal(AppConnection.LaravelForge),
methods: z.nativeEnum(LaravelForgeConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.LaravelForge] }));
@@ -1,6 +1,11 @@
import ldap from "ldapjs";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { GatewayProxyProtocol } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -8,6 +13,66 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
const LDAP_TIMEOUT = 15_000;
const parseLdapUrl = (url: string): { protocol: string; host: string; port: number } => {
const urlObj = new URL(url);
const isSSL = urlObj.protocol === "ldaps:";
const defaultPort = isSSL ? 636 : 389;
return {
protocol: urlObj.protocol.replace(":", ""),
host: urlObj.hostname,
port: urlObj.port ? parseInt(urlObj.port, 10) : defaultPort
};
};
const constructLdapUrl = (protocol: string, host: string, port: number): string => {
return `${protocol}://${host}:${port}`;
};
const setupLdapClientHandlers = <T>(
client: ldap.Client,
dn: string,
password: string,
onSuccess: (client: ldap.Client) => T | Promise<T>
): Promise<T> => {
return new Promise<T>((resolve, reject) => {
const handleError = (errorType: string, err: Error) => {
logger.error(err, errorType);
client.destroy();
reject(new Error(`${errorType.replace("LDAP ", "")} - ${err.message}`));
};
client.on("error", (err: Error) => handleError("LDAP Error", err));
client.on("connectError", (err: Error) => handleError("LDAP Connection Error", err));
client.on("connectRefused", (err: Error) => handleError("LDAP Connection Refused", err));
client.on("connectTimeout", (err: Error) => handleError("LDAP Connection Timeout", err));
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
client.destroy();
reject(new Error(`Bind Error: ${err.message}`));
return;
}
try {
const result = onSuccess(client);
if (result instanceof Promise) {
result.then((value) => resolve(value)).catch(reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
});
});
});
};
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
@@ -16,8 +81,6 @@ export const getLdapConnectionListItem = () => {
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
@@ -25,78 +88,112 @@ export const getLdapConnectionClient = async ({
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
await blockLocalAndPrivateIpAddresses(url, false);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => {
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
resolve(client);
});
});
: undefined
});
return setupLdapClientHandlers<ldap.Client>(client, dn, password, (ldapClient) => ldapClient);
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
export const executeWithPotentialGateway = async <T>(
config: TLdapConnectionConfig,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (client: ldap.Client) => Promise<T>
): Promise<T> => {
const { gatewayId, credentials } = config;
const { protocol, host, port } = parseLdapUrl(credentials.url);
const appCfg = getConfig();
try {
client = await getLdapConnectionClient(credentials);
if (gatewayId && gatewayV2Service) {
await blockLocalAndPrivateIpAddresses(credentials.url, true);
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId,
targetHost: host,
targetPort: port
});
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
if (!platformConnectionDetails) {
throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
});
return withGatewayV2Proxy(
async (proxyPort) => {
const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort);
const isSSL = protocol === "ldaps";
const client = ldap.createClient({
url: proxyUrl,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: config.credentials.sslRejectUnauthorized,
ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined,
servername: host,
// bypass hostname verification for development
...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {})
}
: undefined
});
return setupLdapClientHandlers<T>(client, credentials.dn, credentials.password, async (ldapClient) => {
try {
return await operation(ldapClient);
} finally {
ldapClient.destroy();
}
});
},
{
protocol: GatewayProxyProtocol.Tcp,
relayHost: platformConnectionDetails.relayHost,
gateway: platformConnectionDetails.gateway,
relay: platformConnectionDetails.relay
}
);
}
// Non-gateway path - calls getLdapConnectionClient which has validation
const client = await getLdapConnectionClient(credentials);
try {
return await operation(client);
} finally {
client?.destroy();
client.destroy();
}
};
export const validateLdapConnectionCredentials = async (
config: TLdapConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
try {
await executeWithPotentialGateway(config, gatewayV2Service, async (client) => {
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
});
return config.credentials;
} catch (error) {
throw new BadRequestError({
message: `Unable to validate connection: ${
(error as Error)?.message?.replaceAll(config.credentials.password, "********************") ??
"verify credentials"
}`
});
}
};
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
@@ -61,7 +62,7 @@ export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.LDAP]} (Simple Bind)` }))
]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -74,7 +75,9 @@ export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("met
]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP, {
supportsGateways: true
})
);
export const UpdateLdapConnectionSchema = z
@@ -83,12 +86,18 @@ export const UpdateLdapConnectionSchema = z
AppConnections.UPDATE(AppConnection.LDAP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP, {
supportsGateways: true
})
);
export const LdapConnectionListItemSchema = z.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.LDAP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
});
export const LdapConnectionListItemSchema = z
.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.LDAP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.LDAP] }));
@@ -17,6 +17,9 @@ export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> &
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
export type TLdapConnectionConfig = DiscriminativePick<
TLdapConnectionInput,
"method" | "app" | "credentials" | "gatewayId"
> & {
orgId: string;
};
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { MsSqlConnectionMethod } from "./mssql-connection-enums";
@@ -34,7 +35,7 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.MsSql]} (Username and Password)` }))
]);
export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -68,9 +69,11 @@ export const UpdateMsSqlConnectionSchema = z
})
);
export const MsSqlConnectionListItemSchema = z.object({
name: z.literal("Microsoft SQL Server"),
app: z.literal(AppConnection.MsSql),
methods: z.nativeEnum(MsSqlConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});
export const MsSqlConnectionListItemSchema = z
.object({
name: z.literal("Microsoft SQL Server"),
app: z.literal(AppConnection.MsSql),
methods: z.nativeEnum(MsSqlConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.MsSql] }));
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { MySqlConnectionMethod } from "./mysql-connection-enums";
@@ -32,7 +33,7 @@ export const SanitizedMySqlConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.MySql]} (Username and Password)` }))
]);
export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -66,9 +67,11 @@ export const UpdateMySqlConnectionSchema = z
})
);
export const MySqlConnectionListItemSchema = z.object({
name: z.literal("MySQL"),
app: z.literal(AppConnection.MySql),
methods: z.nativeEnum(MySqlConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});
export const MySqlConnectionListItemSchema = z
.object({
name: z.literal("MySQL"),
app: z.literal(AppConnection.MySql),
methods: z.nativeEnum(MySqlConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.MySql] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { NetlifyConnectionMethod } from "./netlify-connection-constants";
export const NetlifyConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedNetlifyConnectionSchema = z.discriminatedUnion("method", [
BaseNetlifyConnectionSchema.extend({
method: NetlifyConnectionMethodSchema,
credentials: NetlifyConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Netlify]} (Access Token)` }))
]);
export const ValidateNetlifyConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,8 +61,10 @@ export const UpdateNetlifyConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Netlify));
export const NetlifyConnectionListItemSchema = z.object({
name: z.literal("Netlify"),
app: z.literal(AppConnection.Netlify),
methods: z.nativeEnum(NetlifyConnectionMethod).array()
});
export const NetlifyConnectionListItemSchema = z
.object({
name: z.literal("Netlify"),
app: z.literal(AppConnection.Netlify),
methods: z.nativeEnum(NetlifyConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Netlify] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { NorthflankConnectionMethod } from "./northflank-connection-enums";
export const NorthflankConnectionApiTokenCredentialsSchema = z.object({
@@ -27,7 +28,7 @@ export const SanitizedNorthflankConnectionSchema = z.discriminatedUnion("method"
BaseNorthflankConnectionSchema.extend({
method: z.literal(NorthflankConnectionMethod.ApiToken),
credentials: NorthflankConnectionApiTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Northflank]} (API Token)` }))
]);
export const ValidateNorthflankConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -53,8 +54,10 @@ export const UpdateNorthflankConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Northflank));
export const NorthflankConnectionListItemSchema = z.object({
name: z.literal("Northflank"),
app: z.literal(AppConnection.Northflank),
methods: z.nativeEnum(NorthflankConnectionMethod).array()
});
export const NorthflankConnectionListItemSchema = z
.object({
name: z.literal("Northflank"),
app: z.literal(AppConnection.Northflank),
methods: z.nativeEnum(NorthflankConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Northflank] }));
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { OktaConnectionMethod } from "./okta-connection-enums";
export const OktaConnectionApiTokenCredentialsSchema = z.object({
@@ -40,7 +41,7 @@ export const SanitizedOktaConnectionSchema = z.discriminatedUnion("method", [
credentials: OktaConnectionApiTokenCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Okta]} (API Token)` }))
]);
export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -62,8 +63,10 @@ export const UpdateOktaConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta));
export const OktaConnectionListItemSchema = z.object({
name: z.literal("Okta"),
app: z.literal(AppConnection.Okta),
methods: z.nativeEnum(OktaConnectionMethod).array()
});
export const OktaConnectionListItemSchema = z
.object({
name: z.literal("Okta"),
app: z.literal(AppConnection.Okta),
methods: z.nativeEnum(OktaConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Okta] }));
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { PostgresConnectionMethod } from "./postgres-connection-enums";
@@ -32,7 +33,7 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Postgres]} (Username and Password)` }))
]);
export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -66,9 +67,11 @@ export const UpdatePostgresConnectionSchema = z
})
);
export const PostgresConnectionListItemSchema = z.object({
name: z.literal("PostgreSQL"),
app: z.literal(AppConnection.Postgres),
methods: z.nativeEnum(PostgresConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});
export const PostgresConnectionListItemSchema = z
.object({
name: z.literal("PostgreSQL"),
app: z.literal(AppConnection.Postgres),
methods: z.nativeEnum(PostgresConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Postgres] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RailwayConnectionMethod } from "./railway-connection-constants";
export const RailwayConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedRailwayConnectionSchema = z.discriminatedUnion("method", [
BaseRailwayConnectionSchema.extend({
method: RailwayConnectionMethodSchema,
credentials: RailwayConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Railway]} (Access Token)` }))
]);
export const ValidateRailwayConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,11 +61,13 @@ export const UpdateRailwayConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Railway));
export const RailwayConnectionListItemSchema = z.object({
name: z.literal("Railway"),
app: z.literal(AppConnection.Railway),
methods: z.nativeEnum(RailwayConnectionMethod).array()
});
export const RailwayConnectionListItemSchema = z
.object({
name: z.literal("Railway"),
app: z.literal(AppConnection.Railway),
methods: z.nativeEnum(RailwayConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Railway] }));
export const RailwayResourceSchema = z.object({
node: z.object({
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RedisConnectionMethod } from "./redis-connection-enums";
export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
@@ -45,7 +46,7 @@ export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true,
sslCertificate: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Redis]} (Username and Password)` }))
]);
export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -79,9 +80,11 @@ export const UpdateRedisConnectionSchema = z
})
);
export const RedisConnectionListItemSchema = z.object({
name: z.literal("Redis"),
app: z.literal(AppConnection.Redis),
methods: z.nativeEnum(RedisConnectionMethod).array(),
supportsPlatformManagement: z.literal(false)
});
export const RedisConnectionListItemSchema = z
.object({
name: z.literal("Redis"),
app: z.literal(AppConnection.Redis),
methods: z.nativeEnum(RedisConnectionMethod).array(),
supportsPlatformManagement: z.literal(false)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Redis] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RenderConnectionMethod } from "./render-connection-enums";
export const RenderConnectionApiKeyCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedRenderConnectionSchema = z.discriminatedUnion("method", [
BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Render]} (API Key)` }))
]);
export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -49,8 +50,10 @@ export const UpdateRenderConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render));
export const RenderConnectionListItemSchema = z.object({
name: z.literal("Render"),
app: z.literal(AppConnection.Render),
methods: z.nativeEnum(RenderConnectionMethod).array()
});
export const RenderConnectionListItemSchema = z
.object({
name: z.literal("Render"),
app: z.literal(AppConnection.Render),
methods: z.nativeEnum(RenderConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Render] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
export const SupabaseConnectionMethodSchema = z
@@ -39,7 +40,7 @@ export const SanitizedSupabaseConnectionSchema = z.discriminatedUnion("method",
credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Supabase]} (Access Token)` }))
]);
export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -63,8 +64,10 @@ export const UpdateSupabaseConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase));
export const SupabaseConnectionListItemSchema = z.object({
name: z.literal("Supabase"),
app: z.literal(AppConnection.Supabase),
methods: z.nativeEnum(SupabaseConnectionMethod).array()
});
export const SupabaseConnectionListItemSchema = z
.object({
name: z.literal("Supabase"),
app: z.literal(AppConnection.Supabase),
methods: z.nativeEnum(SupabaseConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Supabase] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
@@ -37,7 +38,7 @@ export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method",
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.TeamCity]} (Access Token)` }))
]);
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -63,8 +64,10 @@ export const UpdateTeamCityConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
export const TeamCityConnectionListItemSchema = z.object({
name: z.literal("TeamCity"),
app: z.literal(AppConnection.TeamCity),
methods: z.nativeEnum(TeamCityConnectionMethod).array()
});
export const TeamCityConnectionListItemSchema = z
.object({
name: z.literal("TeamCity"),
app: z.literal(AppConnection.TeamCity),
methods: z.nativeEnum(TeamCityConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.TeamCity] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({
@@ -27,7 +28,7 @@ export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("met
BaseTerraformCloudConnectionSchema.extend({
method: z.literal(TerraformCloudConnectionMethod.ApiToken),
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.TerraformCloud]} (API Token)` }))
]);
export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -53,8 +54,10 @@ export const UpdateTerraformCloudConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud));
export const TerraformCloudConnectionListItemSchema = z.object({
name: z.literal("Terraform Cloud"),
app: z.literal(AppConnection.TerraformCloud),
methods: z.nativeEnum(TerraformCloudConnectionMethod).array()
});
export const TerraformCloudConnectionListItemSchema = z
.object({
name: z.literal("Terraform Cloud"),
app: z.literal(AppConnection.TerraformCloud),
methods: z.nativeEnum(TerraformCloudConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.TerraformCloud] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { VercelConnectionMethod } from "./vercel-connection-enums";
export const VercelConnectionAccessTokenCredentialsSchema = z.object({
@@ -27,7 +28,7 @@ export const SanitizedVercelConnectionSchema = z.discriminatedUnion("method", [
BaseVercelConnectionSchema.extend({
method: z.literal(VercelConnectionMethod.ApiToken),
credentials: VercelConnectionAccessTokenCredentialsSchema.pick({})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Vercel]} (API Token)` }))
]);
export const ValidateVercelConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -51,8 +52,10 @@ export const UpdateVercelConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Vercel));
export const VercelConnectionListItemSchema = z.object({
name: z.literal("Vercel"),
app: z.literal(AppConnection.Vercel),
methods: z.nativeEnum(VercelConnectionMethod).array()
});
export const VercelConnectionListItemSchema = z
.object({
name: z.literal("Vercel"),
app: z.literal(AppConnection.Vercel),
methods: z.nativeEnum(VercelConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Vercel] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { WindmillConnectionMethod } from "./windmill-connection-enums";
export const WindmillConnectionAccessTokenCredentialsSchema = z.object({
@@ -37,7 +38,7 @@ export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method",
credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Windmill]} (Access Token)` }))
]);
export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -63,8 +64,10 @@ export const UpdateWindmillConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill));
export const WindmillConnectionListItemSchema = z.object({
name: z.literal("Windmill"),
app: z.literal(AppConnection.Windmill),
methods: z.nativeEnum(WindmillConnectionMethod).array()
});
export const WindmillConnectionListItemSchema = z
.object({
name: z.literal("Windmill"),
app: z.literal(AppConnection.Windmill),
methods: z.nativeEnum(WindmillConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Windmill] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { ZabbixConnectionMethod } from "./zabbix-connection-enums";
export const ZabbixConnectionApiTokenCredentialsSchema = z.object({
@@ -31,7 +32,7 @@ export const SanitizedZabbixConnectionSchema = z.discriminatedUnion("method", [
BaseZabbixConnectionSchema.extend({
method: z.literal(ZabbixConnectionMethod.ApiToken),
credentials: ZabbixConnectionApiTokenCredentialsSchema.pick({ instanceUrl: true })
})
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Zabbix]} (API Token)` }))
]);
export const ValidateZabbixConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -55,8 +56,10 @@ export const UpdateZabbixConnectionSchema = z
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Zabbix));
export const ZabbixConnectionListItemSchema = z.object({
name: z.literal("Zabbix"),
app: z.literal(AppConnection.Zabbix),
methods: z.nativeEnum(ZabbixConnectionMethod).array()
});
export const ZabbixConnectionListItemSchema = z
.object({
name: z.literal("Zabbix"),
app: z.literal(AppConnection.Zabbix),
methods: z.nativeEnum(ZabbixConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Zabbix] }));
@@ -454,6 +454,30 @@ export const authLoginServiceFactory = ({
});
}
if (organizationId) {
await auditLogService.createAuditLog({
orgId: organizationId,
ipAddress: ip,
userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: userEnc.email,
userId: userEnc.userId,
username: userEnc.username,
authMethod
}
},
event: {
type: EventType.USER_LOGIN,
metadata: {
organizationId
}
}
});
}
return {
tokens: {
accessToken: token.access,
@@ -646,6 +670,29 @@ export const authLoginServiceFactory = ({
}
}
await auditLogService.createAuditLog({
orgId: organizationId,
ipAddress,
userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: user.email,
userId: user.id,
username: user.username,
authMethod: decodedToken.authMethod
}
},
event: {
type: EventType.SELECT_ORGANIZATION,
metadata: {
organizationId,
organizationName: selectedOrg.name
}
}
});
return {
...tokens,
user,
@@ -1039,6 +1086,33 @@ export const authLoginServiceFactory = ({
organizationId
});
if (organizationId) {
await auditLogService.createAuditLog({
orgId: organizationId,
ipAddress: ip,
userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: userEnc.email,
userId: userEnc.userId,
username: userEnc.username,
authMethod: decodedProviderToken.authMethod
}
},
event: {
type: EventType.USER_LOGIN,
metadata: {
organizationId,
...(isAuthMethodSaml(decodedProviderToken.authMethod) && {
authProvider: decodedProviderToken.authMethod
})
}
}
});
}
return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const;
};
@@ -1,7 +1,9 @@
import * as x509 from "@peculiar/x509";
import acme from "acme-client";
import acme, { CsrBuffer } from "acme-client";
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, CryptographyError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
@@ -64,6 +66,20 @@ type TAcmeCertificateAuthorityFnsDeps = {
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type TOrderCertificateDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type DBConfigurationColumn = {
dnsProvider: string;
directoryUrl: string;
@@ -104,6 +120,245 @@ export const castDbEntryToAcmeCertificateAuthority = (
};
};
export const orderCertificate = async (
{
caId,
subscriberId,
commonName,
altNames,
csr,
csrPrivateKey,
keyUsages,
extendedKeyUsages
}: {
caId: string;
subscriberId?: string;
commonName: string;
altNames?: string[];
csr: CsrBuffer;
csrPrivateKey?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
},
deps: TOrderCertificateDeps,
tx?: Knex
) => {
const {
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
} = deps;
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
// For ACME development mode, we mock the DNS challenge API calls. So, no real DNS records are created.
// We need to disable the challenge verification to avoid errors.
skipChallengeVerification: getConfig().isAcmeDevelopmentMode && getConfig().ACME_SKIP_UPSTREAM_VALIDATION,
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = csrPrivateKey
? await kmsEncryptor({
plainText: Buffer.from(csrPrivateKey)
})
: { cipherTextBlob: undefined };
return (tx || certificateDAL).transaction(async (innerTx: Knex) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriberId,
status: CertStatus.ACTIVE,
friendlyName: commonName,
commonName,
altNames: altNames?.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages,
extendedKeyUsages,
projectId: ca.projectId
},
innerTx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
innerTx
);
if (encryptedPrivateKey !== undefined) {
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
innerTx
);
}
return cert;
});
};
export const AcmeCertificateAuthorityFns = ({
appConnectionDAL,
appConnectionService,
@@ -322,77 +577,6 @@ export const AcmeCertificateAuthorityFns = ({
if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
@@ -407,131 +591,28 @@ export const AcmeCertificateAuthorityFns = ({
skLeaf
);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr: certificateCsr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
await orderCertificate(
{
caId: subscriber.caId,
subscriberId: subscriber.id,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames,
csr: certificateCsr,
csrPrivateKey: skLeaf,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
);
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
};
@@ -85,6 +85,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
const findByIdWithConfigs = async (id: string, tx?: Knex): Promise<TCertificateProfileWithConfigs | undefined> => {
try {
const query = (tx || db)(TableName.PkiCertificateProfile)
.leftJoin(TableName.Project, `${TableName.PkiCertificateProfile}.projectId`, `${TableName.Project}.id`)
.leftJoin(
TableName.CertificateAuthority,
`${TableName.PkiCertificateProfile}.caId`,
@@ -112,6 +113,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
)
.select(selectAllTableCols(TableName.PkiCertificateProfile))
.select(
db.ref("id").withSchema(TableName.Project).as("projectId"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"),
db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"),
db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
@@ -165,15 +168,12 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
} as TCertificateProfileWithConfigs["acmeConfig"])
: undefined;
const certificateAuthority =
result.caId && result.caProjectId && result.caStatus && result.caName
? ({
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"])
: undefined;
const certificateAuthority = {
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"];
const certificateTemplate =
result.templateId && result.templateProjectId && result.templateName
@@ -185,6 +185,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
} as TCertificateProfileWithConfigs["certificateTemplate"])
: undefined;
const project = {
id: result.projectId,
orgId: result.orgId
} as TCertificateProfileWithConfigs["project"];
const transformedResult: TCertificateProfileWithConfigs = {
id: result.id,
projectId: result.projectId,
@@ -201,6 +206,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
estConfig,
apiConfig,
acmeConfig,
project,
certificateAuthority,
certificateTemplate
};
@@ -5,8 +5,9 @@
import { ForbiddenError } from "@casl/ability";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import type { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorType, AuthMethod } from "../auth/auth-type";
import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
@@ -166,6 +167,10 @@ describe("CertificateProfileService", () => {
})
} as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">;
const mockLicenseService = {
getPlan: vi.fn()
} as unknown as Pick<TLicenseServiceFactory, "getPlan">;
const mockKmsService = {
encryptWithKmsKey: vi
.fn()
@@ -252,6 +257,7 @@ describe("CertificateProfileService", () => {
certificateAuthorityDAL: mockCertificateAuthorityDAL,
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
permissionService: mockPermissionService,
licenseService: mockLicenseService,
kmsService: mockKmsService,
projectDAL: mockProjectDAL
});
@@ -275,6 +281,13 @@ describe("CertificateProfileService", () => {
};
beforeEach(() => {
(mockProjectDAL.findById as any).mockResolvedValue({
id: "project-123",
orgId: "org-123"
});
(mockLicenseService.getPlan as any).mockResolvedValue({
pkiAcme: true
});
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate);
(mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null);
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
@@ -405,6 +418,24 @@ describe("CertificateProfileService", () => {
expect(result).toEqual(sampleProfile);
expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123");
});
it("should throw BadRequestError when plan does not support ACME", async () => {
(mockLicenseService.getPlan as any).mockResolvedValue({
pkiAcme: false
});
await expect(
service.createProfile({
...mockActor,
projectId: "project-123",
data: validProfileData
})
).rejects.toThrowError(
new BadRequestError({
message: "Failed to create certificate profile: Plan restriction. Upgrade plan to continue"
})
);
});
});
describe("updateProfile", () => {
@@ -699,6 +730,13 @@ describe("CertificateProfileService", () => {
}
};
(mockProjectDAL.findById as any).mockResolvedValue({
id: "project-123",
orgId: "org-123"
});
(mockLicenseService.getPlan as any).mockResolvedValue({
pkiAcme: true
});
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate);
(mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null);
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateActions,
@@ -152,6 +153,7 @@ type TCertificateProfileServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
};
@@ -174,6 +176,7 @@ export const certificateProfileServiceFactory = ({
certificateBodyDAL,
certificateSecretDAL,
permissionService,
licenseService,
kmsService,
projectDAL
}: TCertificateProfileServiceFactoryDep) => {
@@ -205,6 +208,17 @@ export const certificateProfileServiceFactory = ({
ProjectPermissionSub.CertificateProfiles
);
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiAcme) {
throw new BadRequestError({
message: "Failed to create certificate profile: Plan restriction. Upgrade plan to continue"
});
}
// Validate that certificate template exists and belongs to the same project
if (data.certificateTemplateId) {
const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId);
@@ -33,6 +33,10 @@ export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enr
};
export type TCertificateProfileWithConfigs = TCertificateProfile & {
project?: {
id: string;
orgId: string;
};
certificateAuthority?: {
id: string;
projectId: string;
@@ -1,4 +1,5 @@
import * as x509 from "@peculiar/x509";
import forge from "node-forge";
import RE2 from "re2";
import { crypto } from "@app/lib/crypto/cryptography";
@@ -104,3 +105,53 @@ export const getCertificateCredentials = async ({
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
}
};
export const generatePkcs12FromCertificate = async ({
certificate,
certificateChain,
privateKey,
password,
alias
}: {
certificate: string;
certificateChain: string;
privateKey: string;
password: string;
alias: string;
}): Promise<Buffer> => {
try {
if (!password || password.trim() === "") {
throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" });
}
const cert = forge.pki.certificateFromPem(certificate);
const key = forge.pki.privateKeyFromPem(privateKey);
const chainCerts = [];
if (certificateChain) {
const chainPems = splitPemChain(certificateChain);
for (const chainPem of chainPems) {
try {
const chainCert = forge.pki.certificateFromPem(chainPem);
chainCerts.push(chainCert);
} catch (error) {
// Skip invalid certificates in chain
}
}
}
// Generate PKCS12 file
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(key, [cert, ...chainCerts], password, {
algorithm: "aes256", // Modern AES-256 encryption
friendlyName: alias
});
const p12Der = forge.asn1.toDer(p12Asn1).getBytes();
return Buffer.from(p12Der, "binary");
} catch (error) {
throw new BadRequestError({
message: `Failed to generate PKCS12 keystore: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
@@ -29,7 +29,12 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import {
generatePkcs12FromCertificate,
getCertificateCredentials,
revocationReasonToCrlCode,
splitPemChain
} from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertExtendedKeyUsage,
@@ -40,6 +45,7 @@ import {
TGetCertBodyDTO,
TGetCertBundleDTO,
TGetCertDTO,
TGetCertPkcs12DTO,
TGetCertPrivateKeyDTO,
TImportCertDTO,
TRevokeCertDTO
@@ -656,6 +662,71 @@ export const certificateServiceFactory = ({
};
};
const getCertPkcs12 = async ({
serialNumber,
password,
alias,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCertPkcs12DTO) => {
if (!password || password.trim() === "") {
throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" });
}
if (password.length < 6) {
throw new BadRequestError({
message: "Password must be at least 6 characters long for PKCS12 keystore security"
});
}
if (!alias || alias.trim() === "") {
throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" });
}
const cert = await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
// Get certificate bundle (certificate, chain, private key)
const { certificate, certificateChain, privateKey } = await getCertBundle({
serialNumber,
actor,
actorId,
actorAuthMethod,
actorOrgId
});
if (!privateKey) {
throw new BadRequestError({ message: "Certificate private key is required for PKCS12 export" });
}
const pkcs12Data = await generatePkcs12FromCertificate({
certificate,
certificateChain: certificateChain || "",
privateKey,
password,
alias
});
return {
pkcs12Data,
cert
};
};
return {
getCert,
getCertPrivateKey,
@@ -663,6 +734,7 @@ export const certificateServiceFactory = ({
revokeCert,
getCertBody,
importCert,
getCertBundle
getCertBundle,
getCertPkcs12
};
};
@@ -119,6 +119,12 @@ export type TGetCertBundleDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertPkcs12DTO = {
serialNumber: string;
password: string;
alias: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertificateCredentialsDTO = {
certId: string;
projectId: string;
@@ -186,8 +186,8 @@ export const identityAccessTokenServiceFactory = ({
const fnValidateIdentityAccessToken = async (
token: TIdentityAccessTokenJwtPayload,
subOrganizationSelector?: string,
ipAddress?: string
ipAddress?: string,
subOrganizationSelector?: string
) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId,
@@ -227,27 +227,25 @@ export const identityAccessTokenServiceFactory = ({
});
if (!identityOrgMembership) {
throw new BadRequestError({ message: "Identity does not belong to any organization" });
throw new BadRequestError({ message: "Identity does not belong to this organization" });
}
orgId = subOrganization.id;
orgName = subOrganization.name;
parentOrgId = subOrganization.parentOrgId as string;
} else {
const organization = await orgDAL.findOne({ id: rootOrgId });
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: rootOrgId
scopeOrgId: identityOrgDetails.id
});
if (!identityOrgMembership) {
throw new BadRequestError({ message: "Identity does not belong to any organization" });
throw new BadRequestError({ message: "Identity does not belong to this organization" });
}
orgId = rootOrgId;
orgName = organization.name;
orgId = identityOrgDetails.id;
orgName = identityOrgDetails.name;
parentOrgId = rootOrgId;
}
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
@@ -11,6 +11,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto";
@@ -51,7 +52,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = {
>;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
@@ -105,7 +106,18 @@ export const identityAliCloudAuthServiceFactory = ({
// Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
identity.projectId
? {
scope: AccessScope.Project,
scopeOrgId: identity.orgId,
scopeProjectId: identity.projectId,
actorIdentityId: identity.id
}
: {
scope: AccessScope.Organization,
scopeOrgId: identity.orgId,
actorIdentityId: identity.id
},
{
lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH,
lastLoginTime: new Date()
@@ -200,7 +212,7 @@ export const identityAliCloudAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -214,16 +226,34 @@ export const identityAliCloudAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionIdentityActions.Create,
OrgPermissionSubjects.Identity
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
@@ -280,7 +310,7 @@ export const identityAliCloudAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -300,16 +330,31 @@ export const identityAliCloudAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
@@ -350,7 +395,7 @@ export const identityAliCloudAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -362,15 +407,31 @@ export const identityAliCloudAuthServiceFactory = ({
const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
}
return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId };
};
@@ -389,7 +450,7 @@ export const identityAliCloudAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) {
@@ -397,45 +458,61 @@ export const identityAliCloudAuthServiceFactory = ({
message: "The identity does not have Alibaba Cloud auth"
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke Alibaba Cloud auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.RevokeAuth,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke Alibaba Cloud auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => {
const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx);
@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import axios from "axios";
import RE2 from "re2";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
@@ -12,6 +13,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import {
@@ -50,7 +52,7 @@ type TIdentityAwsAuthServiceFactoryDep = {
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
@@ -179,7 +181,18 @@ export const identityAwsAuthServiceFactory = ({
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
identity.projectId
? {
scope: AccessScope.Project,
scopeOrgId: identity.orgId,
scopeProjectId: identity.projectId,
actorIdentityId: identity.id
}
: {
scope: AccessScope.Organization,
scopeOrgId: identity.orgId,
actorIdentityId: identity.id
},
{
lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH,
lastLoginTime: new Date()
@@ -286,7 +299,7 @@ export const identityAwsAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -300,16 +313,34 @@ export const identityAwsAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId,
scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId,
scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionIdentityActions.Create,
OrgPermissionSubjects.Identity
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
@@ -370,7 +401,7 @@ export const identityAwsAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -389,16 +420,31 @@ export const identityAwsAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
@@ -441,7 +487,7 @@ export const identityAwsAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -453,15 +499,31 @@ export const identityAwsAuthServiceFactory = ({
const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
}
return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId };
};
@@ -480,7 +542,7 @@ export const identityAwsAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
@@ -488,45 +550,60 @@ export const identityAwsAuthServiceFactory = ({
message: "The identity does not have aws auth"
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke aws auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.RevokeAuth,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke aws auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AWS_AUTH }, tx);
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
@@ -9,6 +9,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import {
@@ -46,7 +47,7 @@ type TIdentityAzureAuthServiceFactoryDep = {
>;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
@@ -99,7 +100,18 @@ export const identityAzureAuthServiceFactory = ({
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
identity.projectId
? {
scope: AccessScope.Project,
scopeOrgId: identity.orgId,
scopeProjectId: identity.projectId,
actorIdentityId: identity.id
}
: {
scope: AccessScope.Organization,
scopeOrgId: identity.orgId,
actorIdentityId: identity.id
},
{
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
lastLoginTime: new Date()
@@ -192,7 +204,7 @@ export const identityAzureAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -205,16 +217,34 @@ export const identityAzureAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionIdentityActions.Create,
OrgPermissionSubjects.Identity
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
@@ -275,7 +305,7 @@ export const identityAzureAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
@@ -293,16 +323,31 @@ export const identityAzureAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
@@ -348,7 +393,7 @@ export const identityAzureAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
@@ -359,16 +404,31 @@ export const identityAzureAuthServiceFactory = ({
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
}
return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId };
};
@@ -387,7 +447,7 @@ export const identityAzureAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
@@ -395,43 +455,59 @@ export const identityAzureAuthServiceFactory = ({
message: "The identity does not have azure auth"
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke azure auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.RevokeAuth,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke azure auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AZURE_AUTH }, tx);
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { AccessScope, ActionProjectType, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
@@ -9,6 +9,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import {
@@ -44,7 +45,7 @@ type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
@@ -139,7 +140,18 @@ export const identityGcpAuthServiceFactory = ({
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
identity.projectId
? {
scope: AccessScope.Project,
scopeOrgId: identity.orgId,
scopeProjectId: identity.projectId,
actorIdentityId: identity.id
}
: {
scope: AccessScope.Organization,
scopeOrgId: identity.orgId,
actorIdentityId: identity.id
},
{
lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH,
lastLoginTime: new Date()
@@ -232,7 +244,7 @@ export const identityGcpAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -246,16 +258,34 @@ export const identityGcpAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionIdentityActions.Create,
OrgPermissionSubjects.Identity
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
@@ -317,7 +347,7 @@ export const identityGcpAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -336,16 +366,31 @@ export const identityGcpAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
@@ -392,7 +437,7 @@ export const identityGcpAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -404,16 +449,31 @@ export const identityGcpAuthServiceFactory = ({
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
}
return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId };
};
@@ -432,7 +492,7 @@ export const identityGcpAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -441,43 +501,58 @@ export const identityGcpAuthServiceFactory = ({
message: "The identity does not have gcp auth"
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke gcp auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.RevokeAuth,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke gcp auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.GCP_AUTH }, tx);
@@ -1,10 +1,16 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
import {
AccessScope,
ActionProjectType,
IdentityAuthMethod,
OrganizationActionScope,
TIdentityJwtAuthsUpdate
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
@@ -12,6 +18,7 @@ import {
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import {
@@ -50,7 +57,7 @@ type TIdentityJwtAuthServiceFactoryDep = {
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">;
@@ -213,8 +220,22 @@ export const identityJwtAuthServiceFactory = ({
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() },
identity.projectId
? {
scope: AccessScope.Project,
scopeOrgId: identity.orgId,
scopeProjectId: identity.projectId,
actorIdentityId: identity.id
}
: {
scope: AccessScope.Organization,
scopeOrgId: identity.orgId,
actorIdentityId: identity.id
},
{
lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
@@ -309,7 +330,7 @@ export const identityJwtAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
@@ -322,16 +343,35 @@ export const identityJwtAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Create,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionIdentityActions.Create,
OrgPermissionSubjects.Identity
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
@@ -416,7 +456,7 @@ export const identityJwtAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -435,17 +475,32 @@ export const identityJwtAuthServiceFactory = ({
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
}
const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
@@ -524,7 +579,7 @@ export const identityJwtAuthServiceFactory = ({
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -534,17 +589,32 @@ export const identityJwtAuthServiceFactory = ({
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
}
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
@@ -576,7 +646,7 @@ export const identityJwtAuthServiceFactory = ({
if (!identityMembershipOrg) {
throw new NotFoundError({ message: "Failed to find identity" });
}
if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) {
if (identityMembershipOrg.identity.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" });
}
@@ -586,45 +656,60 @@ export const identityJwtAuthServiceFactory = ({
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke jwt auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
if (identityMembershipOrg.identity.projectId) {
const { permission } = await permissionService.getProjectPermission({
actionProjectType: ActionProjectType.Any,
actor,
actorId,
projectId: identityMembershipOrg.identity.projectId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.RevokeAuth,
subject(ProjectPermissionSub.Identity, { identityId })
);
} else {
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId: identityMembershipOrg.identity.id,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId);
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke jwt auth of identity with more privileged role",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx);

Some files were not shown because too many files have changed in this diff Show More