diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index d4596115e..1ece89639 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -118,9 +118,6 @@ WORKDIR /backend ENV TELEMETRY_ENABLED true -HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ - CMD node healthcheck.js - EXPOSE 8080 EXPOSE 443 diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index c1c750225..09ab05443 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -10,7 +10,7 @@ import { seedData1 } from "@app/db/seed-data"; import { initEnvConfig } from "@app/lib/config/env"; import { initLogger } from "@app/lib/logger"; import { main } from "@app/server/app"; -import { AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { mockQueue } from "./mocks/queue"; import { mockSmtpServer } from "./mocks/smtp"; @@ -52,6 +52,8 @@ export default { authTokenType: AuthTokenType.ACCESS_TOKEN, userId: seedData1.id, tokenVersionId: seedData1.token.id, + authMethod: AuthMethod.EMAIL, + organizationId: seedData1.organization.id, accessVersion: 1 }, cfg.AUTH_SECRET, diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 649c54c54..0b59aa8a9 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -19,7 +19,7 @@ import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; -import { ActorType } from "@app/services/auth/auth-type"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; @@ -59,6 +59,7 @@ declare module "fastify" { // identity injection. depending on which kinda of token the information is filled in auth auth: TAuthMode; permission: { + authMethod: ActorAuthMethod; type: ActorType; id: string; orgId?: string; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index de472ff29..d160f97c6 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -122,6 +122,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, orgId: req.query.organizationId, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); return ldap; @@ -151,6 +152,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, orgId: req.body.organizationId, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); @@ -184,6 +186,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, orgId: req.body.organizationId, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 41cd11f7d..bd3fdca18 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -24,6 +24,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod, billingCycle: req.query.billingCycle }); return data; @@ -45,6 +46,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return { plan }; @@ -66,6 +68,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const data = await server.services.license.getOrgPlan({ actorId: req.permission.id, actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -89,6 +93,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod, success_url: req.body.success_url }); return data; @@ -110,6 +115,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -131,6 +137,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -152,6 +159,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -173,6 +181,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -198,6 +207,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, name: req.body.name, email: req.body.email @@ -221,6 +231,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; @@ -246,6 +257,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, success_url: req.body.success_url, cancel_url: req.body.cancel_url @@ -271,6 +283,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const data = await server.services.license.delOrgPmtMethods({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, pmtMethodId: req.params.pmtMethodId @@ -295,6 +308,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const data = await server.services.license.getOrgTaxIds({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId }); @@ -322,6 +336,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const data = await server.services.license.addOrgTaxId({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, type: req.body.type, @@ -348,6 +363,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const data = await server.services.license.delOrgTaxId({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, taxId: req.params.taxId @@ -373,7 +389,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, - orgId: req.params.organizationId + orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod }); return data; } @@ -396,6 +413,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); return data; diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 1e40d2d80..3ecf6fbfd 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -19,7 +19,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { .min(1) .trim() .refine( - (val) => Object.keys(OrgMembershipRole).includes(val), + (val) => !Object.keys(OrgMembershipRole).includes(val), "Please choose a different slug, the slug you have entered is reserved" ) .refine((v) => slugify(v) === v, { @@ -41,6 +41,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.body, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -84,6 +85,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { req.params.organizationId, req.params.roleId, req.body, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -110,6 +112,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.params.roleId, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -138,6 +141,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { const roles = await server.services.orgRole.listRoles( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { data: { roles } }; @@ -163,6 +167,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { const { permissions, membership } = await server.services.orgRole.getUserPermission( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { permissions, membership }; diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index f6fd53e5e..a08a9851b 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -31,6 +31,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.projectId, req.body, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -65,6 +66,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { req.params.projectId, req.params.roleId, req.body, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -92,6 +94,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.projectId, req.params.roleId, + req.permission.authMethod, req.permission.orgId ); return { role }; @@ -121,6 +124,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { req.permission.type, req.permission.id, req.params.projectId, + req.permission.authMethod, req.permission.orgId ); return { data: { roles } }; @@ -148,6 +152,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { const { permissions, membership } = await server.services.projectRole.getUserPermission( req.permission.id, req.params.projectId, + req.permission.authMethod, req.permission.orgId ); return { data: { permissions, membership } }; diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index 606448372..3fbe3f026 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -38,6 +38,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const secretSnapshots = await server.services.snapshot.listSnapshots({ actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, @@ -69,6 +70,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const count = await server.services.snapshot.projectSecretSnapshotCount({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, environment: req.query.environment, @@ -130,6 +132,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const auditLogs = await server.services.auditLog.listProjectAuditLogs({ actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, projectId: req.params.workspaceId, ...req.query, auditLogActor: req.query.actor, diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index fe387143e..b30afda13 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -231,6 +231,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.query.organizationId, type: "org" }); @@ -259,6 +260,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { const saml = await server.services.saml.createSamlCfg({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.body.organizationId, ...req.body @@ -290,6 +292,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { const saml = await server.services.saml.updateSamlCfg({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.body.organizationId, ...req.body diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index 2a3772cd6..965aa94e1 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -39,6 +39,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorOrgId: req.permission.orgId, orgId: req.body.organizationId, + actorAuthMethod: req.permission.authMethod, description: req.body.description, ttlDays: req.body.ttlDays }); @@ -65,6 +66,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { const scimTokens = await server.services.scim.listScimTokens({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.query.organizationId }); @@ -92,6 +94,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { scimTokenId: req.params.scimTokenId, actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 8fce232a7..1ea16178f 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -34,6 +34,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body, @@ -72,6 +73,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, secretPolicyId: req.params.sapId @@ -98,6 +100,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPolicyId: req.params.sapId }); @@ -123,6 +126,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.query.workspaceId }); @@ -150,6 +154,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.query.workspaceId, ...req.query diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 97eb89109..16b6d205b 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -52,6 +52,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const approvals = await server.services.secretApprovalRequest.getSecretApprovals({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, projectId: req.query.workspaceId @@ -81,6 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const approvals = await server.services.secretApprovalRequest.requestCount({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.query.workspaceId }); @@ -106,6 +108,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, approvalId: req.params.id }); @@ -134,6 +137,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const review = await server.services.secretApprovalRequest.reviewApproval({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, approvalId: req.params.id, status: req.body.status @@ -163,6 +167,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const approval = await server.services.secretApprovalRequest.updateApprovalStatus({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, approvalId: req.params.id, status: req.body.status @@ -271,6 +276,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.id }); diff --git a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts index e7201b73f..516885936 100644 --- a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts +++ b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts @@ -30,6 +30,7 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro const providers = await server.services.secretRotation.getProviderTemplates({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId }); diff --git a/backend/src/ee/routes/v1/secret-rotation-router.ts b/backend/src/ee/routes/v1/secret-rotation-router.ts index 8d2e90ac0..447280cf8 100644 --- a/backend/src/ee/routes/v1/secret-rotation-router.ts +++ b/backend/src/ee/routes/v1/secret-rotation-router.ts @@ -39,6 +39,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = handler: async (req) => { const secretRotation = await server.services.secretRotation.createRotation({ actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actorOrgId: req.permission.orgId, ...req.body, @@ -74,6 +75,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = const secretRotation = await server.services.secretRotation.restartById({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, rotationId: req.body.id }); @@ -125,6 +127,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = const secretRotations = await server.services.secretRotation.getByProjectId({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.query.workspaceId }); @@ -158,6 +161,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) = const secretRotation = await server.services.secretRotation.deleteById({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, rotationId: req.params.id }); diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index 7d2c5f1ee..1f8d56c74 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -22,6 +22,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = const session = await server.services.secretScanning.createInstallationSession({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.body.organizationId }); @@ -46,6 +47,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); @@ -67,6 +69,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId }); @@ -88,6 +91,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = const { risks } = await server.services.secretScanning.getRisksByOrg({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId }); @@ -110,6 +114,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = const { risk } = await server.services.secretScanning.updateRiskStatus({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.organizationId, riskId: req.params.riskId, diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts index 89ee4e011..630af10cf 100644 --- a/backend/src/ee/routes/v1/secret-version-router.ts +++ b/backend/src/ee/routes/v1/secret-version-router.ts @@ -27,6 +27,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) => const secretVersions = await server.services.secret.getSecretVersions({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, limit: req.query.limit, offset: req.query.offset, diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 79161bfd3..902d707af 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -47,6 +47,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { const secretSnapshot = await server.services.snapshot.getSnapshotData({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); @@ -79,6 +80,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { const secretSnapshot = await server.services.snapshot.rollbackSnapshot({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.secretSnapshotId }); diff --git a/backend/src/ee/routes/v1/trusted-ip-router.ts b/backend/src/ee/routes/v1/trusted-ip-router.ts index 53bc5b117..3d4ac8010 100644 --- a/backend/src/ee/routes/v1/trusted-ip-router.ts +++ b/backend/src/ee/routes/v1/trusted-ip-router.ts @@ -22,6 +22,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const trustedIps = await server.services.trustedIp.listIpsByProjectId({ + actorAuthMethod: req.permission.authMethod, projectId: req.params.workspaceId, actor: req.permission.type, actorId: req.permission.id, @@ -52,6 +53,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const { trustedIp, project } = await server.services.trustedIp.addProjectIp({ + actorAuthMethod: req.permission.authMethod, projectId: req.params.workspaceId, actor: req.permission.type, actorId: req.permission.id, @@ -99,6 +101,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, trustedIpId: req.params.trustedIpId, ...req.body @@ -140,6 +143,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, trustedIpId: req.params.trustedIpId }); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index c4d4aabc0..1564c6dcb 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -31,10 +31,17 @@ export const auditLogServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, projectId, auditLogActor }: TListProjectAuditLogDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); const auditLogs = await auditLogDAL.find({ startDate, diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index e9ae0264a..0316052cd 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -55,6 +55,7 @@ export const ldapConfigServiceFactory = ({ actorId, orgId, actorOrgId, + actorAuthMethod, isActive, url, bindDN, @@ -62,7 +63,7 @@ export const ldapConfigServiceFactory = ({ searchBase, caCert }: TCreateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -149,13 +150,14 @@ export const ldapConfigServiceFactory = ({ orgId, actorOrgId, isActive, + actorAuthMethod, url, bindDN, bindPass, searchBase, caCert }: TUpdateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -274,8 +276,14 @@ export const ldapConfigServiceFactory = ({ }; }; - const getLdapCfgWithPermissionCheck = async ({ actor, actorId, orgId, actorOrgId }: TOrgPermission) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getLdapCfgWithPermissionCheck = async ({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TOrgPermission) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); return getLdapCfg({ orgId diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index a55f2edff..e81f6dc12 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -224,9 +224,10 @@ export const licenseServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, billingCycle }: TOrgPlansTableDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` @@ -234,15 +235,22 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, projectId }: TOrgPlanDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const plan = await getPlan(orgId, projectId); return plan; }; - const startOrgTrial = async ({ orgId, actorId, actor, actorOrgId, success_url }: TStartOrgTrialDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const startOrgTrial = async ({ + orgId, + actorId, + actor, + actorOrgId, + actorAuthMethod, + success_url + }: TStartOrgTrialDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); @@ -263,8 +271,14 @@ export const licenseServiceFactory = ({ return { url }; }; - const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgId }: TCreateOrgPortalSession) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const createOrganizationPortalSession = async ({ + orgId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TCreateOrgPortalSession) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); @@ -310,8 +324,8 @@ export const licenseServiceFactory = ({ return { url }; }; - const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -327,8 +341,8 @@ export const licenseServiceFactory = ({ }; // returns org current plan feature table - const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -343,8 +357,8 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -364,11 +378,12 @@ export const licenseServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, orgId, name, email }: TUpdateOrgBillingDetailsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -387,8 +402,8 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgId }: TOrgPmtMethodsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -410,11 +425,12 @@ export const licenseServiceFactory = ({ orgId, actor, actorId, + actorAuthMethod, actorOrgId, success_url, cancel_url }: TAddOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -435,8 +451,15 @@ export const licenseServiceFactory = ({ return { url }; }; - const delOrgPmtMethods = async ({ actorId, actor, actorOrgId, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const delOrgPmtMethods = async ({ + actorId, + actor, + actorAuthMethod, + actorOrgId, + orgId, + pmtMethodId + }: TDelOrgPmtMethodDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -452,8 +475,8 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -470,8 +493,8 @@ export const licenseServiceFactory = ({ return taxIds; }; - const addOrgTaxId = async ({ actorId, actor, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -491,8 +514,8 @@ export const licenseServiceFactory = ({ return data; }; - const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgId, taxId }: TDelOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -508,8 +531,8 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, orgId }: TOrgInvoiceDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); @@ -525,8 +548,8 @@ export const licenseServiceFactory = ({ return invoices; }; - const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgId }: TOrgLicensesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); const organization = await orgDAL.findOrgById(orgId); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index d94589b43..fc0a85ac6 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -129,11 +129,18 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .join( + // Join the Project table to later select orgId + TableName.Project, + `${TableName.IdentityProjectMembership}.projectId`, + `${TableName.Project}.id` + ) .where("identityId", identityId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) .select( db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), @@ -144,16 +151,16 @@ export const permissionDALFactory = (db: TDbClient) => { const permission = sqlNestRelationships({ data: docs, key: "membershipId", - parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({ + parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField, orgId }) => ({ id: membershipId, identityId, projectId, role: oldRoleField, createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, + orgId, // just a prefilled value - orgAuthEnforced: false, - orgId: "" + orgAuthEnforced: false }), childrenMapper: [ { diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts new file mode 100644 index 000000000..78b8ad8f2 --- /dev/null +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -0,0 +1,23 @@ +import { TOrganizations } from "@app/db/schemas"; +import { UnauthorizedError } from "@app/lib/errors"; +import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; + +function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { + if (!actorAuthMethod) return false; + + return [AuthMethod.AZURE_SAML, AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.GOOGLE_SAML].includes( + actorAuthMethod + ); +} + +function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { + if (actorAuthMethod === undefined) { + throw new UnauthorizedError({ name: "No auth method defined" }); + } + + if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) { + throw new UnauthorizedError({ name: "Cannot access org-scoped resource" }); + } +} + +export { isAuthMethodSaml, validateOrgSAML }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index c7dcf4b8c..d632be3df 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -11,13 +11,15 @@ import { } from "@app/db/schemas"; import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; -import { ActorType } from "@app/services/auth/auth-type"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; +import { validateOrgSAML } from "./permission-fns"; import { TBuildProjectPermissionDTO } from "./permission-types"; import { buildServiceTokenProjectPermission, @@ -32,6 +34,7 @@ type TPermissionServiceFactoryDep = { orgRoleDAL: Pick; projectRoleDAL: Pick; serviceTokenDAL: Pick; + projectDAL: Pick; permissionDAL: TPermissionDALFactory; }; @@ -41,7 +44,8 @@ export const permissionServiceFactory = ({ permissionDAL, orgRoleDAL, projectRoleDAL, - serviceTokenDAL + serviceTokenDAL, + projectDAL }: TPermissionServiceFactoryDep) => { const buildOrgPermission = (role: string, permission?: unknown) => { switch (role) { @@ -98,16 +102,30 @@ export const permissionServiceFactory = ({ /* * Get user permission in an organization - * */ - const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => { + */ + const getUserOrgPermission = async ( + userId: string, + orgId: string, + authMethod: ActorAuthMethod, + userOrgId?: string + ) => { const membership = await permissionDAL.getOrgPermission(userId, orgId); if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgAuthEnforced && membership.orgId !== userOrgId) { - throw new BadRequestError({ name: "Cannot access org-scoped resource" }); + + // If the org ID is API_KEY, the request is being made with an API Key. + // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. + + // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. + // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. + if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { + throw new UnauthorizedError({ name: "You are not logged into this organization" }); } + + validateOrgSAML(authMethod, membership.orgAuthEnforced); + return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; @@ -120,10 +138,16 @@ export const permissionServiceFactory = ({ return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; - const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => { + const getOrgPermission = async ( + type: ActorType, + id: string, + orgId: string, + authMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { switch (type) { case ActorType.USER: - return getUserOrgPermission(id, orgId, actorOrgId); + return getUserOrgPermission(id, orgId, authMethod, actorOrgId); case ActorType.IDENTITY: return getIdentityOrgPermission(id, orgId); default: @@ -153,34 +177,39 @@ export const permissionServiceFactory = ({ const getUserProjectPermission = async ( userId: string, projectId: string, + authMethod: ActorAuthMethod, userOrgId?: string ): Promise> => { - const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); - if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" }); + const membership = await permissionDAL.getProjectPermission(userId, projectId); + if (!membership) throw new UnauthorizedError({ name: "User not in project" }); - if ( - userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) - ) { + if (membership.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) { - throw new BadRequestError({ name: "Cannot access org-scoped resource" }); + // If the org ID is API_KEY, the request is being made with an API Key. + // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. + + // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. + // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. + if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { + throw new UnauthorizedError({ name: "You are not logged into this organization" }); } + validateOrgSAML(authMethod, membership.orgAuthEnforced); + return { - permission: buildProjectPermission(userProjectPermission.roles), - membership: userProjectPermission, + permission: buildProjectPermission(membership.roles), + membership, hasRole: (role: string) => - userProjectPermission.roles.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 + membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1 }; }; const getIdentityProjectPermission = async ( identityId: string, - projectId: string + projectId: string, + identityOrgId: string | undefined ): Promise> => { const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" }); @@ -193,6 +222,10 @@ export const permissionServiceFactory = ({ throw new BadRequestError({ name: "Custom permission not found" }); } + if (identityProjectPermission.orgId !== identityOrgId) { + throw new UnauthorizedError({ name: "You are not a member of this organization" }); + } + return { permission: buildProjectPermission(identityProjectPermission.roles), membership: identityProjectPermission, @@ -203,14 +236,32 @@ export const permissionServiceFactory = ({ }; }; - const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => { + const getServiceTokenProjectPermission = async ( + serviceTokenId: string, + projectId: string, + actorOrgId: string | undefined + ) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); if (!serviceToken) throw new BadRequestError({ message: "Service token not found" }); + const serviceTokenProject = await projectDAL.findById(serviceToken.projectId); + + if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" }); + + if (serviceTokenProject.orgId !== actorOrgId) { + throw new UnauthorizedError({ message: "Service token not a part of this organization" }); + } + if (serviceToken.projectId !== projectId) throw new UnauthorizedError({ message: "Failed to find service authorization for given project" }); + + if (serviceTokenProject.orgId !== actorOrgId) + throw new UnauthorizedError({ + message: "Failed to find service authorization for given project" + }); + const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), @@ -238,15 +289,16 @@ export const permissionServiceFactory = ({ type: T, id: string, projectId: string, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ): Promise> => { switch (type) { case ActorType.USER: - return getUserProjectPermission(id, projectId, actorOrgId) as Promise>; + return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise>; case ActorType.SERVICE: - return getServiceTokenProjectPermission(id, projectId) as Promise>; + return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise>; case ActorType.IDENTITY: - return getIdentityProjectPermission(id, projectId) as Promise>; + return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise>; default: throw new UnauthorizedError({ message: "Permission not defined", diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index e9249d4aa..dc9728957 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -55,6 +55,7 @@ export const samlConfigServiceFactory = ({ const createSamlCfg = async ({ cert, actor, + actorAuthMethod, actorOrgId, orgId, issuer, @@ -63,7 +64,7 @@ export const samlConfigServiceFactory = ({ entryPoint, authProvider }: TCreateSamlCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); @@ -146,6 +147,7 @@ export const samlConfigServiceFactory = ({ orgId, actor, actorOrgId, + actorAuthMethod, cert, actorId, issuer, @@ -153,7 +155,7 @@ export const samlConfigServiceFactory = ({ entryPoint, authProvider }: TUpdateSamlCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); if (!plan.samlSSO) @@ -238,6 +240,7 @@ export const samlConfigServiceFactory = ({ dto.actor, dto.actorId, ssoConfig.orgId, + dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index ec7c066fc..9aedf5d19 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -1,5 +1,5 @@ import { TOrgPermission } from "@app/lib/types"; -import { ActorType } from "@app/services/auth/auth-type"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; export enum SamlProviders { OKTA_SAML = "okta-saml", @@ -26,7 +26,14 @@ export type TUpdateSamlCfgDTO = Partial<{ TOrgPermission; export type TGetSamlCfgDTO = - | { type: "org"; orgId: string; actor: ActorType; actorId: string; actorOrgId?: string } + | { + type: "org"; + orgId: string; + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + } | { type: "orgSlug"; orgSlug: string; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index c542b2340..6ed594777 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -56,8 +56,16 @@ export const scimServiceFactory = ({ permissionService, smtpService }: TScimServiceFactoryDep) => { - const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const createScimToken = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + orgId, + description, + ttlDays + }: TCreateScimTokenDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -85,8 +93,8 @@ export const scimServiceFactory = ({ return { scimToken }; }; - const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const listScimTokens = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TOrgPermission) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -99,11 +107,17 @@ export const scimServiceFactory = ({ return scimTokens; }; - const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => { + const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => { let scimToken = await scimDAL.findById(scimTokenId); if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + scimToken.orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(scimToken.orgId); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 9d65ec7cc..8ddadb9bf 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -45,6 +45,7 @@ export const secretApprovalPolicyServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, approvals, approvers, projectId, @@ -54,7 +55,13 @@ export const secretApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -98,6 +105,7 @@ export const secretApprovalPolicyServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, approvals, secretPolicyId }: TUpdateSapDTO) => { @@ -108,6 +116,7 @@ export const secretApprovalPolicyServiceFactory = ({ actor, actorId, secretApprovalPolicy.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -152,7 +161,13 @@ export const secretApprovalPolicyServiceFactory = ({ }; }; - const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId, actorOrgId }: TDeleteSapDTO) => { + const deleteSecretApprovalPolicy = async ({ + secretPolicyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TDeleteSapDTO) => { const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -160,6 +175,7 @@ export const secretApprovalPolicyServiceFactory = ({ actor, actorId, sapPolicy.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( @@ -171,8 +187,20 @@ export const secretApprovalPolicyServiceFactory = ({ return sapPolicy; }; - const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TListSapDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getSecretApprovalPolicyByProjectId = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TListSapDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); const sapPolicies = await secretApprovalPolicyDAL.find({ projectId }); @@ -201,10 +229,17 @@ export const secretApprovalPolicyServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, secretPath }: TGetBoardSapDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { secretPath, environment }) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index b48b6bf95..b8ad89e45 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -82,13 +82,14 @@ export const secretApprovalRequestServiceFactory = ({ secretVersionDAL, secretQueueService }: TSecretApprovalRequestServiceFactoryDep) => { - const requestCount = async ({ projectId, actor, actorId, actorOrgId }: TApprovalRequestCountDTO) => { + const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); const { membership } = await permissionService.getProjectPermission( actor as ActorType.USER, actorId, projectId, + actorAuthMethod, actorOrgId ); @@ -100,6 +101,7 @@ export const secretApprovalRequestServiceFactory = ({ projectId, actorId, actor, + actorAuthMethod, actorOrgId, status, environment, @@ -109,7 +111,13 @@ export const secretApprovalRequestServiceFactory = ({ }: TListApprovalsDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { membership } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); const approvals = await secretApprovalRequestDAL.findByProjectId({ projectId, committer, @@ -122,7 +130,13 @@ export const secretApprovalRequestServiceFactory = ({ return approvals; }; - const getSecretApprovalDetails = async ({ actor, actorId, actorOrgId, id }: TSecretApprovalDetailsDTO) => { + const getSecretApprovalDetails = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + id + }: TSecretApprovalDetailsDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); const secretApprovalRequest = await secretApprovalRequestDAL.findById(id); @@ -133,6 +147,7 @@ export const secretApprovalRequestServiceFactory = ({ actor, actorId, secretApprovalRequest.projectId, + actorAuthMethod, actorOrgId ); if ( @@ -150,7 +165,14 @@ export const secretApprovalRequestServiceFactory = ({ return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets }; }; - const reviewApproval = async ({ approvalId, actor, status, actorId, actorOrgId }: TReviewRequestDTO) => { + const reviewApproval = async ({ + approvalId, + actor, + status, + actorId, + actorAuthMethod, + actorOrgId + }: TReviewRequestDTO) => { const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); @@ -160,6 +182,7 @@ export const secretApprovalRequestServiceFactory = ({ ActorType.USER, actorId, secretApprovalRequest.projectId, + actorAuthMethod, actorOrgId ); if ( @@ -192,7 +215,14 @@ export const secretApprovalRequestServiceFactory = ({ return reviewStatus; }; - const updateApprovalStatus = async ({ actorId, status, approvalId, actor, actorOrgId }: TStatusChangeDTO) => { + const updateApprovalStatus = async ({ + actorId, + status, + approvalId, + actor, + actorOrgId, + actorAuthMethod + }: TStatusChangeDTO) => { const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); @@ -202,6 +232,7 @@ export const secretApprovalRequestServiceFactory = ({ ActorType.USER, actorId, secretApprovalRequest.projectId, + actorAuthMethod, actorOrgId ); if ( @@ -229,7 +260,8 @@ export const secretApprovalRequestServiceFactory = ({ approvalId, actor, actorId, - actorOrgId + actorOrgId, + actorAuthMethod }: TMergeSecretApprovalRequestDTO) => { const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); @@ -240,8 +272,10 @@ export const secretApprovalRequestServiceFactory = ({ ActorType.USER, actorId, projectId, + actorAuthMethod, actorOrgId ); + if ( !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerId !== membership.id && @@ -438,6 +472,7 @@ export const secretApprovalRequestServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, policy, projectId, secretPath, @@ -449,6 +484,7 @@ export const secretApprovalRequestServiceFactory = ({ actor, actorId, projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts index c67477bfd..5a2e478e1 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -9,6 +9,7 @@ import jmespath from "jmespath"; import knex from "knex"; import { getConfig } from "@app/lib/config/env"; +import { getDbConnectionHost } from "@app/lib/knex"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types"; @@ -89,7 +90,7 @@ export const secretRotationDbFn = async ({ const appCfg = getConfig(); const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; - if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host)) + if (host === "localhost" || host === "127.0.0.1" || getDbConnectionHost(appCfg.DB_CONNECTION_URI) === host) throw new Error("Invalid db host"); const db = knex({ diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 75c19c6e9..1e1648a66 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -39,8 +39,20 @@ export const secretRotationServiceFactory = ({ folderDAL, secretDAL }: TSecretRotationServiceFactoryDep) => { - const getProviderTemplates = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getProviderTemplates = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); return { @@ -54,6 +66,7 @@ export const secretRotationServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, inputs, outputs, interval, @@ -61,7 +74,13 @@ export const secretRotationServiceFactory = ({ secretPath, environment }: TCreateSecretRotationDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation @@ -139,14 +158,20 @@ export const secretRotationServiceFactory = ({ return secretRotation; }; - const getByProjectId = async ({ actorId, projectId, actor, actorOrgId }: TListByProjectIdDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getByProjectId = async ({ actorId, projectId, actor, actorOrgId, actorAuthMethod }: TListByProjectIdDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); const doc = await secretRotationDAL.find({ projectId }); return doc; }; - const restartById = async ({ actor, actorId, actorOrgId, rotationId }: TRestartDTO) => { + const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => { const doc = await secretRotationDAL.findById(rotationId); if (!doc) throw new BadRequestError({ message: "Rotation not found" }); @@ -157,18 +182,30 @@ export const secretRotationServiceFactory = ({ message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation." }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + doc.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); await secretRotationQueue.removeFromQueue(doc.id, doc.interval); await secretRotationQueue.addToQueue(doc.id, doc.interval); return doc; }; - const deleteById = async ({ actor, actorId, actorOrgId, rotationId }: TDeleteDTO) => { + const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => { const doc = await secretRotationDAL.findById(rotationId); if (!doc) throw new BadRequestError({ message: "Rotation not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + doc.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 7066fd485..9b78da3af 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -39,8 +39,14 @@ export const secretScanningServiceFactory = ({ permissionService, secretScanningQueue }: TSecretScanningServiceFactoryDep) => { - const createInstallationSession = async ({ actor, orgId, actorId, actorOrgId }: TInstallAppSessionDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const createInstallationSession = async ({ + actor, + orgId, + actorId, + actorAuthMethod, + actorOrgId + }: TInstallAppSessionDTO) => { + 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"); @@ -53,12 +59,19 @@ export const secretScanningServiceFactory = ({ actorId, installationId, actor, + actorAuthMethod, actorOrgId }: TLinkInstallSessionDTO) => { const session = await gitAppInstallSessionDAL.findOne({ sessionId }); if (!session) throw new UnauthorizedError({ message: "Session not found" }); - const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + session.orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const installatedApp = await gitAppOrgDAL.transaction(async (tx) => { await gitAppInstallSessionDAL.deleteById(session.id, tx); @@ -89,23 +102,37 @@ export const secretScanningServiceFactory = ({ return { installatedApp }; }; - const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgId }: TGetOrgInstallStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getOrgInstallationStatus = async ({ + actorId, + orgId, + actor, + actorAuthMethod, + actorOrgId + }: TGetOrgInstallStatusDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const appInstallation = await gitAppOrgDAL.findOne({ orgId }); return Boolean(appInstallation); }; - const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgId }: TGetOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); return { risks }; }; - const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgId, riskId, status }: TUpdateRiskStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const updateRiskStatus = async ({ + actorId, + orgId, + actor, + actorOrgId, + actorAuthMethod, + riskId, + status + }: TUpdateRiskStatusDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); const isRiskResolved = Boolean( diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 6ec7a23d5..de2f6efcb 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -59,9 +59,16 @@ export const secretSnapshotServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, path }: TProjectSnapshotCountDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -77,11 +84,18 @@ export const secretSnapshotServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, path, limit = 20, offset = 0 }: TProjectSnapshotListDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); const folder = await folderDAL.findBySecretPath(projectId, environment, path); @@ -91,10 +105,16 @@ export const secretSnapshotServiceFactory = ({ return snapshots; }; - const getSnapshotData = async ({ actorId, actor, actorOrgId, id }: TGetSnapshotDataDTO) => { + const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => { const snapshot = await snapshotDAL.findSecretSnapshotDataById(id); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + snapshot.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); return snapshot; }; @@ -145,11 +165,23 @@ export const secretSnapshotServiceFactory = ({ } }; - const rollbackSnapshot = async ({ id: snapshotId, actor, actorId, actorOrgId }: TRollbackSnapshotDTO) => { + const rollbackSnapshot = async ({ + id: snapshotId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TRollbackSnapshotDTO) => { const snapshot = await snapshotDAL.findById(snapshotId); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + snapshot.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts index 14c73db1f..ecd2b3070 100644 --- a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts +++ b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts @@ -26,8 +26,14 @@ export const trustedIpServiceFactory = ({ licenseService, projectDAL }: TTrustedIpServiceFactoryDep) => { - const listIpsByProjectId = async ({ projectId, actor, actorId, actorOrgId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); const trustedIps = await trustedIpDAL.find({ projectId @@ -38,13 +44,20 @@ export const trustedIpServiceFactory = ({ const addProjectIp = async ({ projectId, actorId, + actorAuthMethod, actor, actorOrgId, ipAddress: ip, comment, isActive }: TCreateIpDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); @@ -78,11 +91,18 @@ export const trustedIpServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, ipAddress: ip, comment, trustedIpId }: TUpdateIpDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); @@ -113,8 +133,21 @@ export const trustedIpServiceFactory = ({ return { trustedIp, project }; // for audit log }; - const deleteProjectIp = async ({ projectId, actorId, actor, actorOrgId, trustedIpId }: TDeleteIpDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const deleteProjectIp = async ({ + projectId, + actorId, + actor, + actorOrgId, + actorAuthMethod, + trustedIpId + }: TDeleteIpDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); const project = await projectDAL.findById(projectId); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 6f5cc2296..2e42f3388 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -84,7 +84,7 @@ export const ORGANIZATIONS = { export const PROJECTS = { CREATE: { - organizationId: "The ID of the organization to create the project in.", + organizationSlug: "The slug of the organization to create the project in.", projectName: "The name of the project to create.", slug: "An optional slug for the project." }, @@ -197,6 +197,7 @@ export const FOLDERS = { export const RAW_SECRETS = { LIST: { workspaceId: "The ID of the project to list secrets from.", + workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.", environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", includeImports: "Weather to include imported secrets or not." diff --git a/backend/src/lib/knex/connection.ts b/backend/src/lib/knex/connection.ts new file mode 100644 index 000000000..993615a0b --- /dev/null +++ b/backend/src/lib/knex/connection.ts @@ -0,0 +1,11 @@ +import { URL } from "url"; // Import the URL class + +export const getDbConnectionHost = (urlString: string) => { + try { + const url = new URL(urlString); + // Split hostname and port (if provided) + return url.hostname.split(":")[0]; + } catch (error) { + return null; + } +}; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 37fae624e..d78020809 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables"; import { DatabaseError } from "../errors"; +export * from "./connection"; export * from "./join"; export * from "./select"; diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 918322d62..7a34222a6 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -1,17 +1,19 @@ -import { ActorType } from "@app/services/auth/auth-type"; +import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; export type TOrgPermission = { actor: ActorType; actorId: string; orgId: string; - actorOrgId?: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; }; export type TProjectPermission = { actor: ActorType; actorId: string; projectId: string; - actorOrgId?: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; }; export type RequiredKeys = { diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 3a0a0ab39..dceb31c03 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -6,42 +6,49 @@ import { TServiceTokens, TUsers } from "@app/db/schemas"; import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; -import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; +import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; export type TAuthMode = | { - orgId?: string; authMode: AuthMode.JWT; actor: ActorType.USER; userId: string; tokenVersionId: string; // the session id of token used user: TUsers; + orgId?: string; + authMethod: AuthMethod; } | { authMode: AuthMode.API_KEY; + authMethod: null; actor: ActorType.USER; userId: string; user: TUsers; - orgId?: string; + orgId: string; } | { authMode: AuthMode.SERVICE_TOKEN; serviceToken: TServiceTokens & { createdByEmail: string }; actor: ActorType.SERVICE; serviceTokenId: string; + orgId: string; + authMethod: null; } | { authMode: AuthMode.IDENTITY_ACCESS_TOKEN; actor: ActorType.IDENTITY; identityId: string; identityName: string; + orgId: string; + authMethod: null; } | { authMode: AuthMode.SCIM_TOKEN; actor: ActorType.SCIM_CLIENT; scimTokenId: string; orgId: string; + authMethod: null; }; const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { @@ -50,6 +57,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const; } const authHeader = req.headers?.authorization; + if (!authHeader) return { authMode: null, token: null }; const authTokenValue = authHeader.slice(7); // slice of after Bearer @@ -71,6 +79,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { actor: ActorType.USER } as const; case AuthTokenType.API_KEY: + // throw new Error("API Key auth is no longer supported."); return { authMode: AuthMode.API_KEY, token: decodedToken, actor: ActorType.USER } as const; case AuthTokenType.IDENTITY_ACCESS_TOKEN: return { @@ -89,17 +98,30 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { } }; +// ! Important: You can only 100% count on the `req.permission.orgId` field being present when the auth method is Identity Access Token (Machine Identity). export const injectIdentity = fp(async (server: FastifyZodProvider) => { server.decorateRequest("auth", null); server.addHook("onRequest", async (req) => { const appCfg = getConfig(); const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET); + + if (req.url.includes("/api/v3/auth/")) { + return; + } if (!authMode) return; switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); - req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId }; + req.auth = { + authMode: AuthMode.JWT, + user, + userId: user.id, + tokenVersionId, + actor, + orgId, + authMethod: token.authMethod + }; break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { @@ -107,29 +129,40 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, + orgId: identity.orgId, identityId: identity.identityId, - identityName: identity.name + identityName: identity.name, + authMethod: null }; break; } case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); req.auth = { + orgId: serviceToken.orgId, authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, serviceTokenId: serviceToken.id, - actor + actor, + authMethod: null }; break; } case AuthMode.API_KEY: { const user = await server.services.apiKey.fnValidateApiKey(token as string); - req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user }; + req.auth = { + authMode: AuthMode.API_KEY as const, + userId: user.id, + actor, + user, + orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon! + authMethod: null + }; break; } case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId }; + req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; break; } default: diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 2d61647e8..084f18198 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -9,13 +9,33 @@ export const injectPermission = fp(async (server) => { if (!req.auth) return; if (req.auth.actor === ActorType.USER) { - req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId }; + req.permission = { + type: ActorType.USER, + id: req.auth.userId, + orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" + authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + }; } else if (req.auth.actor === ActorType.IDENTITY) { - req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId }; + req.permission = { + type: ActorType.IDENTITY, + id: req.auth.identityId, + orgId: req.auth.orgId, + authMethod: null + }; } else if (req.auth.actor === ActorType.SERVICE) { - req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId }; + req.permission = { + type: ActorType.SERVICE, + id: req.auth.serviceTokenId, + orgId: req.auth.orgId, + authMethod: null + }; } else if (req.auth.actor === ActorType.SCIM_CLIENT) { - req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId }; + req.permission = { + type: ActorType.SCIM_CLIENT, + id: req.auth.scimTokenId, + orgId: req.auth.orgId, + authMethod: null + }; } }); }); diff --git a/backend/src/server/plugins/auth/verify-auth.ts b/backend/src/server/plugins/auth/verify-auth.ts index a1274f356..3b3a239f7 100644 --- a/backend/src/server/plugins/auth/verify-auth.ts +++ b/backend/src/server/plugins/auth/verify-auth.ts @@ -3,15 +3,26 @@ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify"; import { UnauthorizedError } from "@app/lib/errors"; import { AuthMode } from "@app/services/auth/auth-type"; +interface TAuthOptions { + requireOrg: boolean; +} + export const verifyAuth = - (authStrats: AuthMode[]) => + (authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) => (req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => { - if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array"); + if (!Array.isArray(authStrategies)) throw new Error("Auth strategy must be array"); if (!req.auth) throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" }); - const isAccessAllowed = authStrats.some((strat) => strat === req.auth.authMode); + const isAccessAllowed = authStrategies.some((strategy) => strategy === req.auth.authMode); if (!isAccessAllowed) { throw new UnauthorizedError({ name: `${req.url} Unauthorized Access` }); } + + // New optional option. There are some routes which do not require an organization ID to be present on the request. + // An example of this is the /v1 auth routes. + if (req.auth.authMode === AuthMode.JWT && options.requireOrg === true && !req.permission.orgId) { + throw new UnauthorizedError({ name: `${req.url} Unauthorized Access, no organization found in request` }); + } + done(); }; diff --git a/backend/src/server/plugins/swagger.ts b/backend/src/server/plugins/swagger.ts index 1eb1a0f4e..2d7e6706c 100644 --- a/backend/src/server/plugins/swagger.ts +++ b/backend/src/server/plugins/swagger.ts @@ -14,13 +14,13 @@ export const fastifySwagger = fp(async (fastify) => { version: "0.0.1" }, servers: [ - { - url: "http://localhost:8080", - description: "Local server" - }, { url: "https://app.infisical.com", description: "Production server" + }, + { + url: "http://localhost:8080", + description: "Local server" } ], components: { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0a49806bd..c452bfbd3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -201,7 +201,8 @@ export const registerRoutes = async ( permissionDAL, orgRoleDAL, projectRoleDAL, - serviceTokenDAL + serviceTokenDAL, + projectDAL }); const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const trustedIpService = trustedIpServiceFactory({ @@ -266,7 +267,7 @@ export const registerRoutes = async ( const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); const userService = userServiceFactory({ userDAL }); - const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService }); + const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, @@ -372,6 +373,7 @@ export const registerRoutes = async ( projectKeyDAL, userDAL, projectEnvDAL, + orgDAL, orgService, projectMembershipDAL, folderDAL, @@ -517,7 +519,8 @@ export const registerRoutes = async ( projectEnvDAL, serviceTokenDAL, userDAL, - permissionService + permissionService, + projectDAL }); const identityService = identityServiceFactory({ @@ -525,7 +528,10 @@ export const registerRoutes = async ( identityDAL, identityOrgMembershipDAL }); - const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL }); + const identityAccessTokenService = identityAccessTokenServiceFactory({ + identityAccessTokenDAL, + identityOrgMembershipDAL + }); const identityProjectService = identityProjectServiceFactory({ permissionService, projectDAL, diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index bd45f59d9..16448b8ca 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -21,7 +21,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req, res) => { const appCfg = getConfig(); if (req.auth.authMode === AuthMode.JWT) { @@ -85,6 +85,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { const token = jwt.sign( { + authMethod: decodedToken.authMethod, authTokenType: AuthTokenType.ACCESS_TOKEN, userId: decodedToken.userId, tokenVersionId: tokenVersion.id, diff --git a/backend/src/server/routes/v1/bot-router.ts b/backend/src/server/routes/v1/bot-router.ts index 507423b0d..7e1d6ec94 100644 --- a/backend/src/server/routes/v1/bot-router.ts +++ b/backend/src/server/routes/v1/bot-router.ts @@ -30,6 +30,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, projectId: req.params.projectId }); return { bot }; @@ -70,6 +71,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, botId: req.params.botId, botKey: req.body.botKey, isActive: req.body.isActive diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index ac389b478..3ce13ca0a 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -35,6 +35,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { const identity = await server.services.identity.createIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, orgId: req.body.organizationId @@ -95,6 +96,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { const identity = await server.services.identity.updateIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.identityId, ...req.body @@ -140,6 +142,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { const identity = await server.services.identity.deleteIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.identityId }); diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-ua.ts index 6146fa242..ac5c78a08 100644 --- a/backend/src/server/routes/v1/identity-ua.ts +++ b/backend/src/server/routes/v1/identity-ua.ts @@ -131,6 +131,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, ...req.body, identityId: req.params.identityId }); @@ -212,6 +213,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, ...req.body, identityId: req.params.identityId }); @@ -260,6 +262,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { const identityUniversalAuth = await server.services.identityUa.getIdentityUa({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId }); @@ -309,6 +312,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, ...req.body @@ -354,6 +358,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId }); @@ -397,6 +402,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { const clientSecretData = await server.services.identityUa.revokeUaClientSecret({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, clientSecretId: req.params.clientSecretId diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 3f48a39d4..004ca298d 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -53,6 +53,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const integrationAuth = await server.services.integrationAuth.getIntegrationAuth({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -80,6 +81,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, integration: req.query.integration, projectId: req.query.projectId }); @@ -117,6 +119,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const integrationAuth = await server.services.integrationAuth.deleteIntegrationAuthById({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -157,6 +160,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const integrationAuth = await server.services.integrationAuth.oauthExchange({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body @@ -200,6 +204,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body @@ -247,6 +252,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const apps = await server.services.integrationAuth.getIntegrationApps({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, ...req.query @@ -278,6 +284,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const teams = await server.services.integrationAuth.getIntegrationAuthTeams({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -306,6 +313,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const branches = await server.services.integrationAuth.getVercelBranches({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, appId: req.query.appId @@ -335,6 +343,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const groups = await server.services.integrationAuth.getChecklyGroups({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, accountId: req.query.accountId @@ -343,6 +352,68 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + url: "/:integrationAuthId/github/orgs", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + orgs: z.object({ name: z.string(), orgId: z.string() }).array() + }) + } + }, + handler: async (req) => { + const orgs = await server.services.integrationAuth.getGithubOrgs({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + id: req.params.integrationAuthId + }); + if (!orgs) throw new Error("No organization found."); + + return { orgs }; + } + }); + + server.route({ + url: "/:integrationAuthId/github/envs", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + repoOwner: z.string().trim(), + repoName: z.string().trim() + }), + response: { + 200: z.object({ + envs: z.object({ name: z.string(), envId: z.string() }).array() + }) + } + }, + handler: async (req) => { + const envs = await server.services.integrationAuth.getGithubEnvs({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + actorAuthMethod: req.permission.authMethod, + repoName: req.query.repoName, + repoOwner: req.query.repoOwner + }); + if (!envs) throw new Error("No organization found."); + + return { envs }; + } + }); + server.route({ url: "/:integrationAuthId/qovery/orgs", method: "GET", @@ -361,6 +432,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const orgs = await server.services.integrationAuth.getQoveryOrgs({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -389,6 +461,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const projects = await server.services.integrationAuth.getQoveryProjects({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, orgId: req.query.orgId @@ -418,6 +491,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const environments = await server.services.integrationAuth.getQoveryEnvs({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, projectId: req.query.projectId @@ -447,6 +521,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const apps = await server.services.integrationAuth.getQoveryApps({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, environmentId: req.query.environmentId @@ -476,6 +551,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const containers = await server.services.integrationAuth.getQoveryContainers({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, environmentId: req.query.environmentId @@ -505,6 +581,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const jobs = await server.services.integrationAuth.getQoveryJobs({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, environmentId: req.query.environmentId @@ -537,6 +614,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const pipelines = await server.services.integrationAuth.getHerokuPipelines({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -565,6 +643,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const environments = await server.services.integrationAuth.getRailwayEnvironments({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, appId: req.query.appId @@ -594,6 +673,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const services = await server.services.integrationAuth.getRailwayServices({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, appId: req.query.appId @@ -630,6 +710,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const workspaces = await server.services.integrationAuth.getBitbucketWorkspaces({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId }); @@ -663,6 +744,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const secretGroups = await server.services.integrationAuth.getNorthFlankSecretGroups({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, appId: req.query.appId @@ -697,6 +779,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) const buildConfigs = await server.services.integrationAuth.getTeamcityBuildConfigs({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationAuthId, appId: req.query.appId diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index ed1914ccc..4859ca584 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -33,6 +33,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { secretPrefix: z.string().optional(), secretSuffix: z.string().optional(), initialSyncBehavior: z.string().optional(), + shouldAutoRedeploy: z.boolean().optional(), secretGCPLabel: z .object({ labelName: z.string(), @@ -53,6 +54,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { const { integration, integrationAuth } = await server.services.integration.createIntegration({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); @@ -123,6 +125,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { const integration = await server.services.integration.updateIntegration({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.integrationId, ...req.body @@ -148,6 +151,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const integration = await server.services.integration.deleteIntegration({ actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, id: req.params.integrationId diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 5956b53df..212020034 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -29,6 +29,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { orgId: req.body.organizationId, userId: req.permission.id, inviteeEmail: req.body.inviteeEmail, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index d31682d88..42e64e9d7 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -15,7 +15,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req) => { const organizations = await server.services.org.findAllOrganizationOfUser(req.permission.id); return { organizations }; @@ -40,6 +40,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const organization = await server.services.org.findOrganizationById( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { organization }; @@ -76,6 +77,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const users = await server.services.org.findAllOrgMembers( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { users }; @@ -111,6 +113,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, data: req.body }); @@ -138,6 +141,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const incidentContactsOrg = await req.server.services.org.findIncidentContacts( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { incidentContactsOrg }; @@ -162,6 +166,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.body.email, + req.permission.authMethod, req.permission.orgId ); return { incidentContactsOrg }; @@ -185,6 +190,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.params.incidentContactId, + req.permission.authMethod, req.permission.orgId ); return { incidentContactsOrg }; diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index cb5be173e..68a5a9001 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -39,6 +39,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, projectId: req.params.workspaceId, ...req.body }); @@ -95,6 +96,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { const { environment, old } = await server.services.projectEnv.updateEnvironment({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, id: req.params.id, @@ -153,6 +155,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { const environment = await server.services.projectEnv.deleteEnvironment({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, id: req.params.id diff --git a/backend/src/server/routes/v1/project-key-router.ts b/backend/src/server/routes/v1/project-key-router.ts index b34260117..440d45140 100644 --- a/backend/src/server/routes/v1/project-key-router.ts +++ b/backend/src/server/routes/v1/project-key-router.ts @@ -30,6 +30,7 @@ export const registerProjectKeyRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, nonce: req.body.key.nonce, receiverId: req.body.key.userId, diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index aece95a5d..9e2f5bd22 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -66,6 +66,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const memberships = await server.services.projectMembership.getProjectMemberships({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId }); @@ -102,6 +103,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const data = await server.services.projectMembership.addUsersToProject({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, members: req.body.members @@ -170,6 +172,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const roles = await server.services.projectMembership.updateProjectMembership({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, membershipId: req.params.membershipId, @@ -219,6 +222,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const membership = await server.services.projectMembership.deleteProjectMembership({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, membershipId: req.params.membershipId diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 3ffedf98d..55f1fdda9 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -10,6 +10,7 @@ import { import { PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectFilterType } from "@app/services/project/project-types"; import { integrationAuthPubSchema } from "../sanitizedSchemas"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; @@ -45,6 +46,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const publicKeys = await server.services.projectKey.getProjectPublicKeys({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId }); @@ -97,6 +99,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const users = await server.services.projectMembership.getProjectMemberships({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, projectId: req.params.workspaceId, actorOrgId: req.permission.orgId }); @@ -137,37 +140,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.getAProject({ + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId - }); - return { workspace }; - } - }); - - server.route({ - url: "/", - method: "POST", - schema: { - body: z.object({ - workspaceName: z.string().trim(), - organizationId: z.string().trim() - }), - response: { - 200: z.object({ - workspace: projectWithEnv - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const workspace = await server.services.project.createProject({ - actorId: req.permission.id, - actor: req.permission.type, - orgId: req.body.organizationId, - actorOrgId: req.permission.orgId, - workspaceName: req.body.workspaceName + actorOrgId: req.permission.orgId }); return { workspace }; } @@ -189,10 +169,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.deleteProject({ + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + actorOrgId: req.permission.orgId }); return { workspace }; } @@ -220,6 +204,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const workspace = await server.services.project.updateName({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, name: req.body.name @@ -253,17 +238,21 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.updateProject({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + filter: { + type: ProjectFilterType.ID, + projectId: req.params.workspaceId + }, update: { name: req.body.name, autoCapitalization: req.body.autoCapitalization - } + }, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId }); return { workspace @@ -293,6 +282,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const workspace = await server.services.project.toggleAutoCapitalization({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, autoCapitalization: req.body.autoCapitalization @@ -329,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const integrations = await server.services.integration.listIntegrationByProject({ actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId @@ -354,6 +345,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({ actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId @@ -379,6 +371,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const serviceTokenData = await server.services.serviceToken.getProjectServiceTokens({ actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index dee075943..bd202b70b 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -39,6 +39,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => const folder = await server.services.folder.createFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, @@ -96,6 +97,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => const { folder, old } = await server.services.folder.updateFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, @@ -154,6 +156,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => const folder = await server.services.folder.deleteFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, @@ -207,6 +210,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => const folders = await server.services.folder.getFolders({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, projectId: req.query.workspaceId, diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts index 823e7dbee..bd47a7424 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v1/secret-import-router.ts @@ -44,6 +44,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => const secretImport = await server.services.secretImport.createImport({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, @@ -114,6 +115,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => const secretImport = await server.services.secretImport.updateImport({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.secretImportId, ...req.body, @@ -175,6 +177,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => const secretImport = await server.services.secretImport.deleteImport({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.secretImportId, ...req.body, @@ -234,6 +237,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => const secretImports = await server.services.secretImport.getImports({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, projectId: req.query.workspaceId @@ -287,6 +291,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => const importedSecrets = await server.services.secretImport.getSecretsFromImports({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, projectId: req.query.workspaceId diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 7ca3e4893..c60f2b9ba 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -23,6 +23,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { const workspaceTags = await server.services.secretTag.getProjectTags({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.projectId }); @@ -53,6 +54,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { const workspaceTag = await server.services.secretTag.createTag({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.projectId, ...req.body @@ -80,6 +82,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { const workspaceTag = await server.services.secretTag.deleteTag({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.tagId }); diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index ca5148659..031bcc941 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -15,7 +15,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req) => { const user = await server.services.user.getMe(req.permission.id); return { user }; diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts index 9a20a5d22..2a5ab49fb 100644 --- a/backend/src/server/routes/v1/webhook-router.ts +++ b/backend/src/server/routes/v1/webhook-router.ts @@ -47,6 +47,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { const webhook = await server.services.webhook.createWebhook({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body @@ -93,6 +94,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { const webhook = await server.services.webhook.updateWebhook({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.webhookId, isDisabled: req.body.isDisabled @@ -130,6 +132,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { const webhook = await server.services.webhook.deleteWebhook({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.webhookId }); @@ -172,6 +175,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { const webhook = await server.services.webhook.testWebhook({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.webhookId }); @@ -204,6 +208,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { const webhooks = await server.services.webhook.listWebhooks({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query, projectId: req.query.workspaceId diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 97477b033..440130e13 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -42,6 +42,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { const identityMemberships = await server.services.identity.listOrgIdentities({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, orgId: req.params.orgId }); diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index 67dccb5e3..d170f87e6 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -35,6 +35,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) const identityMembership = await server.services.identityProject.createProjectIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, projectId: req.params.projectId, @@ -89,6 +90,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) const roles = await server.services.identityProject.updateProjectIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, projectId: req.params.projectId, @@ -123,6 +125,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) const identityMembership = await server.services.identityProject.deleteProjectIdentity({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, projectId: req.params.projectId @@ -177,6 +180,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) const identityMemberships = await server.services.identityProject.listProjectIdentities({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.projectId }); diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 2c9465aa8..d8e46d0da 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -68,11 +68,14 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { }, handler: async (req, res) => { const userAgent = req.headers["user-agent"]; + const mfaJwtToken = req.headers.authorization?.replace("Bearer ", ""); if (!userAgent) throw new Error("user agent header is required"); + if (!mfaJwtToken) throw new Error("authorization header is required"); const appCfg = getConfig(); const { user, token } = await server.services.login.verifyMfaToken({ userAgent, + mfaJwtToken, ip: req.realIp, userId: req.mfa.userId, orgId: req.mfa.orgId, diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 7d5ba3da7..83ab6792e 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -45,6 +45,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const users = await server.services.org.findAllOrgMembers( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { users }; @@ -89,6 +90,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId }); @@ -127,6 +129,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const membership = await server.services.org.updateOrgMembership({ userId: req.permission.id, role: req.body.role, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, membershipId: req.params.membershipId, actorOrgId: req.permission.orgId @@ -162,6 +165,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const membership = await server.services.org.deleteOrgMembership({ userId: req.permission.id, + actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, membershipId: req.params.membershipId, actorOrgId: req.permission.orgId @@ -183,7 +187,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY], { requireOrg: false }), handler: async (req) => { if (req.auth.actor !== ActorType.USER) return; @@ -217,6 +221,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const organization = await server.services.org.deleteOrganizationById( req.permission.id, req.params.organizationId, + req.permission.authMethod, req.permission.orgId ); return { organization }; diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 6f81f8392..51bae6d2b 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -28,7 +28,9 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider handler: async (req) => { const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({ projectId: req.params.projectId, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, + actorOrgId: req.permission.orgId, actor: req.permission.type, emails: req.body.emails, usernames: req.body.usernames @@ -74,6 +76,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider const memberships = await server.services.projectMembership.deleteProjectMemberships({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.projectId, emails: req.body.emails, diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 208edba7b..3258be50a 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -8,6 +8,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectFilterType } from "@app/services/project/project-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; const projectWithEnv = ProjectsSchema.merge( @@ -17,6 +18,14 @@ const projectWithEnv = ProjectsSchema.merge( }) ); +const slugSchema = z + .string() + .min(5) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Slug must be at least 5 character but no more than 36" + }); + export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Get project key */ server.route({ @@ -47,6 +56,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const key = await server.services.projectKey.getLatestProjectKey({ actor: req.permission.type, actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId }); @@ -74,7 +84,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { params: z.object({ projectId: z.string().trim() }), - body: z.object({ userPrivateKey: z.string().trim() }), @@ -82,11 +91,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.void() } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { await server.services.project.upgradeProject({ actorId: req.permission.id, + actorOrgId: req.permission.orgId, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, projectId: req.params.projectId, userPrivateKey: req.body.userPrivateKey }); @@ -107,9 +118,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { const status = await server.services.project.getProjectUpgradeStatus({ + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, projectId: req.params.projectId, actor: req.permission.type, actorId: req.permission.id @@ -137,8 +150,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { message: "Slug must be a valid slug" }) .optional() - .describe(PROJECTS.CREATE.slug), - organizationId: z.string().trim().describe(PROJECTS.CREATE.organizationId) + .describe(PROJECTS.CREATE.slug) }), response: { 200: z.object({ @@ -146,12 +158,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const project = await server.services.project.createProject({ actorId: req.permission.id, actor: req.permission.type, - orgId: req.body.organizationId, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, workspaceName: req.body.projectName, slug: req.body.slug }); @@ -160,7 +173,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { event: PostHogEventTypes.ProjectCreated, distinctId: getTelemetryDistinctId(req), properties: { - orgId: req.body.organizationId, + orgId: project.orgId, name: project.name, ...req.auditLogInfo } @@ -169,4 +182,104 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { project }; } }); + + /* Delete a project by slug */ + server.route({ + method: "DELETE", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to delete.") + }), + response: { + 200: ProjectsSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + + handler: async (req) => { + const project = await server.services.project.deleteProject({ + filter: { + type: ProjectFilterType.SLUG, + slug: req.params.slug, + orgId: req.permission.orgId + }, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + actor: req.permission.type + }); + + return project; + } + }); + + /* Get a project by slug */ + server.route({ + method: "GET", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to get.") + }), + response: { + 200: projectWithEnv + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.getAProject({ + filter: { + slug: req.params.slug, + orgId: req.permission.orgId, + type: ProjectFilterType.SLUG + }, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type + }); + + return project; + } + }); + + /* Update a project by slug */ + server.route({ + method: "PATCH", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to update.") + }), + body: z.object({ + name: z.string().trim().optional().describe("The new name of the project."), + autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.") + }), + response: { + 200: ProjectsSchema + } + }, + + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.updateProject({ + filter: { + type: ProjectFilterType.SLUG, + slug: req.params.slug, + orgId: req.permission.orgId + }, + update: { + name: req.body.name, + autoCapitalization: req.body.autoCapitalization + }, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + return project; + } + }); }; diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts index 2b6445dea..c4d08d104 100644 --- a/backend/src/server/routes/v2/service-token-router.ts +++ b/backend/src/server/routes/v2/service-token-router.ts @@ -46,6 +46,8 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => handler: async (req) => { const { serviceToken, user } = await server.services.serviceToken.getServiceToken({ actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, actor: req.permission.type }); @@ -98,6 +100,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => const { serviceToken, token } = await server.services.serviceToken.createServiceToken({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId @@ -136,6 +139,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) => const serviceTokenData = await server.services.serviceToken.deleteServiceToken({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.serviceTokenId }); diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 97bc3d864..12c9b703d 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -60,7 +60,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }) } }, - preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY], { requireOrg: false }), handler: async (req) => { const user = await server.services.user.updateAuthMethods(req.permission.id, req.body.authMethods); return { user }; diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 240aa21b1..900ad56d2 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -34,6 +34,42 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/select-organization", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + token: z.string() + }) + } + }, + handler: async (req, res) => { + const cfg = getConfig(); + const tokens = await server.services.login.selectOrganization({ + userAgent: req.headers["user-agent"], + authJwtToken: req.headers.authorization, + organizationId: req.body.organizationId, + ipAddress: req.realIp + }); + + void res.setCookie("jid", tokens.refresh, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: cfg.HTTPS_ENABLED + }); + + return { token: tokens.access }; + } + }); + server.route({ method: "POST", url: "/login2", diff --git a/backend/src/server/routes/v3/secret-blind-index-router.ts b/backend/src/server/routes/v3/secret-blind-index-router.ts index 94e6cab83..664eaa2c9 100644 --- a/backend/src/server/routes/v3/secret-blind-index-router.ts +++ b/backend/src/server/routes/v3/secret-blind-index-router.ts @@ -20,6 +20,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) handler: async (req) => { const count = await server.services.secretBlindIndex.getSecretBlindIndexStatus({ projectId: req.params.projectId, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId @@ -52,6 +53,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) handler: async (req) => { const secrets = await server.services.secretBlindIndex.getProjectSecrets({ projectId: req.params.projectId, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId @@ -86,6 +88,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) await server.services.secretBlindIndex.updateProjectSecretName({ projectId: req.params.projectId, secretsToUpdate: req.body.secretsToUpdate, + actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 65219d0ab..1e224aa3b 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -17,6 +17,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ProjectFilterType } from "@app/services/project/project-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { secretRawSchema } from "../sanitizedSchemas"; @@ -35,6 +36,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], querystring: z.object({ workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), + workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), include_imports: z @@ -70,6 +72,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment = scope[0].environment; workspaceId = req.auth.serviceToken.projectId; } + } else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) { + const workspace = await server.services.project.getAProject({ + filter: { + type: ProjectFilterType.SLUG, + orgId: req.permission.orgId, + slug: req.query.workspaceSlug + }, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId + }); + + if (!workspace) throw new BadRequestError({ message: `No project found with slug ${req.query.workspaceSlug}` }); + + workspaceId = workspace.id; } if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); @@ -79,13 +97,14 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, environment, + actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, includeImports: req.query.include_imports }); await server.services.auditLog.createAuditLog({ - projectId: req.query.workspaceId, + projectId: workspaceId, ...req.auditLogInfo, event: { type: EventType.GET_SECRETS, @@ -163,6 +182,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.getSecretByNameRaw({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, environment, projectId: workspaceId, @@ -248,6 +268,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, environment: req.body.environment, + actorAuthMethod: req.permission.authMethod, projectId: req.body.workspaceId, secretPath: req.body.secretPath, secretName: req.params.secretName, @@ -331,6 +352,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, environment: req.body.environment, projectId: req.body.workspaceId, secretPath: req.body.secretPath, @@ -407,6 +429,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.deleteSecretRaw({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, environment: req.body.environment, projectId: req.body.workspaceId, @@ -502,6 +525,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const { secrets, imports } = await server.services.secret.getSecrets({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, environment: req.query.environment, projectId: req.query.workspaceId, @@ -588,6 +612,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.getSecretByName({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, environment: req.query.environment, projectId: req.query.workspaceId, @@ -690,6 +715,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { if (req.body.type !== SecretType.Personal && req.permission.type === ActorType.USER) { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, actor: req.permission.type, secretPath, environment, @@ -699,6 +726,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -742,6 +770,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.createSecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: secretPath, type, @@ -866,6 +895,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -875,6 +905,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -920,6 +951,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.updateSecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: secretPath, type, @@ -1010,6 +1042,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1019,6 +1052,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1052,6 +1086,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secret = await server.services.secret.deleteSecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: secretPath, type, @@ -1134,6 +1169,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1143,6 +1179,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1172,6 +1209,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.createManySecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: secretPath, environment, @@ -1255,6 +1293,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1264,6 +1303,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1292,6 +1332,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.updateManySecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: secretPath, environment, @@ -1364,6 +1405,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1373,6 +1415,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, secretPath, environment, @@ -1400,6 +1443,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { const secrets = await server.services.secret.deleteManySecret({ actorId: req.permission.id, actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, path: req.body.secretPath, environment, diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 17787be84..ac43df36d 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -108,7 +108,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { 200: z.object({ message: z.string(), user: UsersSchema, - token: z.string() + token: z.string(), + organizationId: z.string().nullish() }) } }, @@ -124,12 +125,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { }); } - const { user, accessToken, refreshToken } = await server.services.signup.completeEmailAccountSignup({ - ...req.body, - ip: req.realIp, - userAgent, - authorization: req.headers.authorization as string - }); + const { user, accessToken, refreshToken, organizationId } = + await server.services.signup.completeEmailAccountSignup({ + ...req.body, + ip: req.realIp, + userAgent, + authorization: req.headers.authorization as string + }); if (user.email) { void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); @@ -152,7 +154,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { secure: appCfg.HTTPS_ENABLED }); - return { message: "Successfully set up account", user, token: accessToken }; + return { message: "Successfully set up account", user, token: accessToken, organizationId }; } }); diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index 0b78ab438..80fb0b325 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri if (decodedToken.username !== username) throw new Error("Invalid auth credentials"); if (decodedToken.organizationId) { - return { orgId: decodedToken.organizationId }; + return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod }; } - return {}; + return { authMethod: decodedToken.authMethod, orgId: null }; }; export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 786e69d73..e1b10530d 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,13 +1,16 @@ import jwt from "jsonwebtoken"; import { TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { TTokenDALFactory } from "../auth-token/auth-token-dal"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TOrgDALFactory } from "../org/org-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { validateProviderAuthToken } from "./auth-fns"; @@ -17,16 +20,24 @@ import { TOauthLoginDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; -import { AuthMethod, AuthTokenType } from "./auth-type"; +import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; type TAuthLoginServiceFactoryDep = { userDAL: TUserDALFactory; + orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; + tokenDAL: TTokenDALFactory; }; export type TAuthLoginFactory = ReturnType; -export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: TAuthLoginServiceFactoryDep) => { +export const authLoginServiceFactory = ({ + userDAL, + tokenService, + smtpService, + orgDAL, + tokenDAL +}: TAuthLoginServiceFactoryDep) => { /* * Private * Not exported. This is to update user device list @@ -83,12 +94,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: user, ip, userAgent, - organizationId + organizationId, + authMethod }: { user: TUsers; ip: string; userAgent: string; - organizationId?: string; + organizationId: string | undefined; + authMethod: AuthMethod; }) => { const cfg = getConfig(); await updateUserDeviceSession(user, ip, userAgent); @@ -98,8 +111,10 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: userId: user.id }); if (!tokenSession) throw new Error("Failed to create token"); + const accessToken = jwt.sign( { + authMethod, authTokenType: AuthTokenType.ACCESS_TOKEN, userId: user.id, tokenVersionId: tokenSession.id, @@ -112,6 +127,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: const refreshToken = jwt.sign( { + authMethod, authTokenType: AuthTokenType.REFRESH_TOKEN, userId: user.id, tokenVersionId: tokenSession.id, @@ -158,9 +174,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: const loginExchangeClientProof = async ({ email, clientProof, - providerAuthToken, ip, - userAgent + userAgent, + providerAuthToken }: TLoginClientProofDTO) => { const userEnc = await userDAL.findUserEncKeyByUsername({ username: email @@ -168,14 +184,16 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: if (!userEnc) throw new Error("Failed to find user"); const cfg = getConfig(); - let organizationId; - if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { - const { orgId } = validateProviderAuthToken(providerAuthToken as string, email); - organizationId = orgId; - } else if (providerAuthToken) { - // SAML SSO - const { orgId } = validateProviderAuthToken(providerAuthToken, email); - organizationId = orgId; + let authMethod = AuthMethod.EMAIL; + let organizationId: string | undefined; + + if (providerAuthToken) { + const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); + + authMethod = decodedProviderToken.authMethod; + if (isAuthMethodSaml(authMethod) && decodedProviderToken.orgId) { + organizationId = decodedProviderToken.orgId; + } } if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); @@ -196,9 +214,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: if (userEnc.isMfaEnabled && userEnc.email) { const mfaToken = jwt.sign( { + authMethod, authTokenType: AuthTokenType.MFA_TOKEN, - userId: userEnc.userId, - organizationId + userId: userEnc.userId }, cfg.AUTH_SECRET, { @@ -221,12 +239,60 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: }, ip, userAgent, + authMethod, organizationId }); return { token, isMfaEnabled: false, user: userEnc } as const; }; + const selectOrganization = async ({ + userAgent, + authJwtToken, + ipAddress, + organizationId + }: { + userAgent: string | undefined; + authJwtToken: string | undefined; + ipAddress: string; + organizationId: string; + }) => { + const cfg = getConfig(); + + if (!authJwtToken) throw new UnauthorizedError({ name: "Authorization header is required" }); + if (!userAgent) throw new UnauthorizedError({ name: "user agent header is required" }); + + // eslint-disable-next-line no-param-reassign + authJwtToken = authJwtToken.replace("Bearer ", ""); // remove bearer from token + + // The decoded JWT token, which contains the auth method. + const decodedToken = jwt.verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload; + if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" }); + + const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId); + if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" }); + + // Check if the user actually has access to the specified organization. + const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId); + + if (!hasOrganizationMembership) { + throw new UnauthorizedError({ message: "User does not have access to the organization" }); + } + + await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId); + + const tokens = await generateUserTokens({ + authMethod: decodedToken.authMethod, + user, + userAgent, + ip: ipAddress, + organizationId + }); + + return tokens; + }; + /* * Multi factor authentication re-send code, Get user id from token * saved in frontend @@ -244,12 +310,15 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: * Multi factor authentication verification of code * Third step of login in which user completes with mfa * */ - const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => { + const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => { await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_MFA, userId, code: mfaToken }); + + const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload; + const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc) throw new Error("Failed to authenticate user"); @@ -260,7 +329,8 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: }, ip, userAgent, - organizationId: orgId + organizationId: orgId, + authMethod: decodedToken.authMethod }); return { token, user: userEnc }; @@ -339,6 +409,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: oauth2Login, resendMfaToken, verifyMfaToken, + selectOrganization, generateUserTokens }; }; diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 86af5a5f9..37b90f548 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -17,6 +17,7 @@ export type TLoginClientProofDTO = { export type TVerifyMfaTokenDTO = { userId: string; mfaToken: string; + mfaJwtToken: string; ip: string; userAgent: string; orgId?: string; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 39bfec8b1..3db935769 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -150,11 +150,15 @@ export const authSignupServiceFactory = ({ }); if (!organizationId) { - await orgService.createOrganization({ + const newOrganization = await orgService.createOrganization({ userId: user.id, userEmail: user.email ?? user.username, orgName: organizationName }); + + if (!newOrganization) throw new Error("Failed to create organization"); + + organizationId = newOrganization.id; } const updatedMembersips = await orgDAL.updateMembership( @@ -174,6 +178,7 @@ export const authSignupServiceFactory = ({ const accessToken = jwt.sign( { + authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.ACCESS_TOKEN, userId: updateduser.info.id, tokenVersionId: tokenSession.id, @@ -186,6 +191,7 @@ export const authSignupServiceFactory = ({ const refreshToken = jwt.sign( { + authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.REFRESH_TOKEN, userId: updateduser.info.id, tokenVersionId: tokenSession.id, @@ -196,7 +202,7 @@ export const authSignupServiceFactory = ({ { expiresIn: appCfg.JWT_REFRESH_LIFETIME } ); - return { user: updateduser.info, accessToken, refreshToken }; + return { user: updateduser.info, accessToken, refreshToken, organizationId }; }; /* @@ -277,6 +283,7 @@ export const authSignupServiceFactory = ({ const accessToken = jwt.sign( { + authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.ACCESS_TOKEN, userId: updateduser.info.id, tokenVersionId: tokenSession.id, @@ -288,6 +295,7 @@ export const authSignupServiceFactory = ({ const refreshToken = jwt.sign( { + authMethod: AuthMethod.EMAIL, authTokenType: AuthTokenType.REFRESH_TOKEN, userId: updateduser.info.id, tokenVersionId: tokenSession.id, diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 57c86158f..a3c53658c 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -6,6 +6,7 @@ export enum AuthMethod { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", + GOOGLE_SAML = "google-saml", LDAP = "ldap" } @@ -38,8 +39,12 @@ export enum ActorType { // would extend to AWS, Azure, ... SCIM_CLIENT = "scimClient" } +// This will be null unless the token-type is JWT +export type ActorAuthMethod = AuthMethod | null; + export type AuthModeJwtTokenPayload = { authTokenType: AuthTokenType.ACCESS_TOKEN; + authMethod: AuthMethod; userId: string; tokenVersionId: string; accessVersion: number; @@ -48,12 +53,15 @@ export type AuthModeJwtTokenPayload = { export type AuthModeMfaJwtTokenPayload = { authTokenType: AuthTokenType.MFA_TOKEN; + authMethod: AuthMethod; userId: string; organizationId?: string; }; export type AuthModeRefreshJwtTokenPayload = { + // authMode authTokenType: AuthTokenType.REFRESH_TOKEN; + authMethod: AuthMethod; userId: string; tokenVersionId: string; refreshVersion: number; @@ -63,6 +71,8 @@ export type AuthModeRefreshJwtTokenPayload = { export type AuthModeProviderJwtTokenPayload = { authTokenType: AuthTokenType.PROVIDER_TOKEN; username: string; + authMethod: AuthMethod; + email: string; organizationId?: string; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 32774ccbb..4b53c8174 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -6,17 +6,20 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; + identityOrgMembershipDAL: TIdentityOrgDALFactory; }; export type TIdentityAccessTokenServiceFactory = ReturnType; export const identityAccessTokenServiceFactory = ({ - identityAccessTokenDAL + identityAccessTokenDAL, + identityOrgMembershipDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => { const { @@ -117,8 +120,16 @@ export const identityAccessTokenServiceFactory = ({ }); } + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ + identityId: identityAccessToken.identityId + }); + + if (!identityOrgMembership) { + throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); + } + validateAccessTokenExp(identityAccessToken); - return identityAccessToken; + return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; return { renewAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index b6f6e4343..d554c81ac 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -49,10 +49,17 @@ export const identityProjectServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, projectId, role }: TCreateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); @@ -112,9 +119,16 @@ export const identityProjectServiceFactory = ({ roles, actor, actorId, + actorAuthMethod, actorOrgId }: TUpdateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); @@ -127,6 +141,7 @@ export const identityProjectServiceFactory = ({ ActorType.IDENTITY, projectIdentity.identityId, projectIdentity.projectId, + actorAuthMethod, actorOrgId ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); @@ -185,6 +200,7 @@ export const identityProjectServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, projectId }: TDeleteProjectIdentityDTO) => { const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); @@ -195,6 +211,7 @@ export const identityProjectServiceFactory = ({ actor, actorId, identityProjectMembership.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); @@ -202,6 +219,7 @@ export const identityProjectServiceFactory = ({ ActorType.IDENTITY, identityId, identityProjectMembership.projectId, + actorAuthMethod, actorOrgId ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); @@ -212,8 +230,20 @@ export const identityProjectServiceFactory = ({ return deletedIdentity; }; - const listProjectIdentities = async ({ projectId, actor, actorId, actorOrgId }: TListProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listProjectIdentities = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TListProjectIdentityDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); const identityMemberhips = await identityProjectDAL.findByProjectId(projectId); diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index d375a8fa5..54a074073 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -144,6 +144,7 @@ export const identityUaServiceFactory = ({ accessTokenTrustedIps, clientSecretTrustedIps, actorId, + actorAuthMethod, actor, actorOrgId }: TAttachUaDTO) => { @@ -162,6 +163,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); @@ -233,6 +235,7 @@ export const identityUaServiceFactory = ({ accessTokenTrustedIps, clientSecretTrustedIps, actorId, + actorAuthMethod, actor, actorOrgId }: TUpdateUaDTO) => { @@ -256,6 +259,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); @@ -308,7 +312,7 @@ export const identityUaServiceFactory = ({ return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; }; - const getIdentityUa = async ({ identityId, actorId, actor, actorOrgId }: TGetUaDTO) => { + const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) @@ -322,6 +326,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); @@ -334,6 +339,7 @@ export const identityUaServiceFactory = ({ actorOrgId, identityId, ttl, + actorAuthMethod, description, numUsesLimit }: TCreateUaClientSecretDTO) => { @@ -347,6 +353,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); @@ -355,6 +362,7 @@ export const identityUaServiceFactory = ({ ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); @@ -388,7 +396,13 @@ export const identityUaServiceFactory = ({ }; }; - const getUaClientSecrets = async ({ actor, actorId, actorOrgId, identityId }: TGetUaClientSecretsDTO) => { + const getUaClientSecrets = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + identityId + }: TGetUaClientSecretsDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) @@ -399,6 +413,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); @@ -407,6 +422,7 @@ export const identityUaServiceFactory = ({ ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); @@ -431,6 +447,7 @@ export const identityUaServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, clientSecretId }: TRevokeUaClientSecretDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); @@ -443,6 +460,7 @@ export const identityUaServiceFactory = ({ actor, actorId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); @@ -451,6 +469,7 @@ export const identityUaServiceFactory = ({ ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, + actorAuthMethod, actorOrgId ); const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index e37a3a6dd..cdea45631 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -25,8 +25,16 @@ export const identityServiceFactory = ({ identityOrgMembershipDAL, permissionService }: TIdentityServiceFactoryDep) => { - const createIdentity = async ({ name, role, actor, orgId, actorId, actorOrgId }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const createIdentity = async ({ + name, + role, + actor, + orgId, + actorId, + actorAuthMethod, + actorOrgId + }: TCreateIdentityDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( @@ -54,7 +62,15 @@ export const identityServiceFactory = ({ return identity; }; - const updateIdentity = async ({ id, role, name, actor, actorId, actorOrgId }: TUpdateIdentityDTO) => { + const updateIdentity = async ({ + id, + role, + name, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TUpdateIdentityDTO) => { const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` }); @@ -62,6 +78,7 @@ export const identityServiceFactory = ({ actor, actorId, identityOrgMembership.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); @@ -70,6 +87,7 @@ export const identityServiceFactory = ({ ActorType.IDENTITY, id, identityOrgMembership.orgId, + actorAuthMethod, actorOrgId ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); @@ -108,7 +126,7 @@ export const identityServiceFactory = ({ return { ...identity, orgId: identityOrgMembership.orgId }; }; - const deleteIdentity = async ({ actorId, actor, actorOrgId, id }: TDeleteIdentityDTO) => { + const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => { const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` }); @@ -116,13 +134,16 @@ export const identityServiceFactory = ({ actor, actorId, identityOrgMembership.orgId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); const { permission: identityRolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, id, - identityOrgMembership.orgId + identityOrgMembership.orgId, + actorAuthMethod, + actorOrgId ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); if (!hasRequiredPriviledges) @@ -132,8 +153,8 @@ export const identityServiceFactory = ({ return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; }; - const listOrgIdentities = async ({ orgId, actor, actorId, actorOrgId }: TOrgPermission) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId); diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 4cd0bc159..c8551df7d 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { Octokit } from "@octokit/rest"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -24,6 +25,8 @@ import { TIntegrationAuthAppsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, + TIntegrationAuthGithubEnvsDTO, + TIntegrationAuthGithubOrgsDTO, TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthQoveryEnvironmentsDTO, @@ -61,14 +64,26 @@ export const integrationAuthServiceFactory = ({ projectBotDAL, projectBotService }: TIntegrationAuthServiceFactoryDep) => { - const listIntegrationAuthByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listIntegrationAuthByProjectId = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const authorizations = await integrationAuthDAL.find({ projectId }); return authorizations; }; - const getIntegrationAuth = async ({ actor, id, actorId, actorOrgId }: TGetIntegrationAuthDTO) => { + const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -76,6 +91,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -87,6 +103,7 @@ export const integrationAuthServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, integration, url, code @@ -94,7 +111,13 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const bot = await projectBotDAL.findOne({ isActive: true, projectId }); @@ -150,6 +173,7 @@ export const integrationAuthServiceFactory = ({ url, actor, actorOrgId, + actorAuthMethod, accessId, namespace, accessToken @@ -157,7 +181,13 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const bot = await projectBotDAL.findOne({ isActive: true, projectId }); @@ -274,6 +304,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, teamId, id, workspaceSlug @@ -285,6 +316,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -302,7 +334,13 @@ export const integrationAuthServiceFactory = ({ return apps; }; - const getIntegrationAuthTeams = async ({ actor, actorId, actorOrgId, id }: TIntegrationAuthTeamsDTO) => { + const getIntegrationAuthTeams = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + id + }: TIntegrationAuthTeamsDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -310,6 +348,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -324,7 +363,14 @@ export const integrationAuthServiceFactory = ({ return teams; }; - const getVercelBranches = async ({ appId, id, actor, actorId, actorOrgId }: TIntegrationAuthVercelBranchesDTO) => { + const getVercelBranches = async ({ + appId, + id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIntegrationAuthVercelBranchesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -332,6 +378,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -357,7 +404,14 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getChecklyGroups = async ({ actorId, actor, actorOrgId, id, accountId }: TIntegrationAuthChecklyGroupsDTO) => { + const getChecklyGroups = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + accountId + }: TIntegrationAuthChecklyGroupsDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -365,6 +419,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -383,7 +438,7 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => { + const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -391,6 +446,76 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const botKey = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); + + const octokit = new Octokit({ + auth: accessToken + }); + + const { data } = await octokit.request("GET /user/orgs", { + headers: { + "X-GitHub-Api-Version": "2022-11-28" + } + }); + if (!data) return []; + + return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) })); + }; + + const getGithubEnvs = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + repoOwner, + repoName + }: TIntegrationAuthGithubEnvsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const botKey = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); + + const octokit = new Octokit({ + auth: accessToken + }); + + const { + data: { environments } + } = await octokit.request("GET /repos/{owner}/{repo}/environments", { + headers: { + "X-GitHub-Api-Version": "2022-11-28" + }, + owner: repoOwner, + repo: repoName + }); + if (!environments) return []; + return environments.map(({ id: envId, name }) => ({ name, envId: String(envId) })); + }; + + const getQoveryOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthQoveryOrgsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -409,7 +534,14 @@ export const integrationAuthServiceFactory = ({ return data.results.map(({ name, id: orgId }) => ({ name, orgId })); }; - const getQoveryProjects = async ({ actorId, actor, actorOrgId, id, orgId }: TIntegrationAuthQoveryProjectDTO) => { + const getQoveryProjects = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + orgId + }: TIntegrationAuthQoveryProjectDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -417,6 +549,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -442,6 +575,7 @@ export const integrationAuthServiceFactory = ({ id, actor, actorId, + actorAuthMethod, actorOrgId }: TIntegrationAuthQoveryEnvironmentsDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); @@ -451,6 +585,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -476,7 +611,14 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getQoveryApps = async ({ id, actor, actorId, actorOrgId, environmentId }: TIntegrationAuthQoveryScopesDTO) => { + const getQoveryApps = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod, + environmentId + }: TIntegrationAuthQoveryScopesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -484,6 +626,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -513,6 +656,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environmentId }: TIntegrationAuthQoveryScopesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); @@ -522,6 +666,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -546,7 +691,14 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getQoveryJobs = async ({ id, actor, actorId, actorOrgId, environmentId }: TIntegrationAuthQoveryScopesDTO) => { + const getQoveryJobs = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod, + environmentId + }: TIntegrationAuthQoveryScopesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -554,6 +706,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -578,7 +731,13 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getHerokuPipelines = async ({ id, actor, actorId, actorOrgId }: TIntegrationAuthHerokuPipelinesDTO) => { + const getHerokuPipelines = async ({ + id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TIntegrationAuthHerokuPipelinesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -586,6 +745,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -610,7 +770,14 @@ export const integrationAuthServiceFactory = ({ })); }; - const getRailwayEnvironments = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayEnvDTO) => { + const getRailwayEnvironments = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod, + appId + }: TIntegrationAuthRailwayEnvDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -618,6 +785,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -670,7 +838,14 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getRailwayServices = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayServicesDTO) => { + const getRailwayServices = async ({ + id, + actor, + actorId, + actorOrgId, + actorAuthMethod, + appId + }: TIntegrationAuthRailwayServicesDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -678,6 +853,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -737,7 +913,13 @@ export const integrationAuthServiceFactory = ({ return []; }; - const getBitbucketWorkspaces = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthBitbucketWorkspaceDTO) => { + const getBitbucketWorkspaces = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthBitbucketWorkspaceDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -745,6 +927,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -756,9 +939,7 @@ export const integrationAuthServiceFactory = ({ while (hasNextPage) { // eslint-disable-next-line - const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get( - workspaceUrl, - { + const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(workspaceUrl, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" @@ -785,6 +966,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, appId }: TIntegrationAuthNorthflankSecretGroupDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); @@ -794,6 +976,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -851,6 +1034,7 @@ export const integrationAuthServiceFactory = ({ id, actorId, actorOrgId, + actorAuthMethod, actor }: TGetIntegrationAuthTeamCityBuildConfigDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); @@ -860,6 +1044,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); @@ -891,16 +1076,29 @@ export const integrationAuthServiceFactory = ({ integration, actor, actorId, + actorAuthMethod, actorOrgId }: TDeleteIntegrationAuthsDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); const integrations = await integrationAuthDAL.delete({ integration, projectId }); return integrations; }; - const deleteIntegrationAuthById = async ({ id, actorId, actor, actorOrgId }: TDeleteIntegrationAuthByIdDTO) => { + const deleteIntegrationAuthById = async ({ + id, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TDeleteIntegrationAuthByIdDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -908,6 +1106,7 @@ export const integrationAuthServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); @@ -934,6 +1133,8 @@ export const integrationAuthServiceFactory = ({ getIntegrationApps, getVercelBranches, getApps, + getGithubOrgs, + getGithubEnvs, getChecklyGroups, getQoveryApps, getQoveryEnvs, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index e3dbc8341..0c8671fbf 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -44,6 +44,16 @@ export type TIntegrationAuthChecklyGroupsDTO = { accountId: string; } & Omit; +export type TIntegrationAuthGithubOrgsDTO = { + id: string; +} & Omit; + +export type TIntegrationAuthGithubEnvsDTO = { + id: string; + repoName: string; + repoOwner: string; +} & Omit; + export type TIntegrationAuthQoveryOrgsDTO = { id: string; } & Omit; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 0988aa920..3273b4b2d 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1110,98 +1110,176 @@ const syncSecretsGitHub = async ({ interface GitHubRepoKey { key_id: string; key: string; + id?: number | undefined; + url?: string | undefined; + title?: string | undefined; + created_at?: string | undefined; } interface GitHubSecret { name: string; created_at: string; updated_at: string; - } - - interface GitHubSecretRes { - [index: string]: GitHubSecret; + visibility?: "all" | "private" | "selected"; + selected_repositories_url?: string | undefined; } const octokit = new Octokit({ auth: accessToken }); - // const user = (await octokit.request('GET /user', {})).data; - const repoPublicKey: GitHubRepoKey = ( - await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { - owner: integration.owner as string, - repo: integration.app as string - }) - ).data; + enum GithubScope { + Repo = "github-repo", + Org = "github-org", + Env = "github-env" + } + + let repoPublicKey: GitHubRepoKey; + + switch (integration.scope) { + case GithubScope.Org: { + const { data } = await octokit.request("GET /orgs/{org}/actions/secrets/public-key", { + org: integration.owner as string + }); + repoPublicKey = data; + break; + } + case GithubScope.Env: { + const { data } = await octokit.request( + "GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string + } + ); + repoPublicKey = data; + break; + } + default: { + const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { + owner: integration.owner as string, + repo: integration.app as string + }); + repoPublicKey = data; + break; + } + } // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key - let encryptedSecrets: GitHubSecretRes = ( - await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { - owner: integration.owner as string, - repo: integration.app as string - }) - ).data.secrets.reduce( - (obj, secret) => ({ - ...obj, - [secret.name]: secret - }), - {} - ); + let encryptedSecrets: GitHubSecret[]; - encryptedSecrets = Object.keys(encryptedSecrets).reduce( - ( - result: { - [key: string]: GitHubSecret; - }, - key - ) => { - if ( - (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && - (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) - ) { - result[key] = encryptedSecrets[key]; - } - return result; - }, - {} - ); - - await Promise.all( - Object.keys(encryptedSecrets).map(async (key) => { - if (!(key in secrets)) { - return octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + switch (integration.scope) { + case GithubScope.Org: { + encryptedSecrets = ( + await octokit.request("GET /orgs/{org}/actions/secrets", { + org: integration.owner as string + }) + ).data.secrets; + break; + } + case GithubScope.Env: { + encryptedSecrets = ( + await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string + }) + ).data.secrets; + break; + } + default: { + encryptedSecrets = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner as string, - repo: integration.app as string, - secret_name: key - }); + repo: integration.app as string + }) + ).data.secrets; + break; + } + } + + for await (const encryptedSecret of encryptedSecrets) { + if ( + !(encryptedSecret.name in secrets) && + !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) && + !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix)) + ) { + switch (integration.scope) { + case GithubScope.Org: { + await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { + org: integration.owner as string, + secret_name: encryptedSecret.name + }); + break; + } + case GithubScope.Env: { + await octokit.request( + "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string, + secret_name: encryptedSecret.name + } + ); + break; + } + default: { + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: encryptedSecret.name + }); + break; + } } - }) - ); + } + } - await Promise.all( - Object.keys(secrets).map((key) => { - // let encryptedSecret; - return sodium.ready.then(async () => { - // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL); - const binsec = sodium.from_string(secrets[key].value); + await sodium.ready.then(async () => { + for await (const key of Object.keys(secrets)) { + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL); + const binsec = sodium.from_string(secrets[key].value); - // encrypt secret using libsodium - const encBytes = sodium.crypto_box_seal(binsec, binkey); + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); - // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); - await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { - owner: integration.owner as string, - repo: integration.app as string, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id - }); - }); - }) - ); + switch (integration.scope) { + case GithubScope.Org: + await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", { + org: integration.owner as string, + secret_name: key, + visibility: "all", + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + }); + break; + case GithubScope.Env: + await octokit.request( + "PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + } + ); + break; + default: + await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id + }); + break; + } + } + }); }; /** @@ -1229,6 +1307,22 @@ const syncSecretsRender = async ({ } } ); + + if (integration.metadata) { + const metadata = z.record(z.any()).parse(integration.metadata); + if (metadata.shouldAutoRedeploy === true) { + await request.post( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${integration.appId}/deploys`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + } + } }; /** diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index 4a6bed75f..5bf412c9b 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -42,6 +42,7 @@ export const integrationServiceFactory = ({ metadata, secretPath, targetService, + actorAuthMethod, targetServiceId, integrationAuthId, sourceEnvironment, @@ -55,6 +56,7 @@ export const integrationServiceFactory = ({ actor, actorId, integrationAuth.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); @@ -93,6 +95,7 @@ export const integrationServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, targetEnvironment, app, id, @@ -109,6 +112,7 @@ export const integrationServiceFactory = ({ actor, actorId, integration.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); @@ -129,7 +133,7 @@ export const integrationServiceFactory = ({ return updatedIntegration; }; - const deleteIntegration = async ({ actorId, id, actor, actorOrgId }: TDeleteIntegrationDTO) => { + const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => { const integration = await integrationDAL.findById(id); if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); @@ -137,6 +141,7 @@ export const integrationServiceFactory = ({ actor, actorId, integration.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); @@ -145,8 +150,20 @@ export const integrationServiceFactory = ({ return { ...integration, ...deletedIntegration }; }; - const listIntegrationByProject = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listIntegrationByProject = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const integrations = await integrationDAL.findByProjectId(projectId); diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts index fb8a57440..70c54ff18 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -12,6 +12,7 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError } from "@app/lib/errors"; +import { ActorAuthMethod } from "../auth/auth-type"; import { TOrgRoleDALFactory } from "./org-role-dal"; type TOrgRoleServiceFactoryDep = { @@ -26,9 +27,10 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol userId: string, orgId: string, data: Omit, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId }); if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); @@ -45,9 +47,10 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol orgId: string, roleId: string, data: Omit, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); if (data?.slug) { const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId }); @@ -62,8 +65,14 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol return updatedRole; }; - const deleteRole = async (userId: string, orgId: string, roleId: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const deleteRole = async ( + userId: string, + orgId: string, + roleId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); @@ -71,8 +80,13 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol return deletedRole; }; - const listRoles = async (userId: string, orgId: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const listRoles = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); const customRoles = await orgRoleDAL.find({ orgId }); const roles = [ @@ -115,8 +129,18 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol return roles; }; - const getUserPermission = async (userId: string, orgId: string, actorOrgId?: string) => { - const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const getUserPermission = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission, membership } = await permissionService.getUserOrgPermission( + userId, + orgId, + actorAuthMethod, + actorOrgId + ); return { permissions: packRules(permission.rules), membership }; }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index db6d9654d..64cd51e09 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -18,7 +18,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; -import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TProjectDALFactory } from "../project/project-dal"; @@ -79,8 +79,13 @@ export const orgServiceFactory = ({ /* * Get organization details by the organization id * */ - const findOrganizationById = async (userId: string, orgId: string, actorOrgId?: string) => { - await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const findOrganizationById = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const org = await orgDAL.findOrgById(orgId); if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" }); return org; @@ -95,16 +100,28 @@ export const orgServiceFactory = ({ /* * Get all workspace members * */ - const findAllOrgMembers = async (userId: string, orgId: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const findAllOrgMembers = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); return members; }; - const findOrgMembersByUsername = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); + const findOrgMembersByUsername = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + orgId, + emails + }: TFindOrgMembersByEmailDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findOrgMembersByUsername(orgId, emails); @@ -112,8 +129,8 @@ export const orgServiceFactory = ({ return members; }; - const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const findAllWorkspaces = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TFindAllWorkspacesDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id)); @@ -193,10 +210,11 @@ export const orgServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, orgId, data: { name, slug, authEnforced, scimEnabled } }: TUpdateOrgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); const plan = await licenseService.getPlan(orgId); @@ -309,8 +327,13 @@ export const orgServiceFactory = ({ /* * Delete organization by id * */ - const deleteOrganizationById = async (userId: string, orgId: string, actorOrgId?: string) => { - const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const deleteOrganizationById = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" }); @@ -324,8 +347,15 @@ export const orgServiceFactory = ({ * Org membership management * Not another service because it has close ties with how an org works doesn't make sense to seperate them * */ - const updateOrgMembership = async ({ role, orgId, userId, membershipId, actorOrgId }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const updateOrgMembership = async ({ + role, + orgId, + userId, + membershipId, + actorAuthMethod, + actorOrgId + }: TUpdateOrgMembershipDTO) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); @@ -355,8 +385,14 @@ export const orgServiceFactory = ({ /* * Invite user to organization */ - const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail, actorOrgId }: TInviteUserToOrgDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const inviteUserToOrganization = async ({ + orgId, + userId, + inviteeEmail, + actorAuthMethod, + actorOrgId + }: TInviteUserToOrgDTO) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); const org = await orgDAL.findOrgById(orgId); @@ -515,8 +551,14 @@ export const orgServiceFactory = ({ return { token, user }; }; - const deleteOrgMembership = async ({ orgId, userId, membershipId, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const deleteOrgMembership = async ({ + orgId, + userId, + membershipId, + actorAuthMethod, + actorOrgId + }: TDeleteOrgMembershipDTO) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); const deletedMembership = await orgDAL.transaction(async (tx) => { @@ -568,15 +610,26 @@ export const orgServiceFactory = ({ /* * CRUD operations of incident contacts * */ - const findIncidentContacts = async (userId: string, orgId: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const findIncidentContacts = async ( + userId: string, + orgId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; }; - const createIncidentContact = async (userId: string, orgId: string, email: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const createIncidentContact = async ( + userId: string, + orgId: string, + email: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -590,8 +643,14 @@ export const orgServiceFactory = ({ return incidentContact; }; - const deleteIncidentContact = async (userId: string, orgId: string, id: string, actorOrgId?: string) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); + const deleteIncidentContact = async ( + userId: string, + orgId: string, + id: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); const incidentContact = await incidentContactDAL.deleteById(id, orgId); diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index bd8fe2e95..c811f9bc0 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,26 +1,29 @@ import { TOrgPermission } from "@app/lib/types"; -import { ActorType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; export type TUpdateOrgMembershipDTO = { userId: string; orgId: string; membershipId: string; role: string; - actorOrgId?: string; + actorOrgId: string | undefined; + actorAuthMethod: ActorAuthMethod; }; export type TDeleteOrgMembershipDTO = { userId: string; orgId: string; membershipId: string; - actorOrgId?: string; + actorOrgId: string | undefined; + actorAuthMethod: ActorAuthMethod; }; export type TInviteUserToOrgDTO = { userId: string; orgId: string; - actorOrgId?: string; + actorOrgId: string | undefined; + actorAuthMethod: ActorAuthMethod; inviteeEmail: string; }; @@ -32,7 +35,9 @@ export type TVerifyUserToOrgDTO = { export type TFindOrgMembersByEmailDTO = { actor: ActorType; + actorOrgId: string | undefined; actorId: string; + actorAuthMethod: ActorAuthMethod; orgId: string; emails: string[]; }; @@ -40,7 +45,8 @@ export type TFindOrgMembersByEmailDTO = { export type TFindAllWorkspacesDTO = { actor: ActorType; actorId: string; - actorOrgId?: string; + actorOrgId: string | undefined; + actorAuthMethod: ActorAuthMethod; orgId: string; }; diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 6e281e69d..23667ef67 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -37,10 +37,17 @@ export const projectBotServiceFactory = ({ projectId, actorOrgId, privateKey, + actorAuthMethod, botKey, publicKey }: TFindBotByProjectIdDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const bot = await projectBotDAL.transaction(async (tx) => { @@ -88,11 +95,25 @@ export const projectBotServiceFactory = ({ } }; - const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => { + const setBotActiveState = async ({ + actor, + botId, + botKey, + actorId, + actorOrgId, + actorAuthMethod, + isActive + }: TSetActiveStateDTO) => { const bot = await projectBotDAL.findById(botId); if (!bot) throw new BadRequestError({ message: "Bot not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + bot.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); const project = await projectBotDAL.findProjectByBotId(botId); diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index 6ebb3a3d6..2acda33c0 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -27,8 +27,22 @@ export const projectEnvServiceFactory = ({ projectDAL, folderDAL }: TProjectEnvServiceFactoryDep) => { - const createEnvironment = async ({ projectId, actorId, actor, actorOrgId, name, slug }: TCreateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const createEnvironment = async ({ + projectId, + actorId, + actor, + actorOrgId, + actorAuthMethod, + name, + slug + }: TCreateEnvDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); const envs = await projectEnvDAL.find({ projectId }); @@ -65,11 +79,18 @@ export const projectEnvServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, name, id, position }: TUpdateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); const oldEnv = await projectEnvDAL.findOne({ id, projectId }); @@ -94,8 +115,14 @@ export const projectEnvServiceFactory = ({ return { environment: env, old: oldEnv }; }; - const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, id }: TDeleteEnvDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); const env = await projectEnvDAL.transaction(async (tx) => { diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index fa77760a4..70c8365ee 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -26,11 +26,18 @@ export const projectKeyServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, projectId, nonce, encryptedKey }: TUploadProjectKeyDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const receiverMembership = await projectMembershipDAL.findOne({ @@ -46,14 +53,32 @@ export const projectKeyServiceFactory = ({ await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId }); }; - const getLatestProjectKey = async ({ actorId, projectId, actor, actorOrgId }: TGetLatestProjectKeyDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getLatestProjectKey = async ({ + actorId, + projectId, + actor, + actorOrgId, + actorAuthMethod + }: TGetLatestProjectKeyDTO) => { + await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId); return latestKey; }; - const getProjectPublicKeys = async ({ actor, actorId, actorOrgId, projectId }: TGetLatestProjectKeyDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getProjectPublicKeys = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TGetLatestProjectKeyDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); return projectKeyDAL.findAllProjectUserPubKeys(projectId); }; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 1f751ee89..059c93210 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -67,8 +67,20 @@ export const projectMembershipServiceFactory = ({ projectKeyDAL, licenseService }: TProjectMembershipServiceFactoryDep) => { - const getProjectMemberships = async ({ actorId, actor, actorOrgId, projectId }: TGetProjectMembershipDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getProjectMemberships = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TGetProjectMembershipDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); return projectMembershipDAL.findAllProjectMembers(projectId); @@ -79,13 +91,20 @@ export const projectMembershipServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, members, sendEmails = true }: TAddUsersToWorkspaceDTO) => { const project = await projectDAL.findById(projectId); if (!project) throw new BadRequestError({ message: "Project not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); const orgMembers = await orgDAL.findMembership({ orgId: project.orgId, @@ -145,7 +164,9 @@ export const projectMembershipServiceFactory = ({ const addUsersToProjectNonE2EE = async ({ projectId, actorId, + actorAuthMethod, actor, + actorOrgId, emails, usernames, sendEmails = true @@ -157,7 +178,13 @@ export const projectMembershipServiceFactory = ({ throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); } - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); const usernamesAndEmails = [...emails, ...usernames]; @@ -273,11 +300,18 @@ export const projectMembershipServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, projectId, membershipId, roles }: TUpdateProjectMembershipDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); @@ -347,10 +381,17 @@ export const projectMembershipServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, projectId, membershipId }: TDeleteProjectMembershipOldDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); const member = await userDAL.findUserByProjectMembershipId(membershipId); @@ -374,11 +415,18 @@ export const projectMembershipServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, projectId, emails, usernames }: TDeleteProjectMembershipsDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); const project = await projectDAL.findById(projectId); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index b45a6e8a5..5c8ecdcff 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -13,7 +13,7 @@ import { } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; -import { ActorType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { TProjectRoleDALFactory } from "./project-role-dal"; type TProjectRoleServiceFactoryDep = { @@ -29,9 +29,16 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: actorId: string, projectId: string, data: Omit, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); @@ -49,9 +56,16 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: projectId: string, roleId: string, data: Omit, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); if (data?.slug) { const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); @@ -71,9 +85,16 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: actorId: string, projectId: string, roleId: string, - actorOrgId?: string + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); @@ -81,8 +102,20 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: return deletedRole; }; - const listRoles = async (actor: ActorType, actorId: string, projectId: string, actorOrgId?: string) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listRoles = async ( + actor: ActorType, + actorId: string, + projectId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); const customRoles = await projectRoleDAL.find({ projectId }); const roles = [ @@ -135,8 +168,18 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: return roles; }; - const getUserPermission = async (userId: string, projectId: string, actorOrgId?: string) => { - const { permission, membership } = await permissionService.getUserProjectPermission(userId, projectId, actorOrgId); + const getUserPermission = async ( + userId: string, + projectId: string, + actorAuthMethod: ActorAuthMethod, + actorOrgId: string | undefined + ) => { + const { permission, membership } = await permissionService.getUserProjectPermission( + userId, + projectId, + actorAuthMethod, + actorOrgId + ); return { permissions: packRules(permission.rules), membership }; }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 7d0826e12..369e005ac 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -5,6 +5,8 @@ import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProje import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { Filter, ProjectFilterType } from "./project-types"; + export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { @@ -139,7 +141,7 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Project}.name`, order: "asc" }, { column: `${TableName.Environment}.position`, order: "asc" } ]); - return sqlNestRelationships({ + const project = sqlNestRelationships({ data: workspaces, key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), @@ -155,11 +157,86 @@ export const projectDALFactory = (db: TDbClient) => { } ] })?.[0]; + + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + return project; } catch (error) { throw new DatabaseError({ error, name: "Find all projects" }); } }; + const findProjectBySlug = async (slug: string, orgId: string) => { + try { + const projects = await db(TableName.ProjectMembership) + .where(`${TableName.Project}.slug`, slug) + .where(`${TableName.Project}.orgId`, orgId) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .select( + selectAllTableCols(TableName.Project), + db.ref("id").withSchema(TableName.Project).as("_id"), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + .orderBy([ + { column: `${TableName.Project}.name`, order: "asc" }, + { column: `${TableName.Environment}.position`, order: "asc" } + ]); + + const project = sqlNestRelationships({ + data: projects, + key: "id", + parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), + childrenMapper: [ + { + key: "envId", + label: "environments" as const, + mapper: ({ envId, envSlug, envName: name }) => ({ + id: envId, + slug: envSlug, + name + }) + } + ] + })?.[0]; + + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + return project; + } catch (error) { + throw new DatabaseError({ error, name: "Find project by slug" }); + } + }; + + const findProjectByFilter = async (filter: Filter) => { + try { + if (filter.type === ProjectFilterType.ID) { + return await findProjectById(filter.projectId); + } + if (filter.type === ProjectFilterType.SLUG) { + if (!filter.orgId) { + throw new BadRequestError({ + message: "Organization ID is required when querying with slugs" + }); + } + + return await findProjectBySlug(filter.slug, filter.orgId); + } + throw new BadRequestError({ message: "Invalid filter type" }); + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` }); + } + }; + const checkProjectUpgradeStatus = async (projectId: string) => { const project = await projectOrm.findById(projectId); const upgradeInProgress = @@ -179,6 +256,8 @@ export const projectDALFactory = (db: TDbClient) => { findAllProjectsByIdentity, findProjectGhostUser, findProjectById, + findProjectByFilter, + findProjectBySlug, checkProjectUpgradeStatus }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 873a7d36f..5eaf081dc 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -18,6 +18,7 @@ import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -34,7 +35,9 @@ import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, + TToggleProjectAutoCapitalizationDTO, TUpdateProjectDTO, + TUpdateProjectNameDTO, TUpgradeProjectDTO } from "./project-types"; @@ -61,6 +64,7 @@ type TProjectServiceFactoryDep = { permissionService: TPermissionServiceFactory; orgService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -70,6 +74,7 @@ export const projectServiceFactory = ({ projectQueue, projectKeyDAL, permissionService, + orgDAL, userDAL, folderDAL, orgService, @@ -86,11 +91,21 @@ export const projectServiceFactory = ({ /* * Create workspace. Make user the admin * */ - const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => { + const createProject = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + workspaceName, + slug: projectSlug + }: TCreateProjectDTO) => { + const organization = await orgDAL.findOne({ id: actorOrgId }); + const { permission, membership: orgMembership } = await permissionService.getOrgPermission( actor, actorId, - orgId, + organization.id, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); @@ -98,7 +113,7 @@ export const projectServiceFactory = ({ const appCfg = getConfig(); const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY); - const plan = await licenseService.getPlan(orgId); + const plan = await licenseService.getPlan(organization.id); if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { // case: limit imposed on number of workspaces allowed // case: number of workspaces used exceeds the number of workspaces allowed @@ -108,13 +123,13 @@ export const projectServiceFactory = ({ } const results = await projectDAL.transaction(async (tx) => { - const ghostUser = await orgService.addGhostUser(orgId, tx); + const ghostUser = await orgService.addGhostUser(organization.id, tx); const project = await projectDAL.create( { name: workspaceName, - orgId, - slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), + orgId: organization.id, + slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), version: ProjectVersion.V2 }, tx @@ -270,7 +285,7 @@ export const projectServiceFactory = ({ // Get the role permission for the identity const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( ProjectMembershipRole.Admin, - orgId + organization.id ); const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); @@ -310,20 +325,28 @@ export const projectServiceFactory = ({ return results; }; - const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const deleteProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, filter }: TDeleteProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); const deletedProject = await projectDAL.transaction(async (tx) => { - const project = await projectDAL.deleteById(projectId, tx); - const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null); + const delProject = await projectDAL.deleteById(project.id, tx); + const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id).catch(() => null); // Delete the org membership for the ghost user if it's found. if (projectGhostUser) { await userDAL.deleteById(projectGhostUser.id, tx); } - return project; + return delProject; }); return deletedProject; @@ -334,16 +357,26 @@ export const projectServiceFactory = ({ return workspaces; }; - const getAProject = async ({ actorId, actorOrgId, projectId, actor }: TGetProjectDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); - return projectDAL.findProjectById(projectId); + const getAProject = async ({ actorId, actorOrgId, actorAuthMethod, filter, actor }: TGetProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter); + + await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + return project; }; - const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); - const updatedProject = await projectDAL.updateById(projectId, { + const updatedProject = await projectDAL.updateById(project.id, { name: update.name, autoCapitalization: update.autoCapitalization }); @@ -355,25 +388,58 @@ export const projectServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, autoCapitalization - }: TGetProjectDTO & { autoCapitalization: boolean }) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + }: TToggleProjectAutoCapitalizationDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization }); return updatedProject; }; - const updateName = async ({ projectId, actor, actorId, actorOrgId, name }: TGetProjectDTO & { name: string }) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const updateName = async ({ + projectId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + name + }: TUpdateProjectNameDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); const updatedProject = await projectDAL.updateById(projectId, { name }); return updatedProject; }; - const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => { - const { permission, hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId); + const upgradeProject = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + userPrivateKey + }: TUpgradeProjectDTO) => { + const { permission, hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -397,8 +463,20 @@ export const projectServiceFactory = ({ }); }; - const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + const getProjectUpgradeStatus = async ({ + projectId, + actor, + actorAuthMethod, + actorOrgId, + actorId + }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); const project = await projectDAL.findProjectById(projectId); diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 3843450c2..dcd424e18 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,37 +1,66 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; -import { ActorType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; + +export enum ProjectFilterType { + ID = "id", + SLUG = "slug" +} + +export type Filter = + | { + type: ProjectFilterType.ID; + projectId: string; + } + | { + type: ProjectFilterType.SLUG; + slug: string; + orgId: string | undefined; + }; export type TCreateProjectDTO = { actor: ActorType; + actorAuthMethod: ActorAuthMethod; actorId: string; actorOrgId?: string; - orgId: string; workspaceName: string; slug?: string; }; -export type TDeleteProjectDTO = { +export type TDeleteProjectBySlugDTO = { + slug: string; actor: ActorType; actorId: string; - actorOrgId?: string; - projectId: string; + actorOrgId: string | undefined; }; export type TGetProjectDTO = { - actor: ActorType; - actorId: string; - actorOrgId?: string; - projectId: string; -}; + filter: Filter; +} & Omit; + +export type TToggleProjectAutoCapitalizationDTO = { + autoCapitalization: boolean; +} & TProjectPermission; + +export type TUpdateProjectNameDTO = { + name: string; +} & TProjectPermission; export type TUpdateProjectDTO = { + filter: Filter; update: { name?: string; autoCapitalization?: boolean; }; -} & TProjectPermission; +} & Omit; + +export type TDeleteProjectDTO = { + filter: Filter; + actor: ActorType; + actorId: string; + actorOrgId: string | undefined; +} & Omit; export type TUpgradeProjectDTO = { userPrivateKey: string; diff --git a/backend/src/services/secret-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts index b681266fd..bf2728e95 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts @@ -28,16 +28,29 @@ export const secretBlindIndexServiceFactory = ({ actor, projectId, actorId, + actorAuthMethod, actorOrgId }: TGetProjectBlindIndexStatusDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); const secretCount = await secretBlindIndexDAL.countOfSecretsWithNullSecretBlindIndex(projectId); return Number(secretCount); }; - const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => { - const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId); + const getProjectSecrets = async ({ + projectId, + actorId, + actorAuthMethod, + actorOrgId, + actor + }: TGetProjectSecretsDTO) => { + const { hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); if (!hasRole(ProjectMembershipRole.Admin)) { throw new UnauthorizedError({ message: "User must be admin" }); } @@ -50,10 +63,17 @@ export const secretBlindIndexServiceFactory = ({ projectId, actor, actorId, + actorAuthMethod, actorOrgId, secretsToUpdate }: TUpdateProjectSecretNameDTO) => { - const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); if (!hasRole(ProjectMembershipRole.Admin)) { throw new UnauthorizedError({ message: "User must be admin" }); } diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 26c1c1f4f..baa1484b1 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -34,12 +34,19 @@ export const secretFolderServiceFactory = ({ projectId, actor, actorId, + actorAuthMethod, actorOrgId, name, environment, path: secretPath }: TCreateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) @@ -114,12 +121,19 @@ export const secretFolderServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, name, environment, path: secretPath, id }: TUpdateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) @@ -162,11 +176,18 @@ export const secretFolderServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, path: secretPath, idOrName }: TDeleteFolderDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) @@ -196,12 +217,13 @@ export const secretFolderServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, path: secretPath }: TGetFolderDTO) => { // folder list is allowed to be read by anyone // permission to check does user has access - await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "get folders" }); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 1beae9be6..40f9797e4 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -45,10 +45,17 @@ export const secretImportServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, projectId, path }: TCreateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( @@ -97,10 +104,17 @@ export const secretImportServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, data, id }: TUpdateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -144,9 +158,16 @@ export const secretImportServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, id }: TDeleteSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -167,8 +188,22 @@ export const secretImportServiceFactory = ({ return secImport; }; - const getImports = async ({ path, environment, projectId, actor, actorId, actorOrgId }: TGetSecretImportsDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getImports = async ({ + path, + environment, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TGetSecretImportsDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -186,10 +221,17 @@ export const secretImportServiceFactory = ({ environment, projectId, actor, + actorAuthMethod, actorId, actorOrgId }: TGetSecretsFromImportDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 1007ec4c3..ed8f5fec7 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -15,8 +15,23 @@ type TSecretTagServiceFactoryDep = { export type TSecretTagServiceFactory = ReturnType; export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => { - const createTag = async ({ name, slug, actor, color, actorId, actorOrgId, projectId }: TCreateTagDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const createTag = async ({ + name, + slug, + actor, + color, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TCreateTagDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); const existingTag = await secretTagDAL.findOne({ slug, projectId }); @@ -32,19 +47,31 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe return newTag; }; - const deleteTag = async ({ actorId, actor, actorOrgId, id }: TDeleteTagDTO) => { + const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => { const tag = await secretTagDAL.findById(id); if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, tag.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + tag.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); const deletedTag = await secretTagDAL.deleteById(tag.id); return deletedTag; }; - const getProjectTags = async ({ actor, actorId, actorOrgId, projectId }: TListProjectTagsDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); const tags = await secretTagDAL.find({ projectId }, { sort: [["createdAt", "asc"]] }); diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 51136e40f..f47428fc7 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -145,10 +145,17 @@ export const secretServiceFactory = ({ actorId, actorOrgId, environment, + actorAuthMethod, projectId, ...inputSecret }: TCreateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -230,10 +237,17 @@ export const secretServiceFactory = ({ actorId, actorOrgId, environment, + actorAuthMethod, projectId, ...inputSecret }: TUpdateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -341,11 +355,18 @@ export const secretServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, projectId, ...inputSecret }: TDeleteSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -401,9 +422,16 @@ export const secretServiceFactory = ({ projectId, actor, actorOrgId, + actorAuthMethod, includeImports }: TGetSecretsDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -445,6 +473,7 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, projectId, environment, path, @@ -453,7 +482,13 @@ export const secretServiceFactory = ({ version, includeImports }: TGetASecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -534,12 +569,19 @@ export const secretServiceFactory = ({ path, actor, actorId, + actorAuthMethod, actorOrgId, environment, projectId, secrets: inputSecrets }: TCreateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -597,11 +639,18 @@ export const secretServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, projectId, secrets: inputSecrets }: TUpdateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -678,9 +727,16 @@ export const secretServiceFactory = ({ projectId, actor, actorId, + actorAuthMethod, actorOrgId }: TDeleteBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -728,6 +784,7 @@ export const secretServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, environment, includeImports }: TGetSecretsRawDTO) => { @@ -740,6 +797,7 @@ export const secretServiceFactory = ({ environment, actor, actorOrgId, + actorAuthMethod, path, includeImports }); @@ -763,6 +821,7 @@ export const secretServiceFactory = ({ projectId, actorId, actorOrgId, + actorAuthMethod, secretName, includeImports, version @@ -773,6 +832,7 @@ export const secretServiceFactory = ({ const secret = await getSecretByName({ actorId, projectId, + actorAuthMethod, environment, actor, actorOrgId, @@ -792,6 +852,7 @@ export const secretServiceFactory = ({ environment, actor, actorOrgId, + actorAuthMethod, type, secretPath, secretValue, @@ -813,6 +874,7 @@ export const secretServiceFactory = ({ path: secretPath, actor, actorId, + actorAuthMethod, actorOrgId, secretKeyCiphertext: secretKeyEncrypted.ciphertext, secretKeyIV: secretKeyEncrypted.iv, @@ -839,6 +901,7 @@ export const secretServiceFactory = ({ environment, actor, actorOrgId, + actorAuthMethod, type, secretPath, secretValue, @@ -858,6 +921,7 @@ export const secretServiceFactory = ({ actor, actorId, actorOrgId, + actorAuthMethod, secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueIV: secretValueEncrypted.iv, secretValueTag: secretValueEncrypted.tag, @@ -877,6 +941,7 @@ export const secretServiceFactory = ({ environment, actor, actorOrgId, + actorAuthMethod, type, secretPath }: TDeleteSecretRawDTO) => { @@ -891,7 +956,8 @@ export const secretServiceFactory = ({ path: secretPath, actor, actorId, - actorOrgId + actorOrgId, + actorAuthMethod }); await snapshotService.performSnapshot(secret.folderId); @@ -904,6 +970,7 @@ export const secretServiceFactory = ({ actorId, actor, actorOrgId, + actorAuthMethod, limit = 20, offset = 0, secretId @@ -914,7 +981,13 @@ export const secretServiceFactory = ({ const folder = await folderDAL.findById(secret.folderId); if (!folder) throw new BadRequestError({ message: "Failed to find secret" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + folder.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index cce0d3780..e434bd91f 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -9,6 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorType } from "../auth/auth-type"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TServiceTokenDALFactory } from "./service-token-dal"; @@ -24,6 +25,7 @@ type TServiceTokenServiceFactoryDep = { userDAL: TUserDALFactory; permissionService: Pick; projectEnvDAL: Pick; + projectDAL: Pick; }; export type TServiceTokenServiceFactory = ReturnType; @@ -32,7 +34,8 @@ export const serviceTokenServiceFactory = ({ serviceTokenDAL, userDAL, permissionService, - projectEnvDAL + projectEnvDAL, + projectDAL }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -40,6 +43,7 @@ export const serviceTokenServiceFactory = ({ name, actor, actorOrgId, + actorAuthMethod, scopes, actorId, projectId, @@ -47,7 +51,13 @@ export const serviceTokenServiceFactory = ({ permissions, encryptedKey }: TCreateServiceTokenDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); scopes.forEach(({ environment, secretPath }) => { @@ -91,7 +101,7 @@ export const serviceTokenServiceFactory = ({ return { token, serviceToken }; }; - const deleteServiceToken = async ({ actorId, actor, actorOrgId, id }: TDeleteServiceTokenDTO) => { + const deleteServiceToken = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteServiceTokenDTO) => { const serviceToken = await serviceTokenDAL.findById(id); if (!serviceToken) throw new BadRequestError({ message: "Token not found" }); @@ -99,6 +109,7 @@ export const serviceTokenServiceFactory = ({ actor, actorId, serviceToken.projectId, + actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); @@ -119,8 +130,20 @@ export const serviceTokenServiceFactory = ({ return { serviceToken, user: serviceTokenUser }; }; - const getProjectServiceTokens = async ({ actorId, actor, actorOrgId, projectId }: TProjectServiceTokensDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const getProjectServiceTokens = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TProjectServiceTokensDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); const tokens = await serviceTokenDAL.find({ projectId }, { sort: [["createdAt", "desc"]] }); @@ -130,7 +153,11 @@ export const serviceTokenServiceFactory = ({ const fnValidateServiceToken = async (token: string) => { const [, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>token.split(".", 3); const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER); + if (!serviceToken) throw new UnauthorizedError(); + const project = await projectDAL.findById(serviceToken.projectId); + + if (!project) throw new UnauthorizedError({ message: "Service token project not found" }); if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) { await serviceTokenDAL.deleteById(serviceToken.id); @@ -142,7 +169,8 @@ export const serviceTokenServiceFactory = ({ const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { lastUsed: new Date() }); - return { ...serviceToken, lastUsed: updatedToken.lastUsed }; + + return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId }; }; return { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index d76ad3ec3..07fc2e991 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -136,6 +136,7 @@ export const superAdminServiceFactory = ({ await updateServerCfg({ initialized: true }); const token = await authService.generateUserTokens({ user: userInfo.user, + authMethod: AuthMethod.EMAIL, ip, userAgent, organizationId: undefined diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index c3919cc50..4a05ad219 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -31,13 +31,20 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer actor, actorId, actorOrgId, + actorAuthMethod, projectId, webhookUrl, environment, secretPath, webhookSecretKey }: TCreateWebhookDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Env not found" }); @@ -73,33 +80,51 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer return { ...webhook, projectId, environment: env }; }; - const updateWebhook = async ({ actorId, actor, actorOrgId, id, isDisabled }: TUpdateWebhookDTO) => { + const updateWebhook = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, isDisabled }: TUpdateWebhookDTO) => { const webhook = await webhookDAL.findById(id); if (!webhook) throw new BadRequestError({ message: "Webhook not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + webhook.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); const updatedWebhook = await webhookDAL.updateById(id, { isDisabled }); return { ...webhook, ...updatedWebhook }; }; - const deleteWebhook = async ({ id, actor, actorId, actorOrgId }: TDeleteWebhookDTO) => { + const deleteWebhook = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteWebhookDTO) => { const webhook = await webhookDAL.findById(id); if (!webhook) throw new BadRequestError({ message: "Webhook not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + webhook.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); const deletedWebhook = await webhookDAL.deleteById(id); return { ...webhook, ...deletedWebhook }; }; - const testWebhook = async ({ id, actor, actorId, actorOrgId }: TTestWebhookDTO) => { + const testWebhook = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TTestWebhookDTO) => { const webhook = await webhookDAL.findById(id); if (!webhook) throw new BadRequestError({ message: "Webhook not found" }); - const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + webhook.projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; @@ -119,8 +144,22 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer return { ...webhook, ...updatedWebhook }; }; - const listWebhooks = async ({ actorId, actor, actorOrgId, projectId, secretPath, environment }: TListWebhookDTO) => { - const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + const listWebhooks = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId, + secretPath, + environment + }: TListWebhookDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); return webhookDAL.findAllWebhooks(projectId, environment, secretPath); diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 1b69b8f23..dfb0cf7bc 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -164,6 +164,28 @@ func CallGetAllOrganizations(httpClient *resty.Client) (GetOrganizationsResponse return orgResponse, nil } +func CallSelectOrganization(httpClient *resty.Client, request SelectOrganizationRequest) (SelectOrganizationResponse, error) { + var selectOrgResponse SelectOrganizationResponse + + response, err := httpClient. + R(). + SetBody(request). + SetResult(&selectOrgResponse). + SetHeader("User-Agent", USER_AGENT). + Post(fmt.Sprintf("%v/v3/auth/select-organization", config.INFISICAL_URL)) + + if err != nil { + return SelectOrganizationResponse{}, err + } + + if response.IsError() { + return SelectOrganizationResponse{}, fmt.Errorf("CallSelectOrganization: Unsuccessful response: [response=%v]", response) + } + + return selectOrgResponse, nil + +} + func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) { var workSpacesResponse GetWorkSpacesResponse response, err := httpClient. diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 310976078..f80c17a92 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -135,6 +135,14 @@ type GetOrganizationsResponse struct { } `json:"organizations"` } +type SelectOrganizationResponse struct { + Token string `json:"token"` +} + +type SelectOrganizationRequest struct { + OrganizationId string `json:"organizationId"` +} + type Secret struct { SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` SecretKeyIV string `json:"secretKeyIV,omitempty"` diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index e47eb447e..99d2ef502 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -74,6 +74,21 @@ var initCmd = &cobra.Command{ selectedOrganization := organizations[index] + tokenResponse, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) + + if err != nil { + util.HandleError(err, "Unable to select organization") + } + + // set the config jwt token to the new token + userCreds.UserCredentials.JTWToken = tokenResponse.Token + err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials) + httpClient.SetAuthToken(tokenResponse.Token) + + if err != nil { + util.HandleError(err, "Unable to store your user credentials") + } + workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) if err != nil { util.HandleError(err, "Unable to pull projects that belong to you") diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 5cff7770f..f0337f658 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -301,11 +301,13 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token) util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info") } + // Login is successful so ask user to choose organization + newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token) //updating usercredentials userCredentialsToBeStored.Email = email userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) - userCredentialsToBeStored.JTWToken = loginTwoResponse.Token + userCredentialsToBeStored.JTWToken = newJwtToken } func init() { @@ -480,6 +482,44 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R return &loginOneResponseResult, &loginTwoResponseResult, nil } +func GetJwtTokenWithOrganizationId(oldJwtToken string) string { + log.Debug().Msg(fmt.Sprint("GetJwtTokenWithOrganizationId: ", "oldJwtToken", oldJwtToken)) + + httpClient := resty.New() + httpClient.SetAuthToken(oldJwtToken) + + organizationResponse, err := api.CallGetAllOrganizations(httpClient) + + if err != nil { + util.HandleError(err, "Unable to pull organizations that belong to you") + } + + organizations := organizationResponse.Organizations + + organizationNames := util.GetOrganizationsNameList(organizationResponse) + + prompt := promptui.Select{ + Label: "Which Infisical organization would you like to log into?", + Items: organizationNames, + } + + index, _, err := prompt.Run() + if err != nil { + util.HandleError(err) + } + + selectedOrganization := organizations[index] + + selectedOrgRes, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) + + if err != nil { + util.HandleError(err) + } + + return selectedOrgRes.Token + +} + func userLoginMenu(currentLoggedInUserEmail string) (bool, error) { label := fmt.Sprintf("Current logged in user email: %s on domain: %s", currentLoggedInUserEmail, config.INFISICAL_URL) diff --git a/docs/documentation/getting-started/sdks.mdx b/docs/documentation/getting-started/sdks.mdx index aef15294a..b3e8a3925 100644 --- a/docs/documentation/getting-started/sdks.mdx +++ b/docs/documentation/getting-started/sdks.mdx @@ -18,4 +18,4 @@ Follow the instructions for your language use the SDK for it: - [Java SDK](https://infisical.com/docs/sdks/languages/java) - [.NET SDK](https://infisical.com/docs/sdks/languages/csharp) -Missing a language? [Throw in a request](https://github.com/Infisical/infisical/issues). \ No newline at end of file +Missing a language? [Throw in a request here](https://github.com/Infisical/infisical/issues). diff --git a/docs/images/integrations/github/integrations-github-scope-env.png b/docs/images/integrations/github/integrations-github-scope-env.png new file mode 100644 index 000000000..e38874bd3 Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-env.png differ diff --git a/docs/images/integrations/github/integrations-github-scope-org.png b/docs/images/integrations/github/integrations-github-scope-org.png new file mode 100644 index 000000000..d5ef76a2b Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-org.png differ diff --git a/docs/images/integrations/github/integrations-github-scope-repo.png b/docs/images/integrations/github/integrations-github-scope-repo.png new file mode 100644 index 000000000..353527c78 Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-repo.png differ diff --git a/docs/images/integrations/github/integrations-github.png b/docs/images/integrations/github/integrations-github.png index dccc42c0d..38550b466 100644 Binary files a/docs/images/integrations/github/integrations-github.png and b/docs/images/integrations/github/integrations-github.png differ diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 95fe53ece..10caabc2f 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -3,17 +3,14 @@ title: "GitHub Actions" description: "How to sync secrets from Infisical to GitHub Actions" --- +Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level. + +Prerequisites: +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- Ensure that you have admin privileges to the repository you want to sync secrets to. + - - Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54). - - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - Ensure you have admin privileges to the repo you want to sync secrets to. - Navigate to your project's integrations tab in Infisical. @@ -29,12 +26,27 @@ description: "How to sync secrets from Infisical to GitHub Actions" Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. - - Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo. + + Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment. + + + + ![integrations github](../../images/integrations/github/integrations-github-scope-repo.png) + + + ![integrations github](../../images/integrations/github/integrations-github-scope-org.png) + + + ![integrations github](../../images/integrations/github/integrations-github-scope-env.png) + + + + Finally, press create integration to start syncing secrets to GitHub. ![integrations github](../../images/integrations/github/integrations-github.png) + Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub @@ -45,13 +57,13 @@ description: "How to sync secrets from Infisical to GitHub Actions" ![integrations github config](../../images/integrations/github/integrations-github-config-settings.png) ![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png) - ![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png) + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png) Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com` and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`. - ![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png) - + ![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png) + If you have a GitHub organization, you can create an OAuth application under it in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App. @@ -59,17 +71,17 @@ description: "How to sync secrets from Infisical to GitHub Actions" Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application. - - ![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png) - + + ![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png) + Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application: - `CLIENT_ID_GITHUB`: The **Client ID** of your GitHub OAuth application. - `CLIENT_SECRET_GITHUB`: The **Client Secret** of your GitHub OAuth application. - + Once added, restart your Infisical instance and use the GitHub integration. + - diff --git a/docs/self-hosting/ee.mdx b/docs/self-hosting/ee.mdx index e1f3cff40..832d99eaf 100644 --- a/docs/self-hosting/ee.mdx +++ b/docs/self-hosting/ee.mdx @@ -9,7 +9,7 @@ This guide walks through how you can use these paid features in Infisical. - Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting team@infisical.com to purchase a license. + Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting sales@infisical.com to purchase a license. Once purchased, you will be issued a license key. @@ -25,4 +25,4 @@ This guide walks through how you can use these paid features in Infisical. Once the license expires, Infisical will continue to run, but EE features will be disabled until the license is renewed or a new one is purchased. - \ No newline at end of file + diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 6666aaabf..13e8e5ab7 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { }, plugins: ["react", "prettier", "simple-import-sort", "import"], rules: { + "@typescript-eslint/no-empty-function": "off", quotes: ["error", "double", { avoidEscape: true }], "comma-dangle": ["error", "only-multiline"], "react/react-in-jsx-scope": "off", @@ -72,7 +73,6 @@ module.exports = { ], "@typescript-eslint/no-non-null-assertion": "off", "simple-import-sort/exports": "warn", - "@typescript-eslint/no-empty-function": "off", "simple-import-sort/imports": [ "warn", { diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 83c1ca9e3..1a68699d2 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -1,28 +1,28 @@ -const path = require('path'); +const path = require("path"); module.exports = { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - 'storybook-dark-mode', + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "storybook-dark-mode", { - name: '@storybook/addon-styling', + name: "@storybook/addon-styling", options: { postCss: { - implementation: require('postcss') + implementation: require("postcss") } } } ], framework: { - name: '@storybook/nextjs', + name: "@storybook/nextjs", options: {} }, core: { disableTelemetry: true }, docs: { - autodocs: 'tag' + autodocs: "tag" } }; diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 246d5c9cc..cc26e5512 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config"; export const initPostHog = () => { // @ts-ignore - console.log("Hi there 👋") + console.log("Hi there 👋"); try { if (typeof window !== "undefined") { // @ts-ignore @@ -19,7 +19,7 @@ export const initPostHog = () => { return posthog; } catch (e) { - console.log("posthog err", e) + console.log("posthog err", e); } return undefined; diff --git a/frontend/src/components/basic/Error.tsx b/frontend/src/components/basic/Error.tsx index bf892e03d..1ef937e4a 100644 --- a/frontend/src/components/basic/Error.tsx +++ b/frontend/src/components/basic/Error.tsx @@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const Error = ({ text }: { text: string }): JSX.Element => { return ( -
- - {text &&

{text}

} +
+ + {text &&

{text}

}
); }; diff --git a/frontend/src/components/basic/InputField.tsx b/frontend/src/components/basic/InputField.tsx index 0bc01defc..346294e56 100644 --- a/frontend/src/components/basic/InputField.tsx +++ b/frontend/src/components/basic/InputField.tsx @@ -39,16 +39,16 @@ const InputField = ({ if (isStatic === true) { return ( -
-

{label}

- {text &&

{text}

} +
+

{label}

+ {text &&

{text}

} onChangeHandler(e.target.value)} type={type} placeholder={placeholder} value={value} required={isRequired} - className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none" + className="text-md min-w-16 w-full rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none" name={name} readOnly autoComplete={autoComplete} @@ -58,12 +58,12 @@ const InputField = ({ ); } return ( -
-
-

{label}

+
+
+

{label}

@@ -75,11 +75,11 @@ const InputField = ({ required={isRequired} className={`${ blurred - ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400" + ? "text-bunker-800 focus:text-gray-400 active:text-gray-400 group-hover:text-gray-400" : "" } ${ error ? "focus:ring-red/50" : "focus:ring-primary/50" - } relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} + } text-md min-w-16 peer relative w-full rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`} name={name} spellCheck="false" autoComplete={autoComplete} @@ -91,7 +91,7 @@ const InputField = ({ onClick={() => { setPasswordVisible(!passwordVisible); }} - className="absolute self-end mr-3 text-gray-400 cursor-pointer" + className="absolute mr-3 cursor-pointer self-end text-gray-400" > {passwordVisible ? ( @@ -101,7 +101,7 @@ const InputField = ({ )} {blurred && ( -
+

{value .split("") @@ -109,7 +109,7 @@ const InputField = ({ .map(() => ( ))} @@ -121,7 +121,7 @@ const InputField = ({

)} */}
- {error &&

{errorText}

} + {error &&

{errorText}

}
); }; diff --git a/frontend/src/components/basic/Listbox.tsx b/frontend/src/components/basic/Listbox.tsx index 5cdeb26d9..cad9aaab4 100644 --- a/frontend/src/components/basic/Listbox.tsx +++ b/frontend/src/components/basic/Listbox.tsx @@ -34,19 +34,19 @@ const ListBox = ({
{text} - + {" "} {isSelected}
{data && ( -
+
)} @@ -58,16 +58,16 @@ const ListBox = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {data.map((person, personIdx) => ( - `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ - selected ? "bg-white/10 text-gray-400 font-bold" : "" + `relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${ + selected ? "bg-white/10 font-bold text-gray-400" : "" } ${ active && !selected - ? "bg-white/5 text-mineshaft-200 cursor-pointer" + ? "cursor-pointer bg-white/5 text-mineshaft-200" : "text-gray-400" } ` } @@ -83,7 +83,7 @@ const ListBox = ({ {person} {selected ? ( - + ) : null} @@ -92,9 +92,9 @@ const ListBox = ({ ))} {buttonAction && ( -
diff --git a/frontend/src/components/basic/dialog/AddUserDialog.tsx b/frontend/src/components/basic/dialog/AddUserDialog.tsx index b36c31d6c..dd2aede0a 100644 --- a/frontend/src/components/basic/dialog/AddUserDialog.tsx +++ b/frontend/src/components/basic/dialog/AddUserDialog.tsx @@ -13,76 +13,63 @@ type Props = { orgName: string; }; -const AddUserDialog = ({ - isOpen, - closeModal, - submitModal, - email, - setEmail, - orgName, -}: Props) => { +const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => { const submit = () => { submitModal(email); }; return ( -
+
- + -
+
-
-
+
+
- + Invite others to {orgName} -
-

- An invite is specific to an email address and expires - after 1 day. For security reasons, you will need to - separately add members to projects. +

+

+ An invite is specific to an email address and expires after 1 day. For + security reasons, you will need to separately add members to projects.

-
+
-
-
{/* diff --git a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx index 88dc7cb78..b1cc1fd07 100644 --- a/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx +++ b/frontend/src/components/basic/dialog/AddWorkspaceDialog.tsx @@ -5,7 +5,6 @@ import Button from "../buttons/Button"; import InputField from "../InputField"; import { Checkbox } from "../table/Checkbox"; - type Props = { isOpen: boolean; closeModal: () => void; @@ -26,8 +25,8 @@ const AddWorkspaceDialog = ({ workspaceName, setWorkspaceName, error, - loading, -}:Props) => { + loading +}: Props) => { const [addAllUsers, setAddAllUsers] = useState(true); const submit = () => { submitModal(workspaceName, addAllUsers); @@ -60,11 +59,8 @@ const AddWorkspaceDialog = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - - + + Create a new project
@@ -72,7 +68,7 @@ const AddWorkspaceDialog = ({ This project will contain your secrets and configs.

-
+
- +
-
{textLine1}
-
{textLine2}
-
+
{textLine1}
+
{textLine2}
+ diff --git a/frontend/src/components/context/Notifications/Notification.tsx b/frontend/src/components/context/Notifications/Notification.tsx index 806b68ad2..14b0ba556 100644 --- a/frontend/src/components/context/Notifications/Notification.tsx +++ b/frontend/src/components/context/Notifications/Notification.tsx @@ -36,25 +36,28 @@ const Notification = ({ notification, clearNotification }: NotificationProps) => return (
{notification.type === "error" && ( -
+
)} {notification.type === "success" && ( -
+
)} {notification.type === "info" && ( -
+
)} -

{notification.text}

+

{notification.text}

); diff --git a/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx b/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx index 9c8582d6a..1926e7d37 100644 --- a/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx +++ b/frontend/src/components/dashboard/ConfirmEnvOverwriteModal.tsx @@ -30,9 +30,9 @@ const ConfirmEnvOverwriteModal = ({ onClose={onClose} >
-

Your file contains the following duplicate secrets:

+

Your file contains the following duplicate secrets:

{duplicateKeys.join(", ")}

-

Are you sure you want to overwrite these secrets?

+

Are you sure you want to overwrite these secrets?

diff --git a/frontend/src/components/dashboard/DashboardInputField.tsx b/frontend/src/components/dashboard/DashboardInputField.tsx index 3cf87924e..60b92a51e 100644 --- a/frontend/src/components/dashboard/DashboardInputField.tsx +++ b/frontend/src/components/dashboard/DashboardInputField.tsx @@ -1,5 +1,10 @@ import { memo, SyntheticEvent, useRef } from "react"; -import { faCircle, faCodeBranch, faExclamationCircle, faEye } from "@fortawesome/free-solid-svg-icons"; +import { + faCircle, + faCodeBranch, + faExclamationCircle, + faEye +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import guidGenerator from "../utilities/randomId"; @@ -31,8 +36,8 @@ interface DashboardInputFieldProps { * @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard * @param {boolean} obj.isDuplicate - if the key name is duplicated * @param {boolean} obj.override - whether a secret/row should be displalyed as overriden - * - * + * + * * @returns */ @@ -61,29 +66,31 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
+
onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)} + onChange={(e) => + onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id) + } type={type} value={value} - className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${ + className={`ph-no-capture min-w-16 peer z-10 h-full w-full bg-transparent px-2 font-mono text-sm caret-bunker-200 outline-none ${ error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100" } duration-200`} spellCheck="false" />
{startsWithNumber && ( -
- + )} {isDuplicate && value !== "" && !startsWithNumber && ( -
- +
)} - {!error &&
- -
} + {!error && ( +
+ +
+ )}
); } @@ -127,20 +145,29 @@ const DashboardInputField = ({ return ( -
+
- {value?.split("\n")[0] ? - {value?.split("\n")[0]} - : - } - {value?.split("\n")[1] && - {value?.split("\n")[1]} - } + {value?.split("\n")[0] ? ( + + {value?.split("\n")[0]} + + ) : ( + - + )} + {value?.split("\n")[1] && ( + + {value?.split("\n")[1]} + + )}
@@ -148,10 +175,10 @@ const DashboardInputField = ({ } if (type === "value") { return ( -
-
+
+
{overrideEnabled === true && ( -
+
Override enabled
)} @@ -160,20 +187,20 @@ const DashboardInputField = ({ onChange={(e) => onChangeHandler(e.target.value, id)} onScroll={syncScroll} className={`${ - blurred - ? "text-transparent focus:text-transparent active:text-transparent" - : "" - } z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} + blurred ? "text-transparent focus:text-transparent active:text-transparent" : "" + } ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none duration-200 no-scrollbar`} spellCheck="false" />
{value?.split(REGEX).map((word) => { if (word.match(REGEX) !== null) { @@ -203,20 +230,24 @@ const DashboardInputField = ({ })}
{blurred && ( -
-
+
+
{value?.split("").map(() => ( ))} - {value?.split("").length === 0 && EMPTY} + {value?.split("").length === 0 && EMPTY} +
+
+
-
)}
diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx index 530ee3749..da7357fe6 100644 --- a/frontend/src/components/dashboard/DeleteActionButton.tsx +++ b/frontend/src/components/dashboard/DeleteActionButton.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React from "react"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -8,32 +8,35 @@ import Button from "../basic/buttons/Button"; type Props = { onSubmit: () => void; isPlain?: boolean; -} +}; export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => { const { t } = useTranslation(); return ( -
- {isPlain - ?
null} - role="button" - tabIndex={0} - onClick={onSubmit} - className="invisible group-hover:visible" - > - -
- :
- ) -} + ); +}; diff --git a/frontend/src/components/dashboard/DownloadSecretsMenu.tsx b/frontend/src/components/dashboard/DownloadSecretsMenu.tsx index a921798ca..29942abf0 100644 --- a/frontend/src/components/dashboard/DownloadSecretsMenu.tsx +++ b/frontend/src/components/dashboard/DownloadSecretsMenu.tsx @@ -18,7 +18,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
+ > + {" "} + {String(t("signup.verify"))}{" "} +
-
+
{t("signup.step2-resend-alert")} -
+
-

{t("signup.step2-spam-alert")}

+

{t("signup.step2-spam-alert")}

); diff --git a/frontend/src/components/signup/DonwloadBackupPDFStep.tsx b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx index 73082af32..22d278cc3 100644 --- a/frontend/src/components/signup/DonwloadBackupPDFStep.tsx +++ b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx @@ -57,19 +57,22 @@ export default function DonwloadBackupPDFStep({ }; return ( -
-

- +

+

+ {t("signup.step4-message")}

-
-
+
+
{t("signup.step4-description1")} {t("signup.step4-description3")}
-
-
+
+
+ > + {" "} + {String(t("signup.step1-submit"))}{" "} +
diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index d1cc6ef7d..a06fc3568 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -16,7 +16,7 @@ export default function TeamInviteStep(): JSX.Element { const router = useRouter(); const [emails, setEmails] = useState(""); const { data: serverDetails } = useFetchServerStatus(); - + const { mutateAsync } = useAddUserToOrg(); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const); @@ -40,55 +40,61 @@ export default function TeamInviteStep(): JSX.Element { }; return ( -
-

+

+

{t("signup.step5-invite-team")}

-

+

{t("signup.step5-subtitle")}

-
+
-
+
Emails