Merge branch 'main' into feat/point-in-time-revamp

This commit is contained in:
carlosmonastyrski
2025-05-20 10:37:41 -03:00
parent c01a98ccf1
commit 1748052cb0
731 changed files with 29323 additions and 10098 deletions
@@ -2,6 +2,7 @@ import { z } from "zod";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -18,6 +19,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
server.route({
url: "/",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
permissions: z.any().array(),
+6 -17
View File
@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({
name: z.string(),
id: z.string()
}),
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
})
}).array()
})
}
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({
name: z.string(),
id: z.string()
}),
projectGatewayId: z.string()
})
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({
projectId: req.params.projectId,
projectPermission: req.permission
const gateways = await server.services.gateway.listGateways({
orgPermission: req.permission
});
return { gateways };
}
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string()
}),
body: z.object({
name: slugSchema({ field: "name" }).optional(),
projectIds: z.string().array().optional()
name: slugSchema({ field: "name" }).optional()
}),
response: {
200: z.object({
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission,
id: req.params.id,
name: req.body.name,
projectIds: req.body.projectIds
name: req.body.name
});
return { gateway };
}
+3
View File
@@ -98,6 +98,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/login",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
organizationSlug: z.string().trim()
@@ -1,9 +1,8 @@
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
position: z.number().min(1)
})
.array()
.nullable()
});
const ProjectTemplateRolesSchema = z
@@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
hide: false,
tags: [ApiDocsTags.ProjectTemplates],
description: "List project templates for the current organization.",
querystring: z.object({
type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type)
}),
response: {
200: z.object({
projectTemplates: SanitizedProjectTemplateSchema.array()
@@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const { type } = req.query;
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type);
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
@@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
tags: [ApiDocsTags.ProjectTemplates],
description: "Create a project template.",
body: z.object({
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
@@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
.describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
ProjectTemplates.CREATE.environments
)
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
}),
response: {
200: z.object({
+9
View File
@@ -166,6 +166,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/redirect/saml2/organizations/:orgSlug",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
orgSlug: z.string().trim()
@@ -192,6 +195,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/redirect/saml2/:samlConfigId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
samlConfigId: z.string().trim()
@@ -218,6 +224,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/saml2/:samlConfigId",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
samlConfigId: z.string().trim()
+3
View File
@@ -196,6 +196,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/Users",
method: "POST",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
schemas: z.array(z.string()),
@@ -1,11 +1,11 @@
import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns";
import {
SecretScanningResolvedStatus,
SecretScanningRiskStatus
} from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
body: z.object({ organizationId: z.string().trim() }),
response: {
200: z.object({
sessionId: z.string()
sessionId: z.string(),
gitAppSlug: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
if (!canUseSecretScanning(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});
@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
})
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
message: "Max TLL must be greater than or equal to TTL",
message: "Max TTL must be greater than or equal to TTL",
path: ["maxTTL"]
}),
response: {
+1 -1
View File
@@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const host = await server.services.sshHost.getSshHost({
const host = await server.services.sshHost.getSshHostById({
sshHostId: req.params.sshHostId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -19,9 +19,10 @@ import { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
import {
@@ -119,44 +120,66 @@ export enum EventType {
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth",
ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth",
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth",
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@@ -224,6 +247,8 @@ export enum EventType {
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
GET_CERT_PRIVATE_KEY = "get-cert-private-key",
GET_CERT_BUNDLE = "get-cert-bundle",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
@@ -235,6 +260,13 @@ export enum EventType {
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
@@ -991,6 +1023,55 @@ interface GetIdentityAwsAuthEvent {
};
}
interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
identityOciAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityOciAuthEvent {
type: EventType.ADD_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityOciAuthEvent {
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityOciAuthEvent {
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid?: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityOciAuthEvent {
type: EventType.GET_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityAzureAuthEvent {
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
metadata: {
@@ -1040,6 +1121,55 @@ interface GetIdentityAzureAuthEvent {
};
}
interface LoginIdentityLdapAuthEvent {
type: EventType.LOGIN_IDENTITY_LDAP_AUTH;
metadata: {
identityId: string;
ldapUsername: string;
ldapEmail?: string;
};
}
interface AddIdentityLdapAuthEvent {
type: EventType.ADD_IDENTITY_LDAP_AUTH;
metadata: {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url: string;
};
}
interface UpdateIdentityLdapAuthEvent {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH;
metadata: {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url?: string;
};
}
interface GetIdentityLdapAuthEvent {
type: EventType.GET_IDENTITY_LDAP_AUTH;
metadata: {
identityId: string;
};
}
interface RevokeIdentityLdapAuthEvent {
type: EventType.REVOKE_IDENTITY_LDAP_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityOidcAuthEvent {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
metadata: {
@@ -1798,6 +1928,24 @@ interface GetCertBody {
};
}
interface GetCertPrivateKey {
type: EventType.GET_CERT_PRIVATE_KEY;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCertBundle {
type: EventType.GET_CERT_BUNDLE;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
@@ -1887,6 +2035,77 @@ interface DeletePkiCollectionItem {
};
}
interface CreatePkiSubscriber {
type: EventType.CREATE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
caId?: string;
name: string;
commonName: string;
ttl: string;
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
};
}
interface UpdatePkiSubscriber {
type: EventType.UPDATE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
caId?: string;
name?: string;
commonName?: string;
ttl?: string;
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
};
}
interface DeletePkiSubscriber {
type: EventType.DELETE_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
name: string;
};
}
interface GetPkiSubscriber {
type: EventType.GET_PKI_SUBSCRIBER;
metadata: {
pkiSubscriberId: string;
name: string;
};
}
interface IssuePkiSubscriberCert {
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
serialNumber: string;
};
}
interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
serialNumber: string;
};
}
interface ListPkiSubscriberCerts {
type: EventType.LIST_PKI_SUBSCRIBER_CERTS;
metadata: {
subscriberId: string;
name: string;
projectId: string;
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
@@ -2823,6 +3042,11 @@ export type Event =
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent
| LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent
| GetIdentityOciAuthEvent
| DeleteIdentityOciAuthEvent
| LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent
| DeleteIdentityAzureAuthEvent
@@ -2838,6 +3062,11 @@ export type Event =
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
| LoginIdentityLdapAuthEvent
| AddIdentityLdapAuthEvent
| UpdateIdentityLdapAuthEvent
| GetIdentityLdapAuthEvent
| RevokeIdentityLdapAuthEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
@@ -2897,6 +3126,8 @@ export type Event =
| DeleteCert
| RevokeCert
| GetCertBody
| GetCertPrivateKey
| GetCertBundle
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert
@@ -2908,6 +3139,13 @@ export type Event =
| GetPkiCollectionItems
| AddPkiCollectionItem
| DeletePkiCollectionItem
| CreatePkiSubscriber
| UpdatePkiSubscriber
| DeletePkiSubscriber
| GetPkiSubscriber
| IssuePkiSubscriberCert
| SignPkiSubscriberCert
| ListPkiSubscriberCerts
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
@@ -24,8 +24,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
if (net.isIPv4(el)) {
exclusiveIps.push(el);
} else {
const resolvedIps = await dns.resolve4(el);
exclusiveIps.push(...resolvedIps);
try {
const resolvedIps = await dns.resolve4(el);
exclusiveIps.push(...resolvedIps);
} catch (error) {
// only try lookup if not found
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
const resolvedIps = (await dns.lookup(el, { all: true, family: 4 })).map(({ address }) => address);
exclusiveIps.push(...resolvedIps);
}
}
}
}
@@ -38,8 +46,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") {
throw new BadRequestError({ message: "Invalid db host" });
}
const resolvedIps = await dns.resolve4(host);
inputHostIps.push(...resolvedIps);
try {
const resolvedIps = await dns.resolve4(host);
inputHostIps.push(...resolvedIps);
} catch (error) {
// only try lookup if not found
if ((error as { code: string })?.code !== "ENOTFOUND") throw error;
const resolvedIps = (await dns.lookup(host, { all: true, family: 4 })).map(({ address }) => address);
inputHostIps.push(...resolvedIps);
}
}
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
@@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService,
projectDAL,
kmsService,
projectGatewayDAL,
gatewayDAL,
resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
const projectGatewayId = inputs.projectGatewayId as string;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
message: `Gateway with ID ${gatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
folderId: folder.id,
name,
projectGatewayId: selectedGatewayId
gatewayId: selectedGatewayId
},
tx
);
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null;
if (
updatedInput &&
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
const gatewayId = updatedInput.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
message: `Gateway with ID ${gatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
}
const isConnected = await selectedProvider.validateConnection(newInput);
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
name: newName ?? name,
status: null,
projectGatewayId: selectedGatewayId
gatewayId: selectedGatewayId
},
tx
);
@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export const buildDynamicSecretProviders = ({
@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional()
gatewayId: z.string().nullable().optional()
});
export const DynamicSecretCassandraSchema = z.object({
@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
};
type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
});
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void>
) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
async (port) => {
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
+24 -59
View File
@@ -1,37 +1,34 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import {
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
const find = async (
filter: TFindFilter<TGateways> & { orgId?: string },
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try {
const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter))
.where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
.join(
TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
.select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
if (filter.orgId) {
void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
}
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
@@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
}
const docs = await query;
return sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
...GatewaysSchema.parse(data),
identity: { id: data.identityId, name: data.identityName }
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
return docs.map((el) => ({
...GatewaysSchema.parse(el),
orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
identity: { id: el.identityId, name: el.identityName }
}));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
}
};
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
return { ...orm, find };
};
@@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import {
TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO,
TListGatewaysDTO,
TUpdateGatewayByIdDTO
} from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
kmsService,
permissionService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
keyStore
}: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) {
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway;
};
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway;
};
@@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
return gateway;
};
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const { gatewayId } = projectGateway;
const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
getGatewayById,
updateGatewayById,
deleteGatewayById,
getProjectGateways,
fnGetGatewayClientTls,
fnGetGatewayClientTlsByGatewayId,
heartbeat
};
};
@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = {
id: string;
name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor;
};
@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
export const projectGatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectGateway);
return orm;
};
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/core";
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
import { paginateGraphql } from "@octokit/plugin-paginate-graphql";
import { Octokit as OctokitRest } from "@octokit/rest";
import { OrgMembershipRole } from "@app/db/schemas";
@@ -18,7 +18,7 @@ import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
const OctokitWithPlugin = Octokit.plugin(paginateGraphQL);
const OctokitWithPlugin = Octokit.plugin(paginateGraphql);
type TGithubOrgSyncServiceFactoryDep = {
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
@@ -157,10 +157,23 @@ export const groupDALFactory = (db: TDbClient) => {
}
};
const findGroupsByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.Groups)
.join(TableName.GroupProjectMembership, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.Groups));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "Find groups by project id" });
}
};
return {
findGroups,
findByOrgId,
findAllGroupPossibleMembers,
findGroupsByProjectId,
...groupOrm
};
};
@@ -176,7 +176,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
db.ref("lastName").withSchema(TableName.Users).as("lastName")
db.ref("lastName").withSchema(TableName.Users).as("lastName"),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
);
return docs;
+7 -3
View File
@@ -24,9 +24,13 @@ export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured
isInitialized = true;
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error(err, "Failed to initialize PKCS#11 module");
throw err;
} catch (error) {
if (error instanceof pkcs11js.Pkcs11Error && error.code === pkcs11js.CKR_CRYPTOKI_ALREADY_INITIALIZED) {
logger.info("Skipping HSM initialization because it's already initialized.");
} else {
logger.error(error, "Failed to initialize PKCS#11 module");
throw error;
}
}
};
@@ -14,6 +14,11 @@ export type TLDAPConfig = {
caCert: string;
};
export type TTestLDAPConfigDTO = Omit<
TLDAPConfig,
"organization" | "id" | "groupSearchBase" | "groupSearchFilter" | "isActive" | "uniqueUserAttribute" | "searchBase"
>;
export type TCreateLdapCfgDTO = {
orgId: string;
isActive: boolean;
@@ -2,15 +2,14 @@ import ldapjs from "ldapjs";
import { logger } from "@app/lib/logger";
import { TLDAPConfig } from "./ldap-config-types";
import { TLDAPConfig, TTestLDAPConfigDTO } from "./ldap-config-types";
export const isValidLdapFilter = (filter: string) => {
try {
ldapjs.parseFilter(filter);
return true;
} catch (error) {
logger.error("Invalid LDAP filter");
logger.error(error);
logger.error(error, "Invalid LDAP filter");
return false;
}
};
@@ -20,7 +19,7 @@ export const isValidLdapFilter = (filter: string) => {
* @param ldapConfig - The LDAP configuration to test
* @returns {Boolean} isConnected - Whether or not the connection was successful
*/
export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean> => {
export const testLDAPConfig = async (ldapConfig: TTestLDAPConfigDTO): Promise<boolean> => {
return new Promise((resolve) => {
const ldapClient = ldapjs.createClient({
url: ldapConfig.url,
@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
}
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({
email: claims.email,
externalId: claims.sub,
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
orgId: org.id,
groups: claims.groups as string[] | undefined,
groups,
callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships
})
@@ -0,0 +1,473 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions,
ProjectPermissionSet,
ProjectPermissionSshHostActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything
[
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshHostGroups
].forEach((el) => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el
);
});
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey
],
ProjectPermissionSub.Certificates
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
can(
[
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
);
can(
[
ProjectPermissionPkiSubscriberActions.Edit,
ProjectPermissionPkiSubscriberActions.Read,
ProjectPermissionPkiSubscriberActions.Create,
ProjectPermissionPkiSubscriberActions.Delete,
ProjectPermissionPkiSubscriberActions.IssueCert,
ProjectPermissionPkiSubscriberActions.ListCerts
],
ProjectPermissionSub.PkiSubscribers
);
can(
[
ProjectPermissionMemberActions.Create,
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionMemberActions.AssumePrivileges
],
ProjectPermissionSub.Member
);
can(
[
ProjectPermissionGroupActions.Create,
ProjectPermissionGroupActions.Edit,
ProjectPermissionGroupActions.Delete,
ProjectPermissionGroupActions.Read,
ProjectPermissionGroupActions.GrantPrivileges
],
ProjectPermissionSub.Groups
);
can(
[
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionIdentityActions.AssumePrivileges
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
ProjectPermissionSecretRotationActions.RotateSecrets
],
ProjectPermissionSub.SecretRotation
);
return rules;
};
const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretFolders
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretImports
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Integrations
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Webhooks
);
can(
[
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Delete
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.ServiceTokens
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Settings
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Environments
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Tags
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete
],
ProjectPermissionSub.Certificates
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
return rules;
};
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
ProjectPermissionSub.Secrets
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Commits);
return rules;
};
const buildNoAccessProjectPermission = () => {
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return rules;
};
const buildSshHostBootstrapPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.IssueHostCert],
ProjectPermissionSub.SshHosts
);
return rules;
};
const buildCryptographicOperatorPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
return rules;
};
// General
export const projectAdminPermissions = buildAdminPermissionRules();
export const projectMemberPermissions = buildMemberPermissionRules();
export const projectViewerPermission = buildViewerPermissionRules();
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
// SSH
export const sshHostBootstrapPermissions = buildSshHostBootstrapPermissionRules();
// KMS
export const cryptographicOperatorPermissions = buildCryptographicOperatorPermissionRules();
@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways",
ListGateways = "list-gateways",
EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways"
DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
}
export enum OrgPermissionIdentityActions {
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules;
};
@@ -132,7 +132,7 @@ export const permissionDALFactory = (db: TDbClient) => {
}
};
const getProjectGroupPermissions = async (projectId: string) => {
const getProjectGroupPermissions = async (projectId: string, filterGroupId?: string) => {
try {
const docs = await db
.replicaNode()(TableName.GroupProjectMembership)
@@ -148,6 +148,11 @@ export const permissionDALFactory = (db: TDbClient) => {
`groupCustomRoles.id`
)
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
.where((bd) => {
if (filterGroupId) {
void bd.where(`${TableName.GroupProjectMembership}.groupId`, "=", filterGroupId);
}
})
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.Groups).as("groupId"),
@@ -12,6 +12,14 @@ import {
TIdentityProjectMemberships,
TProjectMemberships
} from "@app/db/schemas";
import {
cryptographicOperatorPermissions,
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission,
sshHostBootstrapPermissions
} from "@app/ee/services/permission/default-roles";
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn";
@@ -32,14 +40,7 @@ import {
TGetServiceTokenProjectPermissionArg,
TGetUserProjectPermissionArg
} from "./permission-service-types";
import {
buildServiceTokenProjectPermission,
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionSet,
projectViewerPermission
} from "./project-permission";
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
type TPermissionServiceFactoryDep = {
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
@@ -95,6 +96,10 @@ export const permissionServiceFactory = ({
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.SshHostBootstrapper:
return sshHostBootstrapPermissions;
case ProjectMembershipRole.KmsCryptographicOperator:
return cryptographicOperatorPermissions;
case ProjectMembershipRole.Custom: {
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
@@ -625,6 +630,34 @@ export const permissionServiceFactory = ({
return { permission };
};
const checkGroupProjectPermission = async ({
groupId,
projectId,
checkPermissions
}: {
groupId: string;
projectId: string;
checkPermissions: ProjectPermissionSet;
}) => {
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId);
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
const rolePermissions =
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const rules = buildProjectPermissionRules(rolePermissions);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return {
permission,
id: groupProjectPermission.groupId,
name: groupProjectPermission.username,
membershipId: groupProjectPermission.id
};
});
return groupPermissions.some((groupPermission) => groupPermission.permission.can(...checkPermissions));
};
return {
getUserOrgPermission,
getOrgPermission,
@@ -634,6 +667,7 @@ export const permissionServiceFactory = ({
getOrgPermissionByRole,
getProjectPermissionByRole,
buildOrgPermission,
buildProjectPermissionRules
buildProjectPermissionRules,
checkGroupProjectPermission
};
};
@@ -22,6 +22,14 @@ export enum ProjectPermissionCommitsActions {
PerformRollback = "perform-rollback"
}
export enum ProjectPermissionCertificateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
}
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
@@ -84,6 +92,15 @@ export enum ProjectPermissionSshHostActions {
IssueHostCert = "issue-host-cert"
}
export enum ProjectPermissionPkiSubscriberActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
ListCerts = "list-certs"
}
export enum ProjectPermissionSecretSyncActions {
Read = "read",
Create = "create",
@@ -141,6 +158,7 @@ export enum ProjectPermissionSub {
SshCertificateTemplates = "ssh-certificate-templates",
SshHosts = "ssh-hosts",
SshHostGroups = "ssh-host-groups",
PkiSubscribers = "pki-subscribers",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
@@ -188,6 +206,11 @@ export type SshHostSubjectFields = {
hostname: string;
};
export type PkiSubscriberSubjectFields = {
name: string;
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type ProjectPermissionSet =
| [
ProjectPermissionSecretActions,
@@ -238,7 +261,7 @@ export type ProjectPermissionSet =
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
@@ -247,6 +270,13 @@ export type ProjectPermissionSet =
ProjectPermissionSshHostActions,
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
]
| [
ProjectPermissionPkiSubscriberActions,
(
| ProjectPermissionSub.PkiSubscribers
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
@@ -398,6 +428,21 @@ const SshHostConditionSchema = z
})
.partial();
const PkiSubscriberConditionSchema = z
.object({
name: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [
z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -485,7 +530,7 @@ const GeneralPermissionSchema = [
}),
z.object({
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe(
"Describe what action an entity can take."
)
}),
@@ -668,6 +713,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.PkiSubscribers).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSubscriberActions).describe(
"Describe what action an entity can take."
),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
conditions: PkiSubscriberConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
@@ -683,404 +738,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
export type TProjectPermissionV2Schema = z.infer<typeof ProjectPermissionV2Schema>;
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything
[
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshHostGroups
].forEach((el) => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el
);
});
can(
[
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
);
can(
[
ProjectPermissionMemberActions.Create,
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionMemberActions.AssumePrivileges
],
ProjectPermissionSub.Member
);
can(
[
ProjectPermissionGroupActions.Create,
ProjectPermissionGroupActions.Edit,
ProjectPermissionGroupActions.Delete,
ProjectPermissionGroupActions.Read,
ProjectPermissionGroupActions.GrantPrivileges
],
ProjectPermissionSub.Groups
);
can(
[
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionIdentityActions.AssumePrivileges
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
ProjectPermissionSecretRotationActions.RotateSecrets
],
ProjectPermissionSub.SecretRotation
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
return rules;
};
export const projectAdminPermissions = buildAdminPermissionRules();
const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretFolders
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretImports
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Integrations
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Webhooks
);
can(
[
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Delete
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.ServiceTokens
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Settings
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Environments
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Tags
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Certificates
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits
);
return rules;
};
export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
return rules;
};
export const projectViewerPermission = buildViewerPermissionRules();
const buildNoAccessProjectPermission = () => {
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return rules;
};
export const buildServiceTokenProjectPermission = (
scopes: Array<{ secretPath: string; environment: string }>,
permission: string[]
@@ -1122,8 +779,6 @@ export const buildServiceTokenProjectPermission = (
return build({ conditionsMatcher });
};
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
/* eslint-disable */
/**
@@ -1,22 +1,27 @@
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { ProjectType } from "@app/db/schemas";
import {
InfisicalProjectTemplate,
TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const getDefaultProjectTemplate = (orgId: string) => ({
import { ProjectTemplateDefaultEnvironments } from "./project-template-constants";
export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
type,
name: InfisicalProjectTemplate.Default,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
description: `Infisical's ${type} default project template`,
environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null,
roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map(
({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})
),
orgId
});
@@ -1,10 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TProjectTemplates } from "@app/db/schemas";
import { ProjectType, TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import {
TCreateProjectTemplateDTO,
@@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla
...rest,
environments: environments as TProjectTemplateEnvironment[],
roles: [
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map(
({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})
),
...(roles as TProjectTemplateRole[]).map((role) => ({
...role,
permissions: unpackPermissions(role.permissions)
@@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({
permissionService,
projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
@@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId
orgId: actor.orgId,
...(type ? { type } : {})
});
return [
getDefaultProjectTemplate(actor.orgId),
...(type
? [getDefaultProjectTemplate(actor.orgId, type)]
: Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))),
...projectTemplates.map((template) => $unpackProjectTemplate(template))
];
};
@@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({
};
const createProjectTemplate = async (
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
{ roles, environments, type, ...params }: TCreateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
@@ -154,6 +160,17 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
if (environments && type !== ProjectType.SecretManager) {
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
}
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Failed to create project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
});
}
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
@@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({
const projectTemplate = await projectTemplateDAL.create({
...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: JSON.stringify(environments),
orgId: actor.orgId
environments:
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
orgId: actor.orgId,
type
});
return $unpackProjectTemplate(projectTemplate);
@@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (projectTemplate.type !== ProjectType.SecretManager && environments)
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
if (projectTemplate.type === ProjectType.SecretManager && environments === null)
throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" });
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Failed to update project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
});
}
if (params.name && projectTemplate.name !== params.name) {
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
@@ -1,6 +1,6 @@
import { z } from "zod";
import { TProjectEnvironments } from "@app/db/schemas";
import { ProjectType, TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
@@ -15,8 +15,9 @@ export type TProjectTemplateRole = {
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
type: ProjectType;
roles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
environments?: TProjectTemplateEnvironment[] | null;
};
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
@@ -334,7 +334,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecret).as("commitSecretId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecret).as("commitId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
`DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank`
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
@@ -483,7 +483,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
`DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank`
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
@@ -0,0 +1,11 @@
import { getConfig } from "@app/lib/config/env";
export const canUseSecretScanning = (orgId: string) => {
const appCfg = getConfig();
if (!appCfg.isCloud) {
return true;
}
return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId);
};
@@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors";
import { TGitAppDALFactory } from "./git-app-dal";
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
import { canUseSecretScanning } from "./secret-scanning-fns";
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
import {
SecretScanningRiskStatus,
@@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({
actorAuthMethod,
actorOrgId
}: TInstallAppSessionDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex");
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
return { sessionId };
return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG };
};
const linkInstallationToOrg = async ({
@@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
if (canUseSecretScanning(actorOrgId)) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
@@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({
)
);
}
return { installatedApp };
};
@@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({
};
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
const appCfg = getConfig();
const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) {
return;
@@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
if (canUseSecretScanning(installationLink.orgId)) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },
@@ -28,6 +28,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.where(`${TableName.SshHostGroup}.projectId`, projectId)
.select(
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
@@ -35,7 +36,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.SshHostGroup),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
db.ref("username").withSchema(TableName.Users),
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
)
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
@@ -69,7 +71,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
}
}));
return {
@@ -99,6 +102,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
.select(
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
@@ -106,7 +110,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.SshHostGroup),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
db.ref("username").withSchema(TableName.Users),
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
);
if (rows.length === 0) return null;
@@ -121,7 +126,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
}
}));
@@ -12,6 +12,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
import {
@@ -43,8 +44,12 @@ type TSshHostGroupServiceFactoryDep = {
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
userDAL: Pick<TUserDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
};
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
@@ -58,7 +63,8 @@ export const sshHostGroupServiceFactory = ({
sshHostLoginUserMappingDAL,
userDAL,
permissionService,
licenseService
licenseService,
groupDAL
}: TSshHostGroupServiceFactoryDep) => {
const createSshHostGroup = async ({
projectId,
@@ -127,6 +133,7 @@ export const sshHostGroupServiceFactory = ({
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
groupDAL,
userDAL,
permissionService,
projectId,
@@ -179,13 +186,42 @@ export const sshHostGroupServiceFactory = ({
});
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
await sshHostGroupDAL.updateById(
sshHostGroupId,
{
name
},
tx
);
if (name && name !== sshHostGroup.name) {
// (dangtony98): room to optimize check to ensure that
// the SSH host group name is unique across the whole org
const project = await projectDAL.findById(sshHostGroup.projectId, tx);
if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` });
const projects = await projectDAL.find(
{
orgId: project.orgId
},
{ tx }
);
const existingSshHostGroup = await sshHostGroupDAL.find(
{
name,
$in: {
projectId: projects.map((p) => p.id)
}
},
{ tx }
);
if (existingSshHostGroup.length) {
throw new BadRequestError({
message: `SSH host group with name '${name}' already exists in the organization`
});
}
await sshHostGroupDAL.updateById(
sshHostGroupId,
{
name
},
tx
);
}
if (loginMappings) {
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
if (loginMappings.length) {
@@ -194,6 +230,7 @@ export const sshHostGroupServiceFactory = ({
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
groupDAL,
userDAL,
permissionService,
projectId: sshHostGroup.projectId,
@@ -9,12 +9,7 @@ export type TCreateSshHostGroupDTO = {
export type TUpdateSshHostGroupDTO = {
sshHostGroupId: string;
name?: string;
loginMappings?: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
loginMappings?: TLoginMapping[];
} & Omit<TProjectPermission, "projectId">;
export type TGetSshHostGroupDTO = {
@@ -31,8 +31,18 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUser}.id`,
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.SshHostLoginUserMapping}.groupId`
)
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
.andWhere((bd) => {
void bd
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
})
.select(
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
@@ -58,8 +68,17 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.SshHostLoginUserMapping}.groupId`
)
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
.andWhere((bd) => {
void bd
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
})
.select(
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost),
@@ -133,6 +152,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.where(`${TableName.SshHost}.projectId`, projectId)
.select(
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
@@ -144,6 +164,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
db.ref("username").withSchema(TableName.Users),
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
db.ref("userSshCaId").withSchema(TableName.SshHost),
db.ref("hostSshCaId").withSchema(TableName.SshHost)
)
@@ -163,10 +184,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.select(
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
db.ref("username").withSchema(TableName.Users)
db.ref("username").withSchema(TableName.Users),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
)
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
@@ -185,7 +208,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
},
source: LoginMappingSource.HOST
}));
@@ -197,7 +221,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
},
source: LoginMappingSource.HOST_GROUP
}));
@@ -229,6 +254,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.where(`${TableName.SshHost}.id`, sshHostId)
.select(
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
@@ -241,7 +267,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("username").withSchema(TableName.Users),
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
db.ref("userSshCaId").withSchema(TableName.SshHost),
db.ref("hostSshCaId").withSchema(TableName.SshHost)
db.ref("hostSshCaId").withSchema(TableName.SshHost),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
);
if (rows.length === 0) return null;
@@ -257,7 +284,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
},
source: LoginMappingSource.HOST
}));
@@ -275,10 +303,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
)
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
.select(
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
db.ref("username").withSchema(TableName.Users)
db.ref("username").withSchema(TableName.Users),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
);
const groupGrouped = groupBy(
@@ -289,7 +319,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
loginUser,
allowedPrincipals: {
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
},
source: LoginMappingSource.HOST_GROUP
}));
@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission";
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
/**
@@ -15,6 +16,7 @@ export const createSshLoginMappings = async ({
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
groupDAL,
userDAL,
permissionService,
projectId,
@@ -35,7 +37,7 @@ export const createSshLoginMappings = async ({
tx
);
if (allowedPrincipals.usernames.length > 0) {
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
const users = await userDAL.find(
{
$in: {
@@ -74,6 +76,41 @@ export const createSshLoginMappings = async ({
tx
);
}
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
const projectGroups = await groupDAL.findGroupsByProjectId(projectId);
const groups = projectGroups.filter((g) => allowedPrincipals.groups?.includes(g.slug));
if (groups.length !== allowedPrincipals.groups?.length) {
throw new BadRequestError({
message: `Invalid group slugs: ${allowedPrincipals.groups
.filter((g) => !projectGroups.some((pg) => pg.slug === g))
.join(", ")}`
});
}
for await (const group of groups) {
// check that each group has access to the SSH project and have read access to hosts
const hasPermission = await permissionService.checkGroupProjectPermission({
groupId: group.id,
projectId,
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
});
if (!hasPermission) {
throw new BadRequestError({
message: `Group ${group.slug} does not have access to the SSH project`
});
}
}
await sshHostLoginUserMappingDAL.insertMany(
groups.map((group) => ({
sshHostLoginUserId: sshHostLoginUser.id,
groupId: group.id
})),
tx
);
}
}
};
@@ -15,7 +15,24 @@ export const sanitizedSshHost = SshHostsSchema.pick({
export const loginMappingSchema = z.object({
loginUser: z.string().trim(),
allowedPrincipals: z.object({
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
})
allowedPrincipals: z
.object({
usernames: z
.array(z.string().trim())
.transform((usernames) => Array.from(new Set(usernames)))
.optional(),
groups: z
.array(z.string().trim())
.transform((groups) => Array.from(new Set(groups)))
.optional()
})
.refine(
(data) => {
return (data.usernames && data.usernames.length > 0) || (data.groups && data.groups.length > 0);
},
{
message: "At least one username or group must be provided",
path: ["allowedPrincipals"]
}
)
});
@@ -1,6 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
@@ -19,6 +20,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
import {
convertActorToPrincipals,
createSshCert,
@@ -39,12 +41,14 @@ import {
type TSshHostServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById" | "find">;
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembershipsByUserIdInOrg">;
sshHostDAL: Pick<
TSshHostDALFactory,
| "transaction"
@@ -58,7 +62,10 @@ type TSshHostServiceFactoryDep = {
>;
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
@@ -66,6 +73,8 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
export const sshHostServiceFactory = ({
userDAL,
userGroupMembershipDAL,
groupDAL,
projectDAL,
projectSshConfigDAL,
sshCertificateAuthorityDAL,
@@ -208,6 +217,7 @@ export const sshHostServiceFactory = ({
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
groupDAL,
userDAL,
permissionService,
projectId,
@@ -278,6 +288,7 @@ export const sshHostServiceFactory = ({
loginMappings,
sshHostLoginUserDAL,
sshHostLoginUserMappingDAL,
groupDAL,
userDAL,
permissionService,
projectId: host.projectId,
@@ -324,7 +335,7 @@ export const sshHostServiceFactory = ({
return host;
};
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
const getSshHostById = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
if (!host) {
throw new NotFoundError({
@@ -387,10 +398,14 @@ export const sshHostServiceFactory = ({
userDAL
});
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actorId, actorOrgId);
const userGroupSlugs = userGroups.map((g) => g.groupSlug);
const mapping = host.loginMappings.find(
(m) =>
m.loginUser === loginUser &&
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
(m.allowedPrincipals.usernames?.some((allowed) => internalPrincipals.includes(allowed)) ||
m.allowedPrincipals.groups?.some((allowed) => userGroupSlugs.includes(allowed)))
);
if (!mapping) {
@@ -616,7 +631,7 @@ export const sshHostServiceFactory = ({
createSshHost,
updateSshHost,
deleteSshHost,
getSshHost,
getSshHostById,
issueSshHostUserCert,
issueSshHostHostCert,
getSshHostUserCaPk,
@@ -7,12 +7,15 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
export type TLoginMapping = {
loginUser: string;
allowedPrincipals: {
usernames: string[];
usernames?: string[];
groups?: string[];
};
};
@@ -63,7 +66,8 @@ type BaseCreateSshLoginMappingsDTO = {
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
userDAL: Pick<TUserDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission" | "checkGroupProjectPermission">;
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
projectId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;