mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Move ssh out to org level
This commit is contained in:
@@ -5,7 +5,7 @@ import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -77,7 +77,10 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@@ -123,14 +126,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}: TCreateCaDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
let projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -161,7 +174,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
type,
|
||||
organization,
|
||||
ou,
|
||||
@@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@@ -323,13 +336,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -348,13 +362,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -434,13 +449,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -819,13 +835,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -965,13 +982,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -1127,13 +1145,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -1455,13 +1474,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ca.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { ProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -67,13 +67,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
message: `CA with ID ${caId} not found`
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -128,13 +129,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -185,13 +187,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -252,13 +255,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -336,13 +340,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -49,13 +50,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -72,13 +74,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -106,13 +109,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -14,24 +15,33 @@ import {
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
|
||||
type TCmekServiceFactoryDep = {
|
||||
kmsService: TKmsServiceFactory;
|
||||
kmsDAL: TKmsKeyDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
|
||||
|
||||
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
|
||||
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => {
|
||||
const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
|
||||
let projectId = preSplitProjectId;
|
||||
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS);
|
||||
if (cmekProjectFromSplit) {
|
||||
projectId = cmekProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
|
||||
|
||||
const cmek = await kmsService.generateKmsKey({
|
||||
@@ -50,13 +60,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
|
||||
|
||||
@@ -72,13 +83,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
|
||||
|
||||
@@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
return cmek;
|
||||
};
|
||||
|
||||
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
|
||||
const listCmeksByProjectId = async (
|
||||
{ projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
let projectId = preSplitProjectId;
|
||||
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS);
|
||||
if (cmekProjectFromSplit) {
|
||||
projectId = cmekProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
@@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
@@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
|
||||
|
||||
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
|
||||
@@ -138,13 +160,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
|
||||
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
@@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.first();
|
||||
@@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
||||
@@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityJwtAuthDALFactory = ReturnType<typeof identityJwtAuthDALFactory>;
|
||||
|
||||
export const identityJwtAuthDALFactory = (db: TDbClient) => {
|
||||
const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth);
|
||||
|
||||
return jwtAuthOrm;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import picomatch from "picomatch";
|
||||
|
||||
export const doesFieldValueMatchJwtPolicy = (fieldValue: string | boolean | number, policyValue: string) => {
|
||||
if (typeof fieldValue === "boolean") {
|
||||
return fieldValue === (policyValue === "true");
|
||||
}
|
||||
|
||||
if (typeof fieldValue === "number") {
|
||||
return fieldValue === parseInt(policyValue, 10);
|
||||
}
|
||||
|
||||
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
};
|
||||
@@ -0,0 +1,534 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
|
||||
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
|
||||
import {
|
||||
JwtConfigurationType,
|
||||
TAttachJwtAuthDTO,
|
||||
TGetJwtAuthDTO,
|
||||
TLoginJwtAuthDTO,
|
||||
TRevokeJwtAuthDTO,
|
||||
TUpdateJwtAuthDTO
|
||||
} from "./identity-jwt-auth-types";
|
||||
|
||||
type TIdentityJwtAuthServiceFactoryDep = {
|
||||
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TIdentityJwtAuthServiceFactory = ReturnType<typeof identityJwtAuthServiceFactory>;
|
||||
|
||||
export const identityJwtAuthServiceFactory = ({
|
||||
identityJwtAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
identityAccessTokenDAL,
|
||||
kmsService
|
||||
}: TIdentityJwtAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
if (!identityJwtAuth) {
|
||||
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||
identityId: identityJwtAuth.identityId
|
||||
});
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({
|
||||
message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const decodedToken = jwt.decode(jwtValue, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid JWT"
|
||||
});
|
||||
}
|
||||
|
||||
let tokenData: Record<string, string | boolean | number> = {};
|
||||
|
||||
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
||||
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
||||
}).toString();
|
||||
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
|
||||
const client = new JwksClient({
|
||||
jwksUri: identityJwtAuth.jwksUrl,
|
||||
requestAgent
|
||||
});
|
||||
|
||||
const { kid } = decodedToken.header;
|
||||
const jwtSigningKey = await client.getSigningKey(kid);
|
||||
|
||||
try {
|
||||
tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
const errors: string[] = [];
|
||||
let isMatchAnyKey = false;
|
||||
for (const publicKey of decryptedPublicKeys) {
|
||||
try {
|
||||
tokenData = jwt.verify(jwtValue, publicKey) as Record<string, string>;
|
||||
isMatchAnyKey = true;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
errors.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMatchAnyKey) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundIssuer) {
|
||||
if (tokenData.iss !== identityJwtAuth.boundIssuer) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: issuer mismatch"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundSubject) {
|
||||
if (!tokenData.sub) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token has no subject field"
|
||||
});
|
||||
}
|
||||
|
||||
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: subject not allowed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundAudiences) {
|
||||
if (!tokenData.aud) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token has no audience field"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!identityJwtAuth.boundAudiences
|
||||
.split(", ")
|
||||
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: token audience not allowed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
|
||||
if (!tokenData[claimKey]) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityJwtAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityJwtAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.JWT_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityJwtAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn:
|
||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||
? undefined
|
||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachJwtAuth = async ({
|
||||
identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
jwksCaCert,
|
||||
publicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add JWT Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(jwksCaCert)
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(publicKeys.join(","))
|
||||
});
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityJwtAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
encryptedJwksCaCert,
|
||||
encryptedPublicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return doc;
|
||||
});
|
||||
return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys };
|
||||
};
|
||||
|
||||
const updateJwtAuth = async ({
|
||||
identityId,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
jwksCaCert,
|
||||
publicKeys,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update JWT Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityJwtAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const updateQuery: TIdentityJwtAuthsUpdate = {
|
||||
boundIssuer,
|
||||
configurationType,
|
||||
jwksUrl,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
};
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (jwksCaCert !== undefined) {
|
||||
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(jwksCaCert)
|
||||
});
|
||||
|
||||
updateQuery.encryptedJwksCaCert = encryptedJwksCaCert;
|
||||
}
|
||||
|
||||
if (publicKeys) {
|
||||
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(publicKeys.join(","))
|
||||
});
|
||||
|
||||
updateQuery.encryptedPublicKeys = encryptedPublicKeys;
|
||||
}
|
||||
|
||||
const updatedJwtAuth = await identityJwtAuthDAL.updateById(identityJwtAuth.id, updateQuery);
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedJwksCaCert }).toString();
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
return {
|
||||
...updatedJwtAuth,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
jwksCaCert: decryptedJwksCaCert,
|
||||
publicKeys: decryptedPublicKeys
|
||||
};
|
||||
};
|
||||
|
||||
const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have JWT Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedJwksCaCert }).toString();
|
||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
||||
.toString()
|
||||
.split(",");
|
||||
|
||||
return {
|
||||
...identityJwtAuth,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
jwksCaCert: decryptedJwksCaCert,
|
||||
publicKeys: decryptedPublicKeys
|
||||
};
|
||||
};
|
||||
|
||||
const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({ message: "Failed to find identity" });
|
||||
}
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have JWT auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to revoke JWT auth of identity with more privileged role"
|
||||
});
|
||||
}
|
||||
|
||||
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx);
|
||||
|
||||
return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
|
||||
return revokedIdentityJwtAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachJwtAuth,
|
||||
updateJwtAuth,
|
||||
getJwtAuth,
|
||||
revokeJwtAuth
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum JwtConfigurationType {
|
||||
JWKS = "jwks",
|
||||
STATIC = "static"
|
||||
}
|
||||
|
||||
export type TAttachJwtAuthDTO = {
|
||||
identityId: string;
|
||||
configurationType: JwtConfigurationType;
|
||||
jwksUrl: string;
|
||||
jwksCaCert: string;
|
||||
publicKeys: string[];
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateJwtAuthDTO = {
|
||||
identityId: string;
|
||||
configurationType?: JwtConfigurationType;
|
||||
jwksUrl?: string;
|
||||
jwksCaCert?: string;
|
||||
publicKeys?: string[];
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetJwtAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeJwtAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TLoginJwtAuthDTO = {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const validateJwtAuthAudiencesField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
||||
|
||||
export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => {
|
||||
const formattedClaims: Record<string, string> = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
formattedClaims[key] = data[key]
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
||||
|
||||
return formattedClaims;
|
||||
});
|
||||
@@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
@@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
projectId,
|
||||
projectName
|
||||
projectName,
|
||||
projectType
|
||||
}) => ({
|
||||
id,
|
||||
identityId,
|
||||
@@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
},
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
name: projectName,
|
||||
type: projectType
|
||||
}
|
||||
}),
|
||||
key: "id",
|
||||
|
||||
@@ -7,7 +7,8 @@ export const buildAuthMethods = ({
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
tokenId,
|
||||
jwtId
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
@@ -16,6 +17,7 @@ export const buildAuthMethods = ({
|
||||
oidcId?: string;
|
||||
azureId?: string;
|
||||
tokenId?: string;
|
||||
jwtId?: string;
|
||||
}) => {
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
@@ -24,6 +26,7 @@ export const buildAuthMethods = ({
|
||||
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
|
||||
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
|
||||
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
|
||||
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityJwtAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOrgMemberships,
|
||||
@@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityJwtAuths>(
|
||||
TableName.IdentityJwtAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
@@ -81,6 +87,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
);
|
||||
@@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityJwtAuths>(
|
||||
TableName.IdentityJwtAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
@@ -200,7 +212,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
jwtId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
@@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
tokenId,
|
||||
jwtId
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Octokit } from "@octokit/rest";
|
||||
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
||||
import {
|
||||
ProjectType,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert
|
||||
} from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -145,13 +151,14 @@ export const integrationAuthServiceFactory = ({
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
const tokenExchange = await exchangeCode({ integration, code, url, installationId });
|
||||
@@ -254,13 +261,14 @@ export const integrationAuthServiceFactory = ({
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
const updateDoc: TIntegrationAuthsInsert = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
@@ -80,13 +81,14 @@ export const integrationServiceFactory = ({
|
||||
if (!integrationAuth)
|
||||
throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integrationAuth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -158,13 +160,14 @@ export const integrationServiceFactory = ({
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
|
||||
const newEnvironment = environment || integration.environment.slug;
|
||||
@@ -293,13 +296,14 @@ export const integrationServiceFactory = ({
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
|
||||
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
@@ -196,26 +195,18 @@ export const orgServiceFactory = ({
|
||||
return org;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||
|
||||
let workspaces: (TProjects & { organization: string } & {
|
||||
environments: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
}[];
|
||||
})[];
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
|
||||
if (actor === ActorType.USER) {
|
||||
workspaces = await projectDAL.findAllProjects(actorId);
|
||||
} else if (actor === ActorType.IDENTITY) {
|
||||
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
|
||||
} else {
|
||||
throw new BadRequestError({ message: "Invalid actor type" });
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||
if (actor === ActorType.IDENTITY) {
|
||||
const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type);
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid actor type" });
|
||||
};
|
||||
|
||||
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
|
||||
@@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
type?: ProjectType;
|
||||
};
|
||||
|
||||
export type TUpdateOrgDTO = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle
|
||||
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TPkiAlertDALFactory } from "./pki-alert-dal";
|
||||
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
||||
|
||||
@@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = {
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
|
||||
@@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
smtpService,
|
||||
projectDAL
|
||||
}: TPkiAlertServiceFactoryDep) => {
|
||||
const sendPkiItemExpiryNotices = async () => {
|
||||
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
|
||||
@@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({
|
||||
};
|
||||
|
||||
const createPkiAlert = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
name,
|
||||
pkiCollectionId,
|
||||
alertBeforeDays,
|
||||
@@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateAlertDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -128,13 +142,14 @@ export const pkiAlertServiceFactory = ({
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -160,13 +175,14 @@ export const pkiAlertServiceFactory = ({
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
|
||||
alert = await pkiAlertDAL.deleteById(alertId);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { ProjectType, TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
|
||||
import { transformPkiCollectionItem } from "./pki-collection-fns";
|
||||
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
|
||||
@@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
|
||||
@@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({
|
||||
pkiCollectionItemDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
projectDAL
|
||||
}: TPkiCollectionServiceFactoryDep) => {
|
||||
const createPkiCollection = async ({
|
||||
name,
|
||||
description,
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreatePkiCollectionDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -106,13 +119,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
|
||||
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
|
||||
@@ -133,13 +147,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -205,13 +220,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -298,13 +314,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
|
||||
if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({
|
||||
name,
|
||||
slug
|
||||
}: TCreateEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
@@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({
|
||||
id,
|
||||
position
|
||||
}: TUpdateEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
@@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
|
||||
@@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project)
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
|
||||
parentMapper: ({
|
||||
email,
|
||||
firstName,
|
||||
username,
|
||||
lastName,
|
||||
publicKey,
|
||||
isGhost,
|
||||
id,
|
||||
projectId,
|
||||
projectName,
|
||||
projectType
|
||||
}) => ({
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
name: projectName,
|
||||
type: projectType
|
||||
}
|
||||
}),
|
||||
key: "id",
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
|
||||
import {
|
||||
ProjectsSchema,
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TProjectsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
@@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||
export const projectDALFactory = (db: TDbClient) => {
|
||||
const projectOrm = ormify(db, TableName.Project);
|
||||
|
||||
const findAllProjects = async (userId: string) => {
|
||||
const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where({ userId })
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.andWhere((qb) => {
|
||||
if (projectType !== "all") {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Project),
|
||||
@@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||
]);
|
||||
|
||||
const groups: string[] = await db(TableName.UserGroupMembership)
|
||||
.where({ userId })
|
||||
.select(selectAllTableCols(TableName.UserGroupMembership))
|
||||
.pluck("groupId");
|
||||
const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId");
|
||||
|
||||
const groupWorkspaces = await db(TableName.GroupProjectMembership)
|
||||
.whereIn("groupId", groups)
|
||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.whereNotIn(
|
||||
`${TableName.Project}.id`,
|
||||
workspaces.map(({ id }) => id)
|
||||
@@ -108,12 +124,17 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||
const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.replicaNode()(TableName.IdentityProjectMembership)
|
||||
.where({ identityId })
|
||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Project),
|
||||
@@ -315,6 +336,22 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => {
|
||||
try {
|
||||
const project = await db(TableName.ProjectSplitBackfillIds)
|
||||
.where({
|
||||
sourceProjectId: projectId,
|
||||
destinationProjectType: projectType
|
||||
})
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`)
|
||||
.select(selectAllTableCols(TableName.Project))
|
||||
.first();
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...projectOrm,
|
||||
findAllProjects,
|
||||
@@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
findProjectByFilter,
|
||||
findProjectBySlug,
|
||||
findProjectWithOrg,
|
||||
checkProjectUpgradeStatus
|
||||
checkProjectUpgradeStatus,
|
||||
getProjectFromSplitId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectType, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -165,10 +165,10 @@ export const projectServiceFactory = ({
|
||||
kmsKeyId,
|
||||
tx: trx,
|
||||
createDefaultEnvs = true,
|
||||
template = InfisicalProjectTemplate.Default
|
||||
template = InfisicalProjectTemplate.Default,
|
||||
type = ProjectType.SecretManager
|
||||
}: TCreateProjectDTO) => {
|
||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||
|
||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -218,6 +218,7 @@ export const projectServiceFactory = ({
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
type,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
@@ -442,8 +443,14 @@ export const projectServiceFactory = ({
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||
const getProjects = async ({
|
||||
actorId,
|
||||
includeRoles,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
type = ProjectType.SecretManager
|
||||
}: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
@@ -693,11 +700,19 @@ export const projectServiceFactory = ({
|
||||
actor
|
||||
}: TListProjectCasDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
let projectId = project.id;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
@@ -709,7 +724,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const cas = await certificateAuthorityDAL.find(
|
||||
{
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
...(status && { status }),
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
@@ -735,18 +750,27 @@ export const projectServiceFactory = ({
|
||||
actor
|
||||
}: TListProjectCertsDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
let projectId = project.id;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
|
||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||
|
||||
const certificates = await certificateDAL.find(
|
||||
{
|
||||
@@ -760,7 +784,7 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesInProject({
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
@@ -775,19 +799,29 @@ export const projectServiceFactory = ({
|
||||
* Return list of (PKI) alerts configured for project
|
||||
*/
|
||||
const listProjectAlerts = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -802,19 +836,28 @@ export const projectServiceFactory = ({
|
||||
* Return list of PKI collections for project
|
||||
*/
|
||||
const listProjectPkiCollections = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
@@ -829,19 +872,29 @@ export const projectServiceFactory = ({
|
||||
* Return list of certificate templates for project
|
||||
*/
|
||||
const listProjectCertificateTemplates = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actor
|
||||
}: TListProjectCertificateTemplatesDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
@@ -865,7 +918,7 @@ export const projectServiceFactory = ({
|
||||
actor,
|
||||
projectId
|
||||
}: TListProjectSshCasDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -873,6 +926,7 @@ export const projectServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SshCertificateAuthorities
|
||||
@@ -900,7 +954,7 @@ export const projectServiceFactory = ({
|
||||
actor,
|
||||
projectId
|
||||
}: TListProjectSshCertificatesDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -908,6 +962,7 @@ export const projectServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
|
||||
const cas = await sshCertificateAuthorityDAL.find({
|
||||
@@ -938,7 +993,7 @@ export const projectServiceFactory = ({
|
||||
actor,
|
||||
projectId
|
||||
}: TListProjectSshCertificateTemplatesDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -946,6 +1001,7 @@ export const projectServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SshCertificateTemplates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TProjectKeys } from "@app/db/schemas";
|
||||
import { ProjectType, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -35,6 +35,7 @@ export type TCreateProjectDTO = {
|
||||
createDefaultEnvs?: boolean;
|
||||
template?: string;
|
||||
tx?: Knex;
|
||||
type?: ProjectType;
|
||||
};
|
||||
|
||||
export type TDeleteProjectBySlugDTO = {
|
||||
@@ -84,6 +85,7 @@ export type TDeleteProjectDTO = {
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
type?: ProjectType | "all";
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
|
||||
|
||||
import { TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { ProjectType, TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
@@ -52,13 +52,14 @@ export const secretFolderServiceFactory = ({
|
||||
environment,
|
||||
path: secretPath
|
||||
}: TCreateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -150,13 +151,14 @@ export const secretFolderServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
folders.forEach(({ environment, path: secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -259,13 +261,14 @@ export const secretFolderServiceFactory = ({
|
||||
path: secretPath,
|
||||
id
|
||||
}: TUpdateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -339,13 +342,14 @@ export const secretFolderServiceFactory = ({
|
||||
path: secretPath,
|
||||
idOrName
|
||||
}: TDeleteFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ProjectType, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -73,13 +73,14 @@ export const secretImportServiceFactory = ({
|
||||
isReplication,
|
||||
path: secretPath
|
||||
}: TCreateSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
// check if user has permission to import into destination path
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -189,13 +190,15 @@ export const secretImportServiceFactory = ({
|
||||
data,
|
||||
id
|
||||
}: TUpdateSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
@@ -283,13 +286,15 @@ export const secretImportServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -23,7 +24,7 @@ export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory
|
||||
|
||||
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
|
||||
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -31,6 +32,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
|
||||
@@ -54,7 +56,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
tag.projectId,
|
||||
@@ -62,6 +64,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
|
||||
return updatedTag;
|
||||
@@ -71,7 +74,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
const tag = await secretTagDAL.findById(id);
|
||||
if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
tag.projectId,
|
||||
@@ -79,6 +82,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const deletedTag = await secretTagDAL.deleteById(tag.id);
|
||||
return deletedTag;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError, PureAbility, subject } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectType, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
@@ -188,13 +188,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TCreateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -310,13 +311,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TUpdateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
if (inputSecret.newSecretName === "") {
|
||||
throw new BadRequestError({ message: "New secret name cannot be empty" });
|
||||
@@ -494,13 +496,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TDeleteSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1081,13 +1084,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TCreateManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1221,13 +1225,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
secrets: inputSecrets
|
||||
}: TUpdateManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1427,13 +1432,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1569,13 +1575,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TBackFillSecretReferencesDTO) => {
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
if (!hasRole(ProjectMembershipRole.Admin))
|
||||
throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" });
|
||||
@@ -1616,13 +1623,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TMoveSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath);
|
||||
if (!sourceFolder) {
|
||||
|
||||
@@ -20,7 +20,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`)
|
||||
.join<TSecretVersionsV2, TSecretVersionsV2 & { secretId: string; max: number }>(
|
||||
(tx || db)(TableName.SecretVersionV2)
|
||||
.groupBy("folderId", "secretId")
|
||||
.where(`${TableName.SecretVersionV2}.folderId`, folderId)
|
||||
.groupBy("secretId")
|
||||
.max("version")
|
||||
.select("secretId")
|
||||
.as("latestVersion"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
@@ -186,13 +187,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TCreateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -301,13 +304,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TUpdateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -443,13 +448,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TDeleteSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -732,13 +739,14 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TCreateBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -817,13 +825,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TUpdateBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -923,13 +933,14 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
|
||||
@@ -53,6 +53,13 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||
const smtp = createTransport(cfg);
|
||||
const isSmtpOn = Boolean(cfg.host);
|
||||
|
||||
handlebars.registerHelper("emailFooter", () => {
|
||||
const { SITE_URL } = getConfig();
|
||||
return new handlebars.SafeString(
|
||||
`<p style="font-size: 12px;">Email sent via Infisical at <a href="${SITE_URL}">${SITE_URL}</a></p>`
|
||||
);
|
||||
});
|
||||
|
||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||
const appCfg = getConfig();
|
||||
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
View the request and approve or deny it
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -11,8 +11,11 @@
|
||||
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
|
||||
|
||||
<p>
|
||||
{{requesterFullName}} ({{requesterEmail}}) has merged
|
||||
a secret to environment {{environment}} at secret path {{secretPath}}
|
||||
{{requesterFullName}}
|
||||
({{requesterEmail}}) has merged a secret to environment
|
||||
{{environment}}
|
||||
at secret path
|
||||
{{secretPath}}
|
||||
without obtaining the required approvals.
|
||||
</p>
|
||||
<p>
|
||||
@@ -24,5 +27,7 @@
|
||||
To review this action, please visit the request panel
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
@@ -14,6 +13,8 @@
|
||||
<h2>{{code}}</h2>
|
||||
<p>The MFA code will be valid for 2 minutes.</p>
|
||||
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -10,6 +10,8 @@
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
|
||||
<h1>{{code}}</h1>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<p>Error: {{error}}</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -12,6 +12,8 @@
|
||||
{{provider}}
|
||||
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
|
||||
has finished or if it fails.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,6 +9,8 @@
|
||||
<body>
|
||||
<h2>An import from {{provider}} to Infisical was successful</h2>
|
||||
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,21 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Incident alert: secrets potentially leaked</title>
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Incident alert: secrets potentially leaked</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your
|
||||
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -26,6 +26,8 @@
|
||||
{{#if syncMessage}}
|
||||
<p><b>Reason: </b>{{syncMessage}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
@@ -13,7 +12,11 @@
|
||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||
<p><strong>IP address</strong>: {{ip}}</p>
|
||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p>
|
||||
<p>If you believe that this login is suspicious, please contact
|
||||
{{#if isCloud}}Infisical{{else}}your administrator{{/if}}
|
||||
or reset your password immediately.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -12,5 +12,7 @@
|
||||
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Click to join</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Account Recovery</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
|
||||
</body>
|
||||
<p>If you didn't initiate this request, please contact
|
||||
{{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,5 +27,7 @@
|
||||
<p>Please take necessary actions to renew these items before they expire.</p>
|
||||
|
||||
<p>For more details, please log in to your Infisical account and check your PKI management section.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Organization Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Join your organization on Infisical</h2>
|
||||
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
|
||||
<a href="{{callback_url}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||
and configs.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,8 @@
|
||||
View the request and approve or deny it
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,25 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Incident alert: secret leaked</title>
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Incident alert: secret leaked</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||
by {{pusher_name}} ({{pusher_email}}). If
|
||||
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
|
||||
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||
by
|
||||
{{pusher_name}}
|
||||
({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line
|
||||
containing the secret as comment in the given programming. This will prevent future notifications from being sent
|
||||
out for those secret(s).</p>
|
||||
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="{{siteUrl}}">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your
|
||||
<a href="{{siteUrl}}">Infisical dashboard</a>.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -13,6 +13,8 @@
|
||||
{{#if reminderNote}}
|
||||
<p>Here's the note included with the reminder: {{reminderNote}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,17 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Code</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<h2>Confirm your email address</h2>
|
||||
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||
<h1>{{code}}</h1>
|
||||
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at [email protected]{{else}}Contact your administrator{{/if}}.</p>
|
||||
</body>
|
||||
<p>Questions about setting up Infisical?
|
||||
{{#if isCloud}}Email us at [email protected]{{else}}Contact your administrator{{/if}}.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -11,6 +11,8 @@
|
||||
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
|
||||
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
|
||||
<p>If these attempts were not made by you, reset your password immediately.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -11,5 +11,7 @@
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||
and configs.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,87 +1,44 @@
|
||||
import { PlainClient } from "@team-plain/typescript-sdk";
|
||||
import axios from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { InternalServerError } from "@app/lib/errors";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
|
||||
type TUserEngagementServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TUserEngagementServiceFactory = ReturnType<typeof userEngagementServiceFactory>;
|
||||
|
||||
export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => {
|
||||
const createUserWish = async (userId: string, text: string) => {
|
||||
export const userEngagementServiceFactory = ({ userDAL, orgDAL }: TUserEngagementServiceFactoryDep) => {
|
||||
const createUserWish = async (userId: string, orgId: string, text: string) => {
|
||||
const user = await userDAL.findById(userId);
|
||||
const org = await orgDAL.findById(orgId);
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (!appCfg.PLAIN_API_KEY) {
|
||||
if (!appCfg.PYLON_API_KEY) {
|
||||
throw new InternalServerError({
|
||||
message: "Plain is not configured."
|
||||
message: "Pylon is not configured."
|
||||
});
|
||||
}
|
||||
|
||||
const client = new PlainClient({
|
||||
apiKey: appCfg.PLAIN_API_KEY
|
||||
});
|
||||
|
||||
const customerUpsertRes = await client.upsertCustomer({
|
||||
identifier: {
|
||||
emailAddress: user.email
|
||||
},
|
||||
onCreate: {
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
shortName: user.firstName,
|
||||
email: {
|
||||
email: user.email as string,
|
||||
isVerified: user.isEmailVerified as boolean
|
||||
},
|
||||
|
||||
externalId: user.id
|
||||
},
|
||||
|
||||
onUpdate: {
|
||||
fullName: {
|
||||
value: `${user.firstName} ${user.lastName}`
|
||||
},
|
||||
shortName: {
|
||||
value: user.firstName
|
||||
},
|
||||
email: {
|
||||
email: user.email as string,
|
||||
isVerified: user.isEmailVerified as boolean
|
||||
},
|
||||
externalId: {
|
||||
value: user.id
|
||||
}
|
||||
const request = axios.create({
|
||||
baseURL: "https://api.usepylon.com",
|
||||
headers: {
|
||||
Authorization: `Bearer ${appCfg.PYLON_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
if (customerUpsertRes.error) {
|
||||
throw new InternalServerError({ message: customerUpsertRes.error.message });
|
||||
}
|
||||
|
||||
const createThreadRes = await client.createThread({
|
||||
title: "Wish",
|
||||
customerIdentifier: {
|
||||
externalId: customerUpsertRes.data.customer.externalId
|
||||
},
|
||||
components: [
|
||||
{
|
||||
componentText: {
|
||||
text
|
||||
}
|
||||
}
|
||||
],
|
||||
labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",")
|
||||
await request.post("/issues", {
|
||||
title: `New Wish From: ${user.firstName} ${user.lastName} (${org.name})`,
|
||||
body_html: text,
|
||||
requester_email: user.email,
|
||||
requester_name: `${user.firstName} ${user.lastName} (${org.name})`,
|
||||
tags: ["wish"]
|
||||
});
|
||||
|
||||
if (createThreadRes.error) {
|
||||
throw new InternalServerError({
|
||||
message: createThreadRes.error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
createUserWish
|
||||
|
||||
Reference in New Issue
Block a user