diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 5e17cf2bb..b5f4f7ac2 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -49,9 +49,6 @@ RUN rm -fr ${SOFTHSM2_SOURCES} # Install pkcs11-tool RUN apt-get install -y opensc -RUN mkdir -p /etc/softhsm2/tokens && \ - softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - # ? App setup # Install Infisical CLI @@ -64,10 +61,14 @@ WORKDIR /app COPY package.json package.json COPY package-lock.json package-lock.json +COPY dev-entrypoint.sh dev-entrypoint.sh +RUN chmod +x dev-entrypoint.sh + RUN npm install COPY . . ENV HOST=0.0.0.0 +ENTRYPOINT ["/app/dev-entrypoint.sh"] CMD ["npm", "run", "dev:docker"] diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index db5107985..4d5b84260 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -50,9 +50,6 @@ RUN rm -fr ${SOFTHSM2_SOURCES} # Install pkcs11-tool RUN apt-get install -y opensc -RUN mkdir -p /etc/softhsm2/tokens && \ - softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - WORKDIR /openssl-build RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && tar -xf openssl-3.1.2.tar.gz \ @@ -77,6 +74,9 @@ WORKDIR /app COPY package.json package.json COPY package-lock.json package-lock.json +COPY dev-entrypoint.sh dev-entrypoint.sh +RUN chmod +x dev-entrypoint.sh + RUN npm install COPY . . @@ -87,4 +87,5 @@ ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules # ENV NODE_OPTIONS=--force-fips # Note(Daniel): We can't set this on the node options because it may break for existing folks using the infisical/infisical-fips image. Instead we call crypto.setFips(true) at runtime. ENV FIPS_ENABLED=true +ENTRYPOINT ["/app/dev-entrypoint.sh"] CMD ["npm", "run", "dev:docker"] diff --git a/backend/dev-entrypoint.sh b/backend/dev-entrypoint.sh new file mode 100755 index 000000000..9cb3c0a5e --- /dev/null +++ b/backend/dev-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +update-ca-certificates + +# Initialize SoftHSM token if it doesn't exist +if [ ! -f /etc/softhsm2/tokens/auth-app.db ]; then + echo "Initializing SoftHSM token..." + mkdir -p /etc/softhsm2/tokens + softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + echo "SoftHSM token initialized" +else + echo "SoftHSM token already exists, skipping initialization" +fi + + +exec "$@" \ No newline at end of file diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts index 4f72987cb..d3a8b0f67 100644 --- a/backend/e2e-test/routes/v2/service-token.spec.ts +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -146,7 +146,8 @@ describe("Service token secret ops", async () => { let folderId = ""; beforeAll(async () => { initLogger(); - await initEnvConfig(testSuperAdminDAL, logger); + + await initEnvConfig(testHsmService, testKmsRootConfigDAL, testSuperAdminDAL, logger); serviceToken = await createServiceToken( [{ secretPath: "/**", environment: seedData1.environment.slug }], diff --git a/backend/e2e-test/routes/v3/secrets.spec.ts b/backend/e2e-test/routes/v3/secrets.spec.ts index 1e58c7f4a..db5953f29 100644 --- a/backend/e2e-test/routes/v3/secrets.spec.ts +++ b/backend/e2e-test/routes/v3/secrets.spec.ts @@ -158,7 +158,7 @@ describe("Secret V3 Router", async () => { let folderId = ""; beforeAll(async () => { initLogger(); - await initEnvConfig(testSuperAdminDAL, logger); + await initEnvConfig(testHsmService, testKmsRootConfigDAL, testSuperAdminDAL, logger); const projectKeyRes = await testServer.inject({ method: "GET", diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 085b8fe30..0f84dbee2 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -6,7 +6,7 @@ import { crypto } from "@app/lib/crypto/cryptography"; import path from "path"; import { seedData1 } from "@app/db/seed-data"; -import { getDatabaseCredentials, initEnvConfig } from "@app/lib/config/env"; +import { getDatabaseCredentials, getHsmConfig, initEnvConfig } from "@app/lib/config/env"; import { initLogger } from "@app/lib/logger"; import { main } from "@app/server/app"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; @@ -20,6 +20,8 @@ import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { buildRedisFromConfig } from "@app/lib/config/redis"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { bootstrapCheck } from "@app/server/boot-strap-check"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -28,6 +30,7 @@ export default { async setup() { const logger = initLogger(); const databaseCredentials = getDatabaseCredentials(logger); + const hsmConfig = getHsmConfig(logger); const db = initDbConnection({ dbConnectionUri: databaseCredentials.dbConnectionUri, @@ -35,7 +38,19 @@ export default { }); const superAdminDAL = superAdminDALFactory(db); - const envCfg = await initEnvConfig(superAdminDAL, logger); + const kmsRootConfigDAL = kmsRootConfigDALFactory(db); + + const hsmModule = initializeHsmModule(hsmConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig: hsmConfig + }); + + await hsmService.startService(); + + const envCfg = await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger); const redis = buildRedisFromConfig(envCfg); await redis.flushdb("SYNC"); @@ -68,16 +83,14 @@ export default { await queue.initialize(); - const hsmModule = initializeHsmModule(envCfg); - hsmModule.initialize(); - const server = await main({ db, smtp, logger, queue, keyStore, - hsmModule: hsmModule.getModule(), + hsmService, + kmsRootConfigDAL, superAdminDAL, redis, envConfig: envCfg @@ -92,6 +105,10 @@ export default { // @ts-expect-error type globalThis.testSuperAdminDAL = superAdminDAL; // @ts-expect-error type + globalThis.testKmsRootConfigDAL = kmsRootConfigDAL; + // @ts-expect-error type + globalThis.testHsmService = hsmService; + // @ts-expect-error type globalThis.jwtAuthToken = crypto.jwt().sign( { authTokenType: AuthTokenType.ACCESS_TOKEN, diff --git a/backend/src/@types/fastify-zod.d.ts b/backend/src/@types/fastify-zod.d.ts index f0240d1a0..91cd00605 100644 --- a/backend/src/@types/fastify-zod.d.ts +++ b/backend/src/@types/fastify-zod.d.ts @@ -1,7 +1,9 @@ import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { CustomLogger } from "@app/lib/logger/logger"; import { ZodTypeProvider } from "@app/server/plugins/fastify-zod"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; declare global { @@ -16,5 +18,7 @@ declare global { // used only for testing const testServer: FastifyZodProvider; const testSuperAdminDAL: TSuperAdminDALFactory; + const testKmsRootConfigDAL: TKmsRootConfigDALFactory; + const testHsmService: THsmServiceFactory; const jwtAuthToken: string; } diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f4951d8f8..273e6d982 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -48,6 +48,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; @@ -182,6 +183,8 @@ declare module "fastify" { type: ActorType; id: string; orgId: string; + parentOrgId: string; + rootOrgId: string; }; rateLimits: RateLimitConfiguration; // passport data @@ -335,6 +338,7 @@ declare module "fastify" { additionalPrivilege: TAdditionalPrivilegeServiceFactory; role: TRoleServiceFactory; convertor: TConvertorServiceFactory; + subOrganization: TSubOrgServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts index 09a346abb..2fbf68128 100644 --- a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts +++ b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts @@ -3,13 +3,14 @@ import { Knex } from "knex"; import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; export async function up(knex: Knex): Promise { @@ -25,10 +26,12 @@ export async function up(knex: Knex): Promise { if (hasUrl) t.string("url").nullable().alter(); }); } - initLogger(); + + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts index 94e30a7b8..179cb9bd6 100644 --- a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts +++ b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts @@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; export async function up(knex: Knex): Promise { @@ -30,8 +31,12 @@ export async function up(knex: Knex): Promise { } initLogger(); + + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts index bbda48dac..aef429ab9 100644 --- a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts +++ b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts @@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; export async function up(knex: Knex): Promise { @@ -24,8 +25,11 @@ export async function up(knex: Knex): Promise { } initLogger(); + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts index a24bfdf0c..f3fa63028 100644 --- a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts +++ b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts @@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; const reencryptIdentityK8sAuth = async (knex: Knex) => { @@ -55,9 +56,11 @@ const reencryptIdentityK8sAuth = async (knex: Knex) => { } initLogger(); - const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts index 25db615fa..f970043f0 100644 --- a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts +++ b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts @@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; const reencryptIdentityOidcAuth = async (knex: Knex) => { @@ -35,8 +36,11 @@ const reencryptIdentityOidcAuth = async (knex: Knex) => { } initLogger(); + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts index 783693da6..62b4e8556 100644 --- a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts +++ b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts @@ -4,16 +4,18 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; -const reencryptSamlConfig = async (knex: Knex) => { +const reencryptSamlConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => { const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); @@ -28,10 +30,6 @@ const reencryptSamlConfig = async (knex: Knex) => { } initLogger(); - const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); - const keyStore = inMemoryKeyStore(); - const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = createCircularCache>>(25); @@ -159,7 +157,7 @@ const reencryptSamlConfig = async (knex: Knex) => { } }; -const reencryptLdapConfig = async (knex: Knex) => { +const reencryptLdapConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => { const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); @@ -194,10 +192,6 @@ const reencryptLdapConfig = async (knex: Knex) => { } initLogger(); - const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); - const keyStore = inMemoryKeyStore(); - const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = createCircularCache>>(25); @@ -323,7 +317,7 @@ const reencryptLdapConfig = async (knex: Knex) => { } }; -const reencryptOidcConfig = async (knex: Knex) => { +const reencryptOidcConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => { const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn( TableName.OidcConfig, @@ -354,10 +348,6 @@ const reencryptOidcConfig = async (knex: Knex) => { } initLogger(); - const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); - const keyStore = inMemoryKeyStore(); - const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = createCircularCache>>(25); @@ -462,9 +452,18 @@ const reencryptOidcConfig = async (knex: Knex) => { }; export async function up(knex: Knex): Promise { - await reencryptSamlConfig(knex); - await reencryptLdapConfig(knex); - await reencryptOidcConfig(knex); + initLogger(); + + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + await reencryptSamlConfig(knex, kmsService); + await reencryptLdapConfig(knex, kmsService); + await reencryptOidcConfig(knex, kmsService); } const dropSamlConfigColumns = async (knex: Knex) => { diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts index a0985471f..dd9ff2d6a 100644 --- a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -3,12 +3,13 @@ import { Knex } from "knex"; import { inMemoryKeyStore } from "@app/keystore/memory"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; // Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed. // In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely. @@ -40,8 +41,10 @@ export async function up(knex: Knex): Promise { ); initLogger(); + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts index 548d6207a..f2bc0a96a 100644 --- a/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts +++ b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts @@ -2,19 +2,23 @@ import { Knex } from "knex"; import { inMemoryKeyStore } from "@app/keystore/memory"; import { selectAllTableCols } from "@app/lib/knex"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; export async function up(knex: Knex) { const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin) .select(selectAllTableCols(TableName.SuperAdmin)) .whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`); + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts index a70dcb8b9..82fa4a039 100644 --- a/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts +++ b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts @@ -2,13 +2,14 @@ import { Knex } from "knex"; import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto } from "@app/lib/crypto/cryptography"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; -import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; -import { getMigrationEncryptionServices } from "./utils/services"; +import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services"; const BATCH_SIZE = 500; export async function up(knex: Knex): Promise { @@ -25,8 +26,10 @@ export async function up(knex: Knex): Promise { }); if (!hasEncryptedCredentials) { + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); @@ -131,8 +134,11 @@ export async function down(knex: Knex): Promise { const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials"); if (hasEncryptedCredentials) { + const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() }); + const superAdminDAL = superAdminDALFactory(knex); - const envConfig = await getMigrationEnvConfig(superAdminDAL); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/20251015042917_pam-account-rotation.ts b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts new file mode 100644 index 000000000..b83dae0ae --- /dev/null +++ b/backend/src/db/migrations/20251015042917_pam-account-rotation.ts @@ -0,0 +1,49 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled"))) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.boolean("rotationEnabled").notNullable().defaultTo(false); + }); + } + if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds"))) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.integer("rotationIntervalSeconds").nullable(); + }); + } + if (!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.timestamp("lastRotatedAt").nullable(); + }); + } + if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.binary("encryptedRotationAccountCredentials").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials")) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.dropColumn("encryptedRotationAccountCredentials"); + }); + } + if (await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("rotationEnabled"); + }); + } + if (await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("rotationIntervalSeconds"); + }); + } + if (await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt")) { + await knex.schema.alterTable(TableName.PamAccount, (t) => { + t.dropColumn("lastRotatedAt"); + }); + } +} diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts new file mode 100644 index 000000000..6378fa66a --- /dev/null +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -0,0 +1,68 @@ +import { Knex } from "knex"; + +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; + +import { AccessScope, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + if (!hasParentOrgId) { + await knex.schema.alterTable(TableName.Organization, async (t) => { + // the one just above the chain + t.uuid("parentOrgId"); + t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + // this would root organization containing various informations like billing etc + t.uuid("rootOrgId"); + t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex); + t.unique(["rootOrgId", "parentOrgId", "slug"]); + }); + + // had to switch to raw for null not distinct + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (!hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + + await knex.raw( + ` + UPDATE ?? AS identity + SET "orgId" = membership."scopeOrgId" + FROM ?? AS membership + WHERE + membership."actorIdentityId" = identity."id" + AND membership."scope" = ? +`, + [TableName.Identity, TableName.Membership, AccessScope.Organization] + ); + + await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]); + + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + const hasRootOrgId = await knex.schema.hasColumn(TableName.Organization, "rootOrgId"); + if (hasParentOrgId || hasRootOrgId) { + await knex.schema.alterTable(TableName.Organization, (t) => { + if (hasParentOrgId) t.dropColumn("parentOrgId"); + if (hasRootOrgId) t.dropColumn("rootOrgId"); + }); + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.dropColumn("orgId"); + }); + } +} diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts index de32f4db9..3a08f0123 100644 --- a/backend/src/db/migrations/utils/env-config.ts +++ b/backend/src/db/migrations/utils/env-config.ts @@ -1,7 +1,10 @@ import { z } from "zod"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { crypto } from "@app/lib/crypto/cryptography"; +import { removeTrailingSlash } from "@app/lib/fn"; import { zpStr } from "@app/lib/zod"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; const envSchema = z @@ -22,13 +25,17 @@ const envSchema = z HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), HSM_KEY_LABEL: zpStr(z.string().optional()), - HSM_SLOT: z.coerce.number().optional().default(0) + HSM_SLOT: z.coerce.number().optional().default(0), + + LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), + LICENSE_SERVER_KEY: zpStr(z.string().optional()), + LICENSE_KEY: zpStr(z.string().optional()), + LICENSE_KEY_OFFLINE: zpStr(z.string().optional()), + INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional()), + + SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional() }) // To ensure that basic encryption is always possible. - .refine( - (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), - "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." - ) .transform((data) => ({ ...data, isHsmConfigured: @@ -37,7 +44,27 @@ const envSchema = z export type TMigrationEnvConfig = z.infer; -export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory) => { +export const getMigrationHsmConfig = () => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + console.error("Invalid environment variables. Check the error below"); + console.error(parsedEnv.error.issues); + process.exit(-1); + } + return { + isHsmConfigured: parsedEnv.data.isHsmConfigured, + HSM_PIN: parsedEnv.data.HSM_PIN, + HSM_SLOT: parsedEnv.data.HSM_SLOT, + HSM_LIB_PATH: parsedEnv.data.HSM_LIB_PATH, + HSM_KEY_LABEL: parsedEnv.data.HSM_KEY_LABEL + }; +}; + +export const getMigrationEnvConfig = async ( + superAdminDAL: TSuperAdminDALFactory, + hsmService: THsmServiceFactory, + kmsRootConfigDAL: TKmsRootConfigDALFactory +) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { // eslint-disable-next-line no-console @@ -53,7 +80,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory let envCfg = Object.freeze(parsedEnv.data); - const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg); + const fipsEnabled = await crypto.initialize(superAdminDAL, hsmService, kmsRootConfigDAL, envCfg); // Fix for 128-bit entropy encryption key expansion issue: // In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY. diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts index 0e071e6fe..cd3e5ac23 100644 --- a/backend/src/db/migrations/utils/services.ts +++ b/backend/src/db/migrations/utils/services.ts @@ -1,28 +1,23 @@ import { Knex } from "knex"; -import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { initializeHsmModule, isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns"; import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { licenseDALFactory } from "@app/ee/services/license/license-dal"; +import { licenseServiceFactory } from "@app/ee/services/license/license-service"; +import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; +import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal"; -import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal"; -import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; -import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; -import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal"; -import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal"; -import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal"; +import { BadRequestError } from "@app/lib/errors"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { orgDALFactory } from "@app/services/org/org-dal"; import { projectDALFactory } from "@app/services/project/project-dal"; -import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; -import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; -import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal"; -import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; -import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal"; -import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; +import { roleDALFactory } from "@app/services/role/role-dal"; +import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { userDALFactory } from "@app/services/user/user-dal"; import { TMigrationEnvConfig } from "./env-config"; @@ -33,8 +28,11 @@ type TDependencies = { keyStore: TKeyStoreFactory; }; -export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { - // eslint-disable-next-line no-param-reassign +type THsmServiceDependencies = { + envConfig: Pick; +}; + +export const getMigrationHsmService = async ({ envConfig }: THsmServiceDependencies) => { const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); @@ -43,67 +41,72 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore } envConfig }); - const orgDAL = orgDALFactory(db); - const kmsRootConfigDAL = kmsRootConfigDALFactory(db); - const kmsDAL = kmskeyDALFactory(db); - const internalKmsDAL = internalKmsDALFactory(db); - const projectDAL = projectDALFactory(db); - - const kmsService = kmsServiceFactory({ - kmsRootConfigDAL, - keyStore, - kmsDAL, - internalKmsDAL, - orgDAL, - projectDAL, - hsmService, - envConfig - }); - await hsmService.startService(); - await kmsService.startService(); - return { kmsService }; + return { hsmService }; }; -export const getMigrationPITServices = async ({ - db, - keyStore, - envConfig -}: { - db: Knex; - keyStore: TKeyStoreFactory; - envConfig: TMigrationEnvConfig; -}) => { +export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { + // ----- DAL dependencies ----- + const orgDAL = orgDALFactory(db); + const licenseDAL = licenseDALFactory(db); + const permissionDAL = permissionDALFactory(db); const projectDAL = projectDALFactory(db); - const folderCommitDAL = folderCommitDALFactory(db); - const folderCommitChangesDAL = folderCommitChangesDALFactory(db); - const folderCheckpointDAL = folderCheckpointDALFactory(db); - const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db); + const roleDAL = roleDALFactory(db); const userDAL = userDALFactory(db); const identityDAL = identityDALFactory(db); - const folderDAL = secretFolderDALFactory(db); - const folderVersionDAL = secretFolderVersionDALFactory(db); - const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db); - const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db); - const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore }); - const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db); - const secretTagDAL = secretTagDALFactory(db); - - const orgDAL = orgDALFactory(db); + const serviceTokenDAL = serviceTokenDALFactory(db); const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const kmsDAL = kmskeyDALFactory(db); const internalKmsDAL = internalKmsDALFactory(db); - const resourceMetadataDAL = resourceMetadataDALFactory(db); - const hsmModule = initializeHsmModule(envConfig); - hsmModule.initialize(); + // ----- Service dependencies ----- + const permissionService = permissionServiceFactory({ + permissionDAL, + serviceTokenDAL, + projectDAL, + keyStore, + roleDAL, + userDAL, + identityDAL + }); - const hsmService = hsmServiceFactory({ - hsmModule: hsmModule.getModule(), + const licenseService = licenseServiceFactory({ + permissionService, + orgDAL, + licenseDAL, + keyStore, + projectDAL, envConfig }); + // ----- HSM startup ----- + + const { hsmService } = await getMigrationHsmService({ envConfig }); + + const hsmStatus = await isHsmActiveAndEnabled({ + hsmService, + kmsRootConfigDAL, + licenseService + }); + + // if the encryption strategy is software - user needs to provide an encryption key + // if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key + const needsEncryptionKey = + hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software || + (hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured); + + if (needsEncryptionKey) { + if (!envConfig.ROOT_ENCRYPTION_KEY && !envConfig.ENCRYPTION_KEY) { + throw new BadRequestError({ + message: + "Root KMS encryption strategy is set to software. Please set the ENCRYPTION_KEY environment variable and restart your deployment.\nYou can enable HSM encryption in the Server Console." + }); + } + } + + // ----- KMS startup ----- + const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, @@ -115,27 +118,7 @@ export const getMigrationPITServices = async ({ envConfig }); - await hsmService.startService(); - await kmsService.startService(); + await kmsService.startService(hsmStatus); - const folderCommitService = folderCommitServiceFactory({ - folderCommitDAL, - folderCommitChangesDAL, - folderCheckpointDAL, - folderTreeCheckpointDAL, - userDAL, - identityDAL, - folderDAL, - folderVersionDAL, - secretVersionV2BridgeDAL, - projectDAL, - folderCheckpointResourcesDAL, - secretV2BridgeDAL, - folderTreeCheckpointResourcesDAL, - kmsService, - secretTagDAL, - resourceMetadataDAL - }); - - return { folderCommitService }; + return { kmsService, hsmService }; }; diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index a592e2480..06c37ff22 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({ authMethod: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - hasDeleteProtection: z.boolean().default(false) + hasDeleteProtection: z.boolean().default(false), + orgId: z.string().uuid() }); export type TIdentities = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 28e9471ad..86bc929b8 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -316,6 +316,12 @@ export enum ActionProjectType { Any = "any" } +export enum OrganizationActionScope { + ChildOrganization = "child-organization-only", + ParentOrganization = "parent-organization-only", + Any = "any" +} + export enum TemporaryPermissionMode { Relative = "relative" } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index afc9e2b73..a1c01151f 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -38,7 +38,9 @@ export const OrganizationsSchema = z.object({ maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(), googleSsoAuthEnforced: z.boolean().default(false), - googleSsoAuthLastUsed: z.date().nullable().optional() + googleSsoAuthLastUsed: z.date().nullable().optional(), + parentOrgId: z.string().uuid().nullable().optional(), + rootOrgId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/pam-accounts.ts b/backend/src/db/schemas/pam-accounts.ts index 5a9a45617..7e78e0874 100644 --- a/backend/src/db/schemas/pam-accounts.ts +++ b/backend/src/db/schemas/pam-accounts.ts @@ -18,7 +18,10 @@ export const PamAccountsSchema = z.object({ description: z.string().nullable().optional(), encryptedCredentials: zodBuffer, createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + rotationEnabled: z.boolean().default(false), + rotationIntervalSeconds: z.number().nullable().optional(), + lastRotatedAt: z.date().nullable().optional() }); export type TPamAccounts = z.infer; diff --git a/backend/src/db/schemas/pam-resources.ts b/backend/src/db/schemas/pam-resources.ts index d34017d0f..325f6eddc 100644 --- a/backend/src/db/schemas/pam-resources.ts +++ b/backend/src/db/schemas/pam-resources.ts @@ -17,7 +17,8 @@ export const PamResourcesSchema = z.object({ resourceType: z.string(), encryptedConnectionDetails: zodBuffer, createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedRotationAccountCredentials: zodBuffer.nullable().optional() }); export type TPamResources = z.infer; diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index 43ce4dadf..9f42ef12b 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -1,7 +1,10 @@ import { Knex } from "knex"; -import { initEnvConfig } from "@app/lib/config/env"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { getHsmConfig, initEnvConfig } from "@app/lib/config/env"; import { initLogger, logger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { AuthMethod } from "../../services/auth/auth-type"; @@ -17,7 +20,21 @@ export async function seed(knex: Knex): Promise { initLogger(); const superAdminDAL = superAdminDALFactory(knex); - await initEnvConfig(superAdminDAL, logger); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + + const hsmConfig = getHsmConfig(logger); + + const hsmModule = initializeHsmModule(hsmConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig: hsmConfig + }); + + await hsmService.startService(); + + await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger); await knex(TableName.SuperAdmin).insert([ // eslint-disable-next-line diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index d0294022f..99083ab94 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -1,11 +1,14 @@ import { Knex } from "knex"; -import { initEnvConfig } from "@app/lib/config/env"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { getHsmConfig, initEnvConfig } from "@app/lib/config/env"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { initLogger, logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; @@ -192,7 +195,21 @@ export async function seed(knex: Knex): Promise { initLogger(); const superAdminDAL = superAdminDALFactory(knex); - await initEnvConfig(superAdminDAL, logger); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + + const hsmConfig = getHsmConfig(logger); + + const hsmModule = initializeHsmModule(hsmConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig: hsmConfig + }); + + await hsmService.startService(); + + await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger); const [project] = await knex(TableName.Project) .insert({ diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 333fc7e3a..4e4e3eb7f 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -1,8 +1,11 @@ import { Knex } from "knex"; -import { initEnvConfig } from "@app/lib/config/env"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { getHsmConfig, initEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger, logger } from "@app/lib/logger"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; @@ -15,7 +18,20 @@ export async function seed(knex: Knex): Promise { initLogger(); const superAdminDAL = superAdminDALFactory(knex); - await initEnvConfig(superAdminDAL, logger); + const kmsRootConfigDAL = kmsRootConfigDALFactory(knex); + const hsmConfig = getHsmConfig(logger); + + const hsmModule = initializeHsmModule(hsmConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig: hsmConfig + }); + + await hsmService.startService(); + + await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger); // Inserts seed entries await knex(TableName.Identity).insert([ @@ -24,7 +40,8 @@ export async function seed(knex: Knex): Promise { // @ts-ignore id: seedData1.machineIdentity.id, name: seedData1.machineIdentity.name, - authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + orgId: seedData1.organization.id } ]); const identityUa = await knex(TableName.IdentityUniversalAuth) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 42392ba55..31847b503 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -48,12 +48,14 @@ import { registerSshCertRouter } from "./ssh-certificate-router"; import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router"; import { registerSshHostGroupRouter } from "./ssh-host-group-router"; import { registerSshHostRouter } from "./ssh-host-router"; +import { registerSubOrgRouter } from "./sub-org-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization await server.register(registerOrgRoleRouter, { prefix: "/organization" }); + await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" }); await server.register(registerLicenseRouter, { prefix: "/organizations" }); // depreciated in favour of infisical workspace diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 17923975d..2ccdce93a 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -58,7 +58,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const plan = await server.services.license.getOrgPlan({ actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, + actorOrgId: req.permission.rootOrgId, actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, refreshCache: req.query.refreshCache diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 5a8f03038..591458bb4 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -3,12 +3,35 @@ import { z } from "zod"; import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionSchema, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const INVALID_SUBORG_PERMISSIONS = [ + OrgPermissionSubjects.Sso, + OrgPermissionSubjects.Ldap, + OrgPermissionSubjects.Scim, + OrgPermissionSubjects.GithubOrgSync, + OrgPermissionSubjects.GithubOrgSyncManual, + OrgPermissionSubjects.Billing, + OrgPermissionSubjects.SubOrganization +]; + +const validateSubOrganizationSubjects = (permissions: unknown) => { + const invalidPermissionSubjects = (permissions as { subject: OrgPermissionSubjects }[]) + .filter((el) => INVALID_SUBORG_PERMISSIONS.includes(el.subject)) + .map((el) => el.subject); + if (invalidPermissionSubjects.length) { + const deduplication = Array.from(new Set(invalidPermissionSubjects)); + throw new BadRequestError({ + message: `Suborganization contains invalid permission subjects: ${deduplication.join(",")}` + }); + } +}; + export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -37,6 +60,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); const role = await server.services.role.createRole({ permission: req.permission, @@ -133,6 +161,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization && req.body.permissions) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; const role = await server.services.role.updateRole({ permission: req.permission, diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts index 0ed7e238a..44e2a5ea1 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -22,11 +22,15 @@ export const registerPamResourceEndpoints = ({ folderId?: C["folderId"]; name: C["name"]; description?: C["description"]; + rotationEnabled: C["rotationEnabled"]; + rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; updateAccountSchema: z.ZodType<{ credentials?: C["credentials"]; name?: C["name"]; description?: C["description"]; + rotationEnabled?: C["rotationEnabled"]; + rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; accountResponseSchema: z.ZodTypeAny; }) => { @@ -60,7 +64,9 @@ export const registerPamResourceEndpoints = ({ resourceType, folderId: req.body.folderId, name: req.body.name, - description: req.body.description + description: req.body.description, + rotationEnabled: req.body.rotationEnabled, + rotationIntervalSeconds: req.body.rotationIntervalSeconds } } }); @@ -108,7 +114,9 @@ export const registerPamResourceEndpoints = ({ resourceId: account.resourceId, resourceType, name: req.body.name, - description: req.body.description + description: req.body.description, + rotationEnabled: req.body.rotationEnabled, + rotationIntervalSeconds: req.body.rotationIntervalSeconds } } }); diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index a63b67d94..6b53781ae 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -1,7 +1,7 @@ import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { CreatePostgresResourceSchema, - PostgresResourceSchema, + SanitizedPostgresResourceSchema, UpdatePostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; @@ -12,7 +12,7 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record({ connectionDetails: T["connectionDetails"]; gatewayId: T["gatewayId"]; name: T["name"]; + rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; updateResourceSchema: z.ZodType<{ connectionDetails?: T["connectionDetails"]; gatewayId?: T["gatewayId"]; name?: T["name"]; + rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; resourceResponseSchema: z.ZodTypeAny; }) => { diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index c19c2030d..d42a73021 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -3,14 +3,14 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PostgresResourceListItemSchema, - PostgresResourceSchema + SanitizedPostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; // Use z.union([...]) when more resources are added -const ResourceSchema = PostgresResourceSchema; +const SanitizedResourceSchema = SanitizedPostgresResourceSchema; const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]); @@ -50,7 +50,7 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - resources: ResourceSchema.array() + resources: SanitizedResourceSchema.array() }) } }, diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts new file mode 100644 index 000000000..200130488 --- /dev/null +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -0,0 +1,163 @@ +import { z } from "zod"; + +import { OrganizationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({ + id: true, + name: true, + slug: true, + createdAt: true, + updatedAt: true, + parentOrgId: true +}); + +export const registerSubOrgRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Create a sub organization", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.CREATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.createSubOrg({ + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "List of sub organizations", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit), + offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset), + isAccessible: z + .enum(["true", "false"]) + .optional() + .transform((value) => value === "true") + .describe(SUB_ORGANIZATIONS.LIST.isAccessible) + }), + response: { + 200: z.object({ + organizations: sanitizedSubOrganizationSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organizations } = await server.services.subOrganization.listSubOrgs({ + permissionActor: req.permission, + data: { + limit: req.query.limit, + offset: req.query.offset, + isAccessible: req.query.isAccessible + } + }); + + return { organizations }; + } + }); + + server.route({ + method: "PATCH", + url: "/:subOrgId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Update a sub organization", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.UPDATE.subOrgId) + }), + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.UPDATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.updateSubOrg({ + subOrgId: req.params.subOrgId, + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 5dd0fd4ba..b3cd34ac9 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { TAuditLogs } from "@app/db/schemas"; +import { OrganizationActionScope, TAuditLogs } from "@app/db/schemas"; import { decryptLogStream, decryptLogStreamCredentials, @@ -45,13 +45,14 @@ export const auditLogStreamServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -94,13 +95,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -160,13 +162,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -185,14 +188,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); - - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - logStream.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -206,13 +209,14 @@ export const auditLogStreamServiceFactory = ({ }; const list = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); 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 ece5edaf9..eab7792f8 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; @@ -47,13 +47,14 @@ export const auditLogServiceFactory = ({ ); } else { // Organization-wide logs - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAuditLogsActions.Read, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index f3c95e434..6ceaa7778 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,6 +173,9 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", + CREATE_SUB_ORGANIZATION = "create-sub-organization", + UPDATE_SUB_ORGANIZATION = "update-sub-organization", + ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", @@ -524,6 +527,8 @@ export enum EventType { PAM_ACCOUNT_CREATE = "pam-account-create", PAM_ACCOUNT_UPDATE = "pam-account-update", PAM_ACCOUNT_DELETE = "pam-account-delete", + PAM_ACCOUNT_CREDENTIAL_ROTATION = "pam-account-credential-rotation", + PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED = "pam-account-credential-rotation-failed", PAM_RESOURCE_LIST = "pam-resource-list", PAM_RESOURCE_GET = "pam-resource-get", PAM_RESOURCE_CREATE = "pam-resource-create", @@ -616,6 +621,22 @@ interface GetSecretsEvent { }; } +interface CreateSubOrganizationEvent { + type: EventType.CREATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + +interface UpdateSubOrganizationEvent { + type: EventType.UPDATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + type TSecretMetadata = { key: string; value: string }[]; interface GetSecretEvent { @@ -3896,6 +3917,8 @@ interface PamAccountCreateEvent { folderId?: string | null; name: string; description?: string | null; + rotationEnabled: boolean; + rotationIntervalSeconds?: number | null; }; } @@ -3907,6 +3930,8 @@ interface PamAccountUpdateEvent { resourceType: string; name?: string; description?: string | null; + rotationEnabled?: boolean; + rotationIntervalSeconds?: number | null; }; } @@ -3920,6 +3945,27 @@ interface PamAccountDeleteEvent { }; } +interface PamAccountCredentialRotationEvent { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION; + metadata: { + accountName: string; + accountId: string; + resourceId: string; + resourceType: string; + }; +} + +interface PamAccountCredentialRotationFailedEvent { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED; + metadata: { + accountName: string; + accountId: string; + resourceId: string; + resourceType: string; + errorMessage: string; + }; +} + interface PamResourceListEvent { type: EventType.PAM_RESOURCE_LIST; metadata: { @@ -3964,6 +4010,8 @@ interface PamResourceDeleteEvent { } export type Event = + | CreateSubOrganizationEvent + | UpdateSubOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent @@ -4319,6 +4367,8 @@ export type Event = | PamAccountCreateEvent | PamAccountUpdateEvent | PamAccountDeleteEvent + | PamAccountCredentialRotationEvent + | PamAccountCredentialRotationFailedEvent | PamResourceListEvent | PamResourceGetEvent | PamResourceCreateEvent diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 659e07bca..d2d683a84 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -134,13 +134,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - gateway?.orgId ?? gatewayv2?.orgId, + orgId: gateway?.orgId || gatewayv2?.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, @@ -297,13 +298,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: gateway?.orgId || gatewayv2?.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index d515c5973..9614f3298 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -3,6 +3,7 @@ import { STSServiceException } from "@aws-sdk/client-sts"; import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; @@ -51,13 +52,14 @@ export const externalKmsServiceFactory = ({ actorOrgId, actorAuthMethod }: TCreateExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(actorOrgId); @@ -154,13 +156,14 @@ export const externalKmsServiceFactory = ({ actorAuthMethod }: TUpdateExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(kmsDoc.orgId); @@ -257,13 +260,14 @@ export const externalKmsServiceFactory = ({ const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); @@ -278,13 +282,14 @@ export const externalKmsServiceFactory = ({ }; const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); @@ -294,13 +299,14 @@ export const externalKmsServiceFactory = ({ const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); @@ -342,13 +348,14 @@ export const externalKmsServiceFactory = ({ name: kmsName }: TGetExternalKmsBySlugDTO) => { const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index a2d323790..fd4954a00 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -3,7 +3,7 @@ import net from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -73,13 +73,14 @@ export const gatewayV2ServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -258,13 +259,14 @@ export const gatewayV2ServiceFactory = ({ }; const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, @@ -815,13 +817,14 @@ export const gatewayV2ServiceFactory = ({ throw new NotFoundError({ message: `Gateway ${id} not found` }); } - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - gateway.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: gateway.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, @@ -845,13 +848,14 @@ export const gatewayV2ServiceFactory = ({ }; const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 5c8ad80bf..261640cc9 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { z } from "zod"; +import { OrganizationActionScope } from "@app/db/schemas"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -68,13 +69,14 @@ export const gatewayServiceFactory = ({ "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway @@ -480,13 +482,14 @@ export const gatewayServiceFactory = ({ }; const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -501,13 +504,14 @@ export const gatewayServiceFactory = ({ }; const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -521,13 +525,14 @@ export const gatewayServiceFactory = ({ }; const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway @@ -542,13 +547,14 @@ export const gatewayServiceFactory = ({ }; const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index b2bcb4ef3..d2713f269 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -6,7 +6,7 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; import { Octokit as OctokitRest } from "@octokit/rest"; import RE2 from "re2"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -104,13 +104,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TCreateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -162,13 +163,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TUpdateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + scope: OrganizationActionScope.ParentOrganization, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -226,13 +228,14 @@ export const githubOrgSyncServiceFactory = ({ }; const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); @@ -256,13 +259,14 @@ export const githubOrgSyncServiceFactory = ({ }; const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -422,13 +426,14 @@ export const githubOrgSyncServiceFactory = ({ }; const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -509,13 +514,14 @@ export const githubOrgSyncServiceFactory = ({ }; const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorId: orgPermission.id, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Edit, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 075488488..956d7853a 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -73,13 +73,14 @@ export const groupServiceFactory = ({ const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -167,13 +168,14 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); @@ -270,13 +272,14 @@ export const groupServiceFactory = ({ const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -297,17 +300,18 @@ export const groupServiceFactory = ({ const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findById(id); - if (!group) { + if (!group || group.orgId !== actorOrgId) { throw new NotFoundError({ message: `Cannot find group with ID ${id}` }); @@ -330,13 +334,14 @@ export const groupServiceFactory = ({ }: TListGroupUsersDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findOne({ @@ -365,13 +370,14 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists @@ -451,13 +457,14 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 1afccdafe..400fa31e9 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,8 +1,14 @@ import * as pkcs11js from "pkcs11js"; import { TEnvConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; +import { TLicenseServiceFactory } from "../license/license-service"; +import { THsmServiceFactory } from "./hsm-service"; import { HsmModule } from "./hsm-types"; export const initializeHsmModule = (envConfig: Pick) => { @@ -25,10 +31,9 @@ export const initializeHsmModule = (envConfig: Pick; + kmsRootConfigDAL: Pick; + licenseService?: Pick; +}) => { + const isHsmConfigured = await hsmService.isActive(); + + // null if the root kms config does not exist + let rootKmsConfigEncryptionStrategy: RootKeyEncryptionStrategy | null = null; + + const rootKmsConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID).catch(() => null); + + rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null; + if ( + rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM && + licenseService && + !licenseService.onPremFeatures.hsm + ) { + throw new BadRequestError({ + message: "Your license does not include HSM integration. Please upgrade to the Enterprise plan to use HSM." + }); + } + + return { + rootKmsConfigEncryptionStrategy, + isHsmConfigured + }; +}; diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 0ed4c5faf..1207b1cd3 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -25,6 +25,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon const AES_KEY_SIZE = 256; const HMAC_KEY_SIZE = 256; + let pkcs11TestPassed = false; + const $withSession = async (callbackWithSession: SessionCallback): Promise => { const RETRY_INTERVAL = 200; // 200ms between attempts const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time @@ -363,7 +365,9 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon return false; } - let pkcs11TestPassed = false; + if (pkcs11TestPassed) { + return true; + } try { pkcs11TestPassed = await $withSession($testPkcs11Module); @@ -371,7 +375,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon logger.error(err, "HSM: Error testing PKCS#11 module"); } - return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; + return pkcs11TestPassed; }; const startService = async () => { @@ -460,10 +464,23 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon } }; + const randomBytes = async (length: number) => { + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); + } + + const randomData = await $withSession((sessionHandle) => + pkcs11.C_GenerateRandom(sessionHandle, Buffer.alloc(length)) + ); + + return randomData; + }; + return { encrypt, startService, isActive, - decrypt + decrypt, + randomBytes }; }; diff --git a/backend/src/ee/services/hsm/hsm-types.ts b/backend/src/ee/services/hsm/hsm-types.ts index b688147f5..ada527329 100644 --- a/backend/src/ee/services/hsm/hsm-types.ts +++ b/backend/src/ee/services/hsm/hsm-types.ts @@ -1,5 +1,7 @@ import pkcs11js from "pkcs11js"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; + export type HsmModule = { pkcs11: pkcs11js.PKCS11; isInitialized: boolean; @@ -9,3 +11,8 @@ export enum HsmKeyType { AES = "AES", HMAC = "hmac" } + +export type THsmStatus = { + rootKmsConfigEncryptionStrategy: RootKeyEncryptionStrategy | null; + isHsmConfigured: boolean; +}; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts index ef071742d..10aa3b190 100644 --- a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { @@ -68,13 +69,14 @@ export const identityAuthTemplateServiceFactory = ({ templateFields: Record; } & Omit) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -113,13 +115,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -227,13 +230,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -282,13 +286,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -316,13 +321,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TListIdentityAuthTemplatesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -352,13 +358,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TGetTemplatesByAuthMethodDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -385,13 +392,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TFindTemplateUsagesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -415,13 +423,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TUnlinkTemplateUsageDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 27f59a99f..b3eace6bc 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -46,13 +47,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipCreateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -78,13 +80,14 @@ export const kmipOperationServiceFactory = ({ }; const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -131,13 +134,14 @@ export const kmipOperationServiceFactory = ({ }; const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -189,13 +193,14 @@ export const kmipOperationServiceFactory = ({ }; const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -226,13 +231,14 @@ export const kmipOperationServiceFactory = ({ }; const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -287,13 +293,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipGetAttributesDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -339,13 +346,14 @@ export const kmipOperationServiceFactory = ({ }; const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -377,13 +385,14 @@ export const kmipOperationServiceFactory = ({ actorOrgId, kmipMetadata }: TKmipRegisterDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 8daa5a37a..482eb41be 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { isValidIp } from "@app/lib/ip"; @@ -401,13 +401,14 @@ export const kmipServiceFactory = ({ }; const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); const kmipConfig = await kmipOrgConfigDAL.findOne({ @@ -566,7 +567,14 @@ export const kmipServiceFactory = ({ }; const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { - await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); const kmipConfig = await kmipOrgConfigDAL.findOne({ orgId: actorOrgId @@ -759,13 +767,14 @@ export const kmipServiceFactory = ({ keyAlgorithm, hostnamesOrIps }: TRegisterServerDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 43ca5ab3d..86bfcc687 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,7 +1,14 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrganizationActionScope, + OrgMembershipStatus, + TableName, + TLdapConfigsUpdate, + TUsers +} from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -119,7 +126,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TCreateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -238,7 +252,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TUpdateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -316,7 +337,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); return getLdapCfg({ orgId @@ -649,7 +677,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapGroupMapsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); const ldapConfig = await ldapConfigDAL.findOne({ @@ -678,7 +713,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -732,7 +774,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -771,7 +820,14 @@ export const ldapConfigServiceFactory = ({ caCert, url }: TTestLdapConnectionDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index a2bd7ec51..853f3a994 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { const doc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .andWhere((bd) => { if (orgId) { @@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); return Number(doc?.[0]?.count ?? 0); } catch (error) { @@ -25,10 +27,31 @@ export const licenseDALFactory = (db: TDbClient) => { } }; + const countOfOrgIdentities = async (orgId: string | null, tx?: Knex) => { + try { + // count org identities + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) + .where((bd) => { + if (orgId) { + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); + } + }) + .count(); + + const identityCount = Number(identityDoc?.[0].count); + + return identityCount; + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Users + Identities" }); + } + }; + const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users const userDoc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .whereNotNull(`${TableName.Membership}.actorUserId`) .andWhere((bd) => { @@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db.replicaNode())(TableName.Membership) - .where({ scope: AccessScope.Organization }) - .whereNotNull(`${TableName.Membership}.actorIdentityId`) + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) .where((bd) => { if (orgId) { - void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); } }) .count(); @@ -61,5 +84,5 @@ export const licenseDALFactory = (db: TDbClient) => { } }; - return { countOfOrgMembers, countOrgUsersAndIdentities }; + return { countOfOrgMembers, countOrgUsersAndIdentities, countOfOrgIdentities }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index aba2c5e78..97061e3ca 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, githubOrgSync: false, customRateLimits: false, + subOrganization: false, customAlerts: false, secretAccessInsights: false, auditLogs: false, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index fba9d0cca..bb56d4df4 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -9,12 +9,12 @@ import { AxiosError } from "axios"; import { CronJob } from "cron"; import { Knex } from "knex"; +import { OrganizationActionScope } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -45,11 +45,14 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + envConfig: Pick< + TEnvConfig, + "LICENSE_SERVER_URL" | "LICENSE_SERVER_KEY" | "LICENSE_KEY" | "LICENSE_KEY_OFFLINE" | "INTERNAL_REGION" | "SITE_URL" + >; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; - identityOrgMembershipDAL: TIdentityOrgDALFactory; projectDAL: TProjectDALFactory; }; @@ -66,27 +69,26 @@ export const licenseServiceFactory = ({ permissionService, licenseDAL, keyStore, - identityOrgMembershipDAL, - projectDAL + projectDAL, + envConfig }: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); let selfHostedLicense: TOfflineLicense | null = null; - const appCfg = getConfig(); const licenseServerCloudApi = setupLicenseRequestWithStore( - appCfg.LICENSE_SERVER_URL || "", + envConfig.LICENSE_SERVER_URL || "", LICENSE_SERVER_CLOUD_LOGIN, - appCfg.LICENSE_SERVER_KEY || "", - appCfg.INTERNAL_REGION + envConfig.LICENSE_SERVER_KEY || "", + envConfig.INTERNAL_REGION ); const licenseServerOnPremApi = setupLicenseRequestWithStore( - appCfg.LICENSE_SERVER_URL || "", + envConfig.LICENSE_SERVER_URL || "", LICENSE_SERVER_ON_PREM_LOGIN, - appCfg.LICENSE_KEY || "", - appCfg.INTERNAL_REGION + envConfig.LICENSE_KEY || "", + envConfig.INTERNAL_REGION ); const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => { @@ -120,7 +122,7 @@ export const licenseServiceFactory = ({ const init = async () => { try { - if (appCfg.LICENSE_SERVER_KEY) { + if (envConfig.LICENSE_SERVER_KEY) { const token = await licenseServerCloudApi.refreshLicense(); if (token) instanceType = InstanceType.Cloud; logger.info(`Instance type: ${InstanceType.Cloud}`); @@ -128,7 +130,7 @@ export const licenseServiceFactory = ({ return; } - if (appCfg.LICENSE_KEY) { + if (envConfig.LICENSE_KEY) { const token = await licenseServerOnPremApi.refreshLicense(); if (token) { await syncLicenseKeyOnPremFeatures(true); @@ -139,10 +141,10 @@ export const licenseServiceFactory = ({ return; } - if (appCfg.LICENSE_KEY_OFFLINE) { + if (envConfig.LICENSE_KEY_OFFLINE) { let isValidOfflineLicense = true; const contents: TOfflineLicenseContents = JSON.parse( - Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8") + Buffer.from(envConfig.LICENSE_KEY_OFFLINE, "base64").toString("utf8") ); const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature); @@ -181,7 +183,7 @@ export const licenseServiceFactory = ({ }; const initializeBackgroundSync = async () => { - if (appCfg.LICENSE_KEY) { + if (envConfig.LICENSE_KEY) { logger.info("Setting up background sync process for refresh onPremFeatures"); const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures); job.start(); @@ -199,22 +201,23 @@ export const licenseServiceFactory = ({ return JSON.parse(cachedPlan) as TFeatureSet; } - const org = await orgDAL.findOrgById(orgId); + const org = await orgDAL.findRootOrgDetails(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const rootOrgId = org.id; + const { data: { currentPlan } } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( `/api/license-server/v1/customers/${org.customerId}/cloud-plan` ); - const workspacesUsed = await projectDAL.countOfOrgProjects(orgId); + const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId); currentPlan.workspacesUsed = workspacesUsed; - const membersUsed = await licenseDAL.countOfOrgMembers(orgId); + const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId); currentPlan.membersUsed = membersUsed; - const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); - currentPlan.identitiesUsed = identityUsed; + const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId); - if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) { + if (currentPlan?.identitiesUsed && currentPlan.identitiesUsed !== identityUsed) { try { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { quantity: membersUsed, @@ -227,6 +230,7 @@ export const licenseServiceFactory = ({ ); } } + currentPlan.identitiesUsed = identityUsed; await keyStore.setItemWithExpiry( FEATURE_CACHE_KEY(org.id), @@ -284,19 +288,20 @@ export const licenseServiceFactory = ({ }; const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { - if (instanceType === InstanceType.Cloud) { - const org = await orgDAL.findOrgById(orgId); - if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const org = await orgDAL.findRootOrgDetails(orgId, tx); + if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); - const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); + const rootOrgId = org.id; + if (instanceType === InstanceType.Cloud) { + const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx); + const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx); if (org?.customerId) { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { quantity, quantityIdentities }); } - await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); + await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId)); } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); @@ -307,7 +312,7 @@ export const licenseServiceFactory = ({ usedIdentitySeats }); } - await refreshPlan(orgId); + await refreshPlan(rootOrgId); }; // below all are api calls @@ -319,7 +324,14 @@ export const licenseServiceFactory = ({ actorAuthMethod, billingCycle }: TOrgPlansTableDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` @@ -336,7 +348,14 @@ export const licenseServiceFactory = ({ projectId, refreshCache }: TOrgPlanDTO) => { - await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); if (refreshCache) { await refreshPlan(orgId); } @@ -352,13 +371,20 @@ export const licenseServiceFactory = ({ actorAuthMethod, success_url }: TStartOrgTrialDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -384,13 +410,20 @@ export const licenseServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateOrgPortalSession) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: "Organization not found" @@ -411,8 +444,8 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`, { - success_url: `${appCfg.SITE_URL}/organization/billing`, - cancel_url: `${appCfg.SITE_URL}/organization/billing` + success_url: `${envConfig.SITE_URL}/organization/billing`, + cancel_url: `${envConfig.SITE_URL}/organization/billing` } ); @@ -425,7 +458,7 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`, { - return_url: `${appCfg.SITE_URL}/organization/billing` + return_url: `${envConfig.SITE_URL}/organization/billing` } ); @@ -433,10 +466,17 @@ export const licenseServiceFactory = ({ }; const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -502,7 +542,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), + licenseDAL.countOfOrgIdentities(orgId), projectDAL.countOfOrgProjects(orgId) ]); @@ -516,10 +556,17 @@ export const licenseServiceFactory = ({ // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -553,10 +600,17 @@ export const licenseServiceFactory = ({ }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -578,13 +632,20 @@ export const licenseServiceFactory = ({ name, email }: TUpdateOrgBillingDetailsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -601,10 +662,17 @@ export const licenseServiceFactory = ({ }; const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -628,13 +696,20 @@ export const licenseServiceFactory = ({ success_url, cancel_url }: TAddOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -660,13 +735,20 @@ export const licenseServiceFactory = ({ orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -692,10 +774,17 @@ export const licenseServiceFactory = ({ }; const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -710,13 +799,20 @@ export const licenseServiceFactory = ({ }; const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -734,13 +830,20 @@ export const licenseServiceFactory = ({ }; const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -754,10 +857,17 @@ export const licenseServiceFactory = ({ }; const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -771,10 +881,17 @@ export const licenseServiceFactory = ({ }; const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -819,7 +936,6 @@ export const licenseServiceFactory = ({ getLicenseId, invalidateGetPlan, updateSubscriptionOrgMemberCount, - refreshPlan, getOrgPlan, getOrgPlansTableByBillCycle, startOrgTrial, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 2276dcf36..93f40ae6e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -33,6 +33,7 @@ export type TFeatureSet = { membersUsed: number; identityLimit: null; identitiesUsed: number; + subOrganization: false; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index c2672a94e..e80ec7cf5 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -118,13 +118,14 @@ export const oidcConfigServiceFactory = ({ } if (dto.type === "external") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - dto.organizationId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: dto.actorId, + actor: dto.actor, + orgId: dto.organizationId, + actorOrgId: dto.actorOrgId, + actorAuthMethod: dto.actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } @@ -508,13 +509,14 @@ export const oidcConfigServiceFactory = ({ "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -602,13 +604,14 @@ export const oidcConfigServiceFactory = ({ "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -764,7 +767,14 @@ export const oidcConfigServiceFactory = ({ }; const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { - await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: actor.id, + orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); const oidcConfig = await oidcConfigDAL.findOne({ orgId, diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index b62e940fe..6ef7df76e 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -18,7 +18,8 @@ export const pamAccountDALFactory = (db: TDbClient) => { .select( // resource db.ref("name").withSchema(TableName.PamResource).as("resourceName"), - db.ref("resourceType").withSchema(TableName.PamResource) + db.ref("resourceType").withSchema(TableName.PamResource), + db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource) ); if (filter) { @@ -28,16 +29,35 @@ export const pamAccountDALFactory = (db: TDbClient) => { const accounts = await query; - return accounts.map(({ resourceId, resourceName, resourceType, ...account }) => ({ - ...account, - resourceId, - resource: { - id: resourceId, - name: resourceName, - resourceType - } - })); + return accounts.map( + ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ + ...account, + resourceId, + resource: { + id: resourceId, + name: resourceName, + resourceType, + encryptedRotationAccountCredentials + } + }) + ); }; - return { ...orm, findWithResourceDetails }; + const findAccountsDueForRotation = async (tx?: Knex) => { + const dbClient = tx || db.replicaNode(); + + const accounts = await dbClient(TableName.PamAccount) + .innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`) + .whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`) + .where(`${TableName.PamAccount}.rotationEnabled`, true) + .whereRaw( + `COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()` + ) + .select(selectAllTableCols(TableName.PamAccount)); + + return accounts; + }; + + return { ...orm, findWithResourceDetails, findAccountsDueForRotation }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index e9ea76e8c..fd3615013 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -11,12 +11,14 @@ import { } from "@app/ee/services/permission/project-permission"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal"; @@ -45,10 +47,12 @@ type TPamAccountServiceFactoryDep = { "getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId" >; userDAL: TUserDALFactory; + auditLogService: Pick; }; - export type TPamAccountServiceFactory = ReturnType; +const ROTATION_CONCURRENCY_LIMIT = 10; + export const pamAccountServiceFactory = ({ pamResourceDAL, pamSessionDAL, @@ -59,10 +63,19 @@ export const pamAccountServiceFactory = ({ permissionService, licenseService, kmsService, - gatewayV2Service + gatewayV2Service, + auditLogService }: TPamAccountServiceFactoryDep) => { const create = async ( - { credentials, resourceId, name, description, folderId }: TCreateAccountDTO, + { + credentials, + resourceId, + name, + description, + folderId, + rotationEnabled, + rotationIntervalSeconds + }: TCreateAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -72,6 +85,12 @@ export const pamAccountServiceFactory = ({ }); } + if (rotationEnabled && (rotationIntervalSeconds === undefined || rotationIntervalSeconds === null)) { + throw new BadRequestError({ + message: "Rotation interval must be defined when rotation is enabled." + }); + } + const resource = await pamResourceDAL.findById(resourceId); if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` }); @@ -84,6 +103,10 @@ export const pamAccountServiceFactory = ({ actionProjectType: ActionProjectType.PAM }); + if (!resource.encryptedRotationAccountCredentials && rotationEnabled) { + throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" }); + } + const accountPath = await getFullPamFolderPath({ pamFolderDAL, folderId, @@ -126,12 +149,19 @@ export const pamAccountServiceFactory = ({ encryptedCredentials, name, description, - folderId + folderId, + rotationEnabled, + rotationIntervalSeconds }); return { ...(await decryptAccount(account, resource.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; } catch (err) { if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { @@ -145,7 +175,7 @@ export const pamAccountServiceFactory = ({ }; const updateById = async ( - { accountId, credentials, description, name }: TUpdateAccountDTO, + { accountId, credentials, description, name, rotationEnabled, rotationIntervalSeconds }: TUpdateAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -195,6 +225,17 @@ export const pamAccountServiceFactory = ({ updateDoc.description = description; } + if (rotationEnabled !== undefined) { + if (!resource.encryptedRotationAccountCredentials && rotationEnabled) { + throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" }); + } + updateDoc.rotationEnabled = rotationEnabled; + } + + if (rotationIntervalSeconds !== undefined) { + updateDoc.rotationIntervalSeconds = rotationIntervalSeconds; + } + if (credentials !== undefined) { const connectionDetails = await decryptResourceConnectionDetails({ projectId: account.projectId, @@ -211,7 +252,7 @@ export const pamAccountServiceFactory = ({ // Logic to prevent overwriting unedited censored values const finalCredentials = { ...credentials }; - if (credentials.password === "******") { + if (credentials.password === "__INFISICAL_UNCHANGED__") { const decryptedCredentials = await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, projectId: account.projectId, @@ -239,7 +280,12 @@ export const pamAccountServiceFactory = ({ return { ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; }; @@ -278,7 +324,12 @@ export const pamAccountServiceFactory = ({ return { ...(await decryptAccount(deletedAccount, account.projectId, kmsService)), - resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType } + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } }; }; @@ -300,7 +351,7 @@ export const pamAccountServiceFactory = ({ const decryptedAndPermittedAccounts: Array< TPamAccounts & { - resource: Pick; + resource: Pick & { rotationCredentialsConfigured: boolean }; credentials: TPamAccountCredentials; } > = []; @@ -330,7 +381,8 @@ export const pamAccountServiceFactory = ({ resource: { id: account.resource.id, name: account.resource.name, - resourceType: account.resource.resourceType + resourceType: account.resource.resourceType, + rotationCredentialsConfigured: !!account.resource.encryptedRotationAccountCredentials } }); } @@ -459,13 +511,14 @@ export const pamAccountServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -516,12 +569,116 @@ export const pamAccountServiceFactory = ({ }; }; + const rotateAllDueAccounts = async () => { + const accounts = await pamAccountDAL.findAccountsDueForRotation(); + + for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) { + const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT); + + const rotationPromises = batch.map(async (account) => + pamAccountDAL.transaction(async (tx) => { + let logResourceType = "unknown"; + try { + const resource = await pamResourceDAL.findById(account.resourceId, tx); + if (!resource || !resource.encryptedRotationAccountCredentials) return; + logResourceType = resource.resourceType; + + const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource( + resource, + account.projectId, + kmsService + ); + + if (!rotationAccountCredentials) return; + + const accountCredentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + projectId: account.projectId, + kmsService + }); + + const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource]( + resourceType as PamResource, + connectionDetails, + gatewayId, + gatewayV2Service + ); + + const newCredentials = await factory.rotateAccountCredentials( + rotationAccountCredentials, + accountCredentials + ); + + const encryptedCredentials = await encryptAccountCredentials({ + credentials: newCredentials, + projectId: account.projectId, + kmsService + }); + + await pamAccountDAL.updateById( + account.id, + { + encryptedCredentials, + lastRotatedAt: new Date() + }, + tx + ); + + await auditLogService.createAuditLog({ + projectId: account.projectId, + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + event: { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION, + metadata: { + accountId: account.id, + accountName: account.name, + resourceId: resource.id, + resourceType: logResourceType + } + } + }); + } catch (error) { + logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`); + + const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; + + await auditLogService.createAuditLog({ + projectId: account.projectId, + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + event: { + type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED, + metadata: { + accountId: account.id, + accountName: account.name, + resourceId: account.resourceId, + resourceType: logResourceType, + errorMessage + } + } + }); + throw error; // Rollback transaction + } + }) + ); + + // eslint-disable-next-line no-await-in-loop + await Promise.all(rotationPromises); + } + }; + return { create, updateById, deleteById, list, access, - getSessionCredentials + getSessionCredentials, + rotateAllDueAccounts }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 514d7d780..4bbccc6fa 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,7 +1,10 @@ import { TPamAccount } from "../pam-resource/pam-resource-types"; // DTOs -export type TCreateAccountDTO = Pick; +export type TCreateAccountDTO = Pick< + TPamAccount, + "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds" +>; export type TUpdateAccountDTO = Partial> & { accountId: string; diff --git a/backend/src/ee/services/pam-resource/pam-resource-fns.ts b/backend/src/ee/services/pam-resource/pam-resource-fns.ts index 1d79e892e..9d7493e68 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-fns.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-fns.ts @@ -2,6 +2,7 @@ import { TPamResources } from "@app/db/schemas"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { decryptAccountCredentials } from "../pam-account/pam-account-fns"; import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; @@ -63,6 +64,13 @@ export const decryptResource = async ( encryptedConnectionDetails: resource.encryptedConnectionDetails, projectId, kmsService - }) + }), + rotationAccountCredentials: resource.encryptedRotationAccountCredentials + ? await decryptAccountCredentials({ + encryptedCredentials: resource.encryptedRotationAccountCredentials, + projectId, + kmsService + }) + : null } as TPamResource; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 80a50a9a4..7f6165d88 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -6,6 +6,7 @@ import { slugSchema } from "@app/server/lib/schemas"; // Resources export const BasePamResourceSchema = PamResourcesSchema.omit({ encryptedConnectionDetails: true, + encryptedRotationAccountCredentials: true, resourceType: true }); @@ -30,6 +31,8 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({ id: true, name: true, resourceType: true + }).extend({ + rotationCredentialsConfigured: z.boolean() }) }); @@ -37,10 +40,14 @@ export const BaseCreatePamAccountSchema = z.object({ resourceId: z.string().uuid(), folderId: z.string().uuid().optional(), name: slugSchema({ field: "name" }), - description: z.string().max(512).nullable().optional() + description: z.string().max(512).nullable().optional(), + rotationEnabled: z.boolean(), + rotationIntervalSeconds: z.number().min(3600).nullable().optional() }); export const BaseUpdatePamAccountSchema = z.object({ name: slugSchema({ field: "name" }).optional(), - description: z.string().max(512).nullable().optional() + description: z.string().max(512).nullable().optional(), + rotationEnabled: z.boolean().optional(), + rotationIntervalSeconds: z.number().min(3600).nullable().optional() }); diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 312795a50..d97905dbe 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -10,10 +10,16 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; +import { decryptAccountCredentials, encryptAccountCredentials } from "../pam-account/pam-account-fns"; import { TPamResourceDALFactory } from "./pam-resource-dal"; import { PamResource } from "./pam-resource-enums"; import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory"; -import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns"; +import { + decryptResource, + decryptResourceConnectionDetails, + encryptResourceConnectionDetails, + listResourceOptions +} from "./pam-resource-fns"; import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types"; type TPamResourceServiceFactoryDep = { @@ -61,7 +67,7 @@ export const pamResourceServiceFactory = ({ }; const create = async ( - { resourceType, connectionDetails, gatewayId, name, projectId }: TCreateResourceDTO, + { resourceType, connectionDetails, gatewayId, name, projectId, rotationAccountCredentials }: TCreateResourceDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -88,26 +94,42 @@ export const pamResourceServiceFactory = ({ gatewayId, gatewayV2Service ); - const validatedConnectionDetails = await factory.validateConnection(); + const validatedConnectionDetails = await factory.validateConnection(); const encryptedConnectionDetails = await encryptResourceConnectionDetails({ connectionDetails: validatedConnectionDetails, projectId, kmsService }); + let encryptedRotationAccountCredentials: Buffer | null = null; + + if (rotationAccountCredentials) { + const validatedRotationAccountCredentials = await factory.validateAccountCredentials(rotationAccountCredentials); + + encryptedRotationAccountCredentials = await encryptAccountCredentials({ + credentials: validatedRotationAccountCredentials, + projectId, + kmsService + }); + } + const resource = await pamResourceDAL.create({ resourceType, encryptedConnectionDetails, gatewayId, name, - projectId + projectId, + encryptedRotationAccountCredentials }); return decryptResource(resource, projectId, kmsService); }; - const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => { + const updateById = async ( + { connectionDetails, resourceId, name, rotationAccountCredentials }: TUpdateResourceDTO, + actor: OrgServiceActor + ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); if (!orgLicensePlan.pam) { throw new BadRequestError({ @@ -151,6 +173,60 @@ export const pamResourceServiceFactory = ({ updateDoc.encryptedConnectionDetails = encryptedConnectionDetails; } + if (rotationAccountCredentials !== undefined) { + updateDoc.encryptedRotationAccountCredentials = null; + + if (rotationAccountCredentials) { + const decryptedConnectionDetails = + connectionDetails ?? + (await decryptResourceConnectionDetails({ + encryptedConnectionDetails: resource.encryptedConnectionDetails, + projectId: resource.projectId, + kmsService + })); + + const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource]( + resource.resourceType as PamResource, + decryptedConnectionDetails, + resource.gatewayId, + gatewayV2Service + ); + + // Logic to prevent overwriting unedited censored values + const finalCredentials = { ...rotationAccountCredentials }; + if ( + resource.encryptedRotationAccountCredentials && + rotationAccountCredentials.password === "__INFISICAL_UNCHANGED__" + ) { + const decryptedCredentials = await decryptAccountCredentials({ + encryptedCredentials: resource.encryptedRotationAccountCredentials, + projectId: resource.projectId, + kmsService + }); + + finalCredentials.password = decryptedCredentials.password; + } + + try { + const validatedRotationAccountCredentials = await factory.validateAccountCredentials(finalCredentials); + + updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({ + credentials: validatedRotationAccountCredentials, + projectId: resource.projectId, + kmsService + }); + } catch (err) { + if (err instanceof BadRequestError) { + throw new BadRequestError({ + message: `Rotation Account Error: ${err.message}` + }); + } + + throw err; + } + } + } + // If nothing was updated, return the fetched resource if (Object.keys(updateDoc).length === 0) { return decryptResource(resource, resource.projectId, kmsService); diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index fb1b669ed..f2016420a 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -18,7 +18,7 @@ export type TPamAccountCredentials = TPostgresAccountCredentials; // Resource DTOs export type TCreateResourceDTO = Pick< TPamResource, - "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" + "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials" >; export type TUpdateResourceDTO = Partial> & { @@ -30,6 +30,10 @@ export type TPamResourceFactoryValidateConnection = ( credentials: C ) => Promise; +export type TPamResourceFactoryRotateAccountCredentials = ( + rotationAccountCredentials: C, + currentCredentials: C +) => Promise; export type TPamResourceFactory = ( resourceType: PamResource, @@ -39,4 +43,5 @@ export type TPamResourceFactory { validateConnection: TPamResourceFactoryValidateConnection; validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; + rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials; }; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts index a97e3f2e7..bbe83a3a4 100644 --- a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -15,13 +15,24 @@ import { BaseSqlResourceConnectionDetailsSchema } from "../shared/sql/sql-resource-schemas"; -// Resources export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema; +export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; +// Resources const BasePostgresResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.Postgres) }); export const PostgresResourceSchema = BasePostgresResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() +}); + +export const SanitizedPostgresResourceSchema = BasePostgresResourceSchema.extend({ + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.pick({ + username: true + }) + .nullable() + .optional() }); export const PostgresResourceListItemSchema = z.object({ @@ -30,16 +41,16 @@ export const PostgresResourceListItemSchema = z.object({ }); export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema + connectionDetails: PostgresResourceConnectionDetailsSchema, + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({ - connectionDetails: PostgresResourceConnectionDetailsSchema.optional() + connectionDetails: PostgresResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); // Accounts -export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; - export const PostgresAccountSchema = BasePamAccountSchema.extend({ credentials: PostgresAccountCredentialsSchema }); diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index 74a2c74ae..73defd6e6 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -6,9 +6,14 @@ import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2 import { BadRequestError } from "@app/lib/errors"; import { GatewayProxyProtocol } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { PamResource } from "../../pam-resource-enums"; -import { TPamResourceFactory, TPamResourceFactoryValidateAccountCredentials } from "../../pam-resource-types"; +import { + TPamResourceFactory, + TPamResourceFactoryRotateAccountCredentials, + TPamResourceFactoryValidateAccountCredentials +} from "../../pam-resource-types"; import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types"; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; @@ -176,8 +181,66 @@ export const sqlResourceFactory: TPamResourceFactory = async ( + rotationAccountCredentials, + currentCredentials + ) => { + try { + const newPassword = alphaNumericNanoId(32); + + await executeWithGateway( + { + connectionDetails, + gatewayId, + resourceType, + username: rotationAccountCredentials.username, + password: rotationAccountCredentials.password + }, + gatewayV2Service, + async (client) => { + switch (resourceType) { + case PamResource.Postgres: + await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]); + break; + default: + throw new BadRequestError({ + message: `Password rotation for ${resourceType as PamResource} is not supported.` + }); + } + } + ); + + return { username: currentCredentials.username, password: newPassword }; + } catch (error) { + if (error instanceof BadRequestError) { + if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) { + throw new BadRequestError({ + message: "Management credentials invalid: Username or password incorrect" + }); + } + + if (error.message.includes("permission denied")) { + throw new BadRequestError({ + message: `Management credentials lack permission to rotate password for user "${currentCredentials.username}"` + }); + } + + if (error.message === "Connection terminated unexpectedly") { + throw new BadRequestError({ + message: "Connection terminated unexpectedly. Verify that host and port are correct" + }); + } + } + + throw new BadRequestError({ + message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + return { validateConnection, - validateAccountCredentials + validateAccountCredentials, + rotateAccountCredentials }; }; diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts index cb3abf109..96b6a6a24 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-schemas.ts @@ -16,6 +16,6 @@ export const BaseSqlResourceConnectionDetailsSchema = z.object({ // Accounts export const BaseSqlAccountCredentialsSchema = z.object({ - username: z.string().trim().min(1), - password: z.string().trim().min(1) + username: z.string().trim().min(1).max(63), + password: z.string().trim().min(1).max(256) }); diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 713383306..26ff7daa6 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -102,13 +102,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -142,13 +143,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); if (actor.type === ActorType.IDENTITY) { ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index d4da8c98f..743dcd63f 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -15,6 +15,11 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export enum OrgPermissionAppConnectionActions { Read = "read", Create = "create", @@ -117,7 +122,8 @@ export enum OrgPermissionSubjects { Kmip = "kmip", Gateway = "gateway", Relay = "relay", - SecretShare = "secret-share" + SecretShare = "secret-share", + SubOrganization = "sub-organization" } export type AppConnectionSubjectFields = { @@ -128,6 +134,7 @@ export type OrgPermissionSet = | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization] | [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] @@ -185,6 +192,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") }), + z.object({ + subject: z.literal(OrgPermissionSubjects.SubOrganization).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSubOrgActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") @@ -308,6 +321,10 @@ const buildAdminPermission = () => { // ws permissions can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); + + can(OrgPermissionSubOrgActions.Create, OrgPermissionSubjects.SubOrganization); + can(OrgPermissionSubOrgActions.DirectAccess, OrgPermissionSubjects.SubOrganization); + // role permission can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 49a375f8f..95480a54a 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships { orgAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null; + rootOrgId?: string | null; bypassOrgAuthEnabled?: boolean | null; roles: { id: string; @@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled") + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), + db.ref("rootOrgId").withSchema(TableName.Organization).as("rootOrgId") ); const data = sqlNestRelationships({ @@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), + rootOrgId: z.string().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean() }).parse(el), diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 1f0e00470..c69564b31 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; import { Knex } from "knex"; -import { ActionProjectType, TMemberships } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TMemberships } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = { role: string; }[]; -export type TGetUserProjectPermissionArg = { - userId: string; - projectId: string; - authMethod: ActorAuthMethod; - actionProjectType: ActionProjectType; - userOrgId?: string; -}; - -export type TGetIdentityProjectPermissionArg = { - identityId: string; - projectId: string; - identityOrgId?: string; - actionProjectType: ActionProjectType; -}; - export type TGetServiceTokenProjectPermissionArg = { serviceTokenId: string; projectId: string; @@ -54,17 +39,12 @@ export type TGetOrgPermissionArg = { actorId: string; orgId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId?: string; + actorOrgId: string; + scope: OrganizationActionScope; }; export type TPermissionServiceFactory = { - getOrgPermission: ( - type: ActorType, - id: string, - orgId: string, - authMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => Promise<{ + getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{ permission: MongoAbility; memberships: Array< TMemberships & { diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 2d879b4fd..48b78d980 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -7,6 +7,7 @@ import { Knex } from "knex"; import { AccessScope, ActionProjectType, + OrganizationActionScope, OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes @@ -179,14 +180,15 @@ export const permissionServiceFactory = ({ // return minTtl; // }; - const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( - type, - id, + const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({ + actor, + actorId, orgId, - authMethod, - actorOrgId - ) => { - if (type !== ActorType.USER && type !== ActorType.IDENTITY) { + actorOrgId, + scope, + actorAuthMethod + }) => { + if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) { throw new BadRequestError({ message: "Invalid actor provided", name: "Get org permission" @@ -202,11 +204,19 @@ export const permissionServiceFactory = ({ scope: AccessScope.Organization, orgId }, - actorId: id, - actorType: type + actorId, + actorType: actor }); if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + const rootOrgId = permissionData?.[0]?.rootOrgId; + const isChild = Boolean(rootOrgId); + if (scope === OrganizationActionScope.ParentOrganization && isChild) { + throw new ForbiddenRequestError({ message: `Child organization cannot do this operation` }); + } else if (scope === OrganizationActionScope.ChildOrganization && !isChild) { + throw new ForbiddenRequestError({ message: `Parent organization cannot do this operation` }); + } + const permissionFromRoles = permissionData.flatMap((membership) => { const activeRoles = membership?.roles .filter( @@ -227,7 +237,7 @@ export const permissionServiceFactory = ({ permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); validateOrgSSO( - authMethod, + actorAuthMethod, permissionData?.[0].orgAuthEnforced, Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), Boolean(permissionData?.[0].bypassOrgAuthEnabled), diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index f3fe07aa8..5a9f04d8d 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { ProjectType, TProjectTemplates } from "@app/db/schemas"; +import { OrganizationActionScope, ProjectType, TProjectTemplates } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -59,13 +59,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -97,13 +98,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -125,13 +127,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -152,13 +155,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); @@ -213,13 +217,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); if (projectTemplate.type !== ProjectType.SecretManager && environments) @@ -272,13 +277,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 8d9e952a7..1815344a7 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { OrgServiceActor } from "@app/lib/types"; +import { ProjectServiceActor } from "@app/lib/types"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; @@ -31,7 +31,7 @@ export enum InfisicalProjectTemplate { export type TProjectTemplateServiceFactory = { listProjectTemplatesByOrg: ( - actor: OrgServiceActor, + actor: ProjectServiceActor, type?: ProjectType ) => Promise< ( @@ -85,7 +85,7 @@ export type TProjectTemplateServiceFactory = { >; createProjectTemplate: ( arg: TCreateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -109,7 +109,7 @@ export type TProjectTemplateServiceFactory = { updateProjectTemplateById: ( id: string, { roles, environments, ...params }: TUpdateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -132,7 +132,7 @@ export type TProjectTemplateServiceFactory = { }>; deleteProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -155,7 +155,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; @@ -179,7 +179,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateByName: ( name: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index d791e9919..b2eb932ed 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -3,7 +3,7 @@ import { isIP } from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -126,8 +126,8 @@ export const relayServiceFactory = ({ // generate instance relay CA const instanceRelayCaSerialNumber = createSerialNumber(); - const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey); const instanceRelayCaCert = await x509.X509CertificateGenerator.create({ @@ -972,13 +972,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, @@ -1102,13 +1103,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay @@ -1155,13 +1157,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, - actorAuthMethod, + orgId: actorOrgId, + actorAuthMethod: actorAuthMethod!, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay); @@ -1189,13 +1192,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay); 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 ab84ebd39..13b862343 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -5,6 +5,7 @@ import RE2 from "re2"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -251,7 +252,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); @@ -317,7 +325,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); if (!plan.samlSSO) @@ -393,7 +408,7 @@ export const samlConfigServiceFactory = ({ }); } } else if (dto.type === "orgSlug") { - const org = await orgDAL.findOne({ slug: dto.orgSlug }); + const org = await orgDAL.findOne({ slug: dto.orgSlug, rootOrgId: null }); if (!org) { throw new NotFoundError({ message: `Organization with slug '${dto.orgSlug}' not found` @@ -424,13 +439,14 @@ export const samlConfigServiceFactory = ({ // when dto is type id means it's internally used if (dto.type === "org") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - samlConfig.orgId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: dto.actor, + actorId: dto.actorId, + orgId: samlConfig.orgId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } const { decryptor } = await kmsService.createCipherPairWithDataKey({ 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 bdf65b988..983ec4db7 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -37,7 +37,7 @@ export type TGetSamlCfgDTO = actor: ActorType; actorId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; } | { type: "orgSlug"; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index a08cd5dbf..8b9256023 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -4,6 +4,7 @@ import { scimPatch } from "scim-patch"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -56,6 +57,7 @@ type TScimServiceFactoryDep = { TOrgDALFactory, | "createMembership" | "findById" + | "find" | "findMembership" | "findMembershipWithScimFilter" | "deleteMembershipById" @@ -125,7 +127,14 @@ export const scimServiceFactory = ({ description, ttlDays }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -160,7 +169,14 @@ export const scimServiceFactory = ({ actorAuthMethod, orgId }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -183,13 +199,14 @@ export const scimServiceFactory = ({ let scimToken = await scimDAL.findById(scimTokenId); if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, actor, actorId, - scimToken.orgId, + orgId: 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-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 85a3cd5f2..a5fbe37a7 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { WebhookEventMap } from "@octokit/webhooks-types"; import { ProbotOctokit } from "probot"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; @@ -49,7 +50,14 @@ export const secretScanningServiceFactory = ({ }: TInstallAppSessionDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const sessionId = crypto.randomBytes(16).toString("hex"); @@ -68,13 +76,14 @@ export const secretScanningServiceFactory = ({ const session = await gitAppInstallSessionDAL.findOne({ sessionId }); if (!session) throw new NotFoundError({ message: "Session was not found" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - session.orgId, + orgId: 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); @@ -117,7 +126,14 @@ export const secretScanningServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgInstallStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const appInstallation = await gitAppOrgDAL.findOne({ orgId }); @@ -125,7 +141,14 @@ export const secretScanningServiceFactory = ({ }; const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const results = await secretScanningDAL.findByOrgId(orgId, filter); @@ -134,7 +157,14 @@ export const secretScanningServiceFactory = ({ }; const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); @@ -150,7 +180,14 @@ export const secretScanningServiceFactory = ({ riskId, status }: TUpdateRiskStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); const isRiskResolved = Boolean( diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts new file mode 100644 index 000000000..74bf784d5 --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -0,0 +1,160 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects, OrgPermissionSubOrgActions } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; +import { TCreateSubOrgDTO, TListSubOrgDTO, TUpdateSubOrgDTO } from "./sub-org-types"; + +type TSubOrgServiceFactoryDep = { + orgDAL: Pick< + TOrgDALFactory, + "findOne" | "create" | "transaction" | "listSubOrganizations" | "updateById" | "findById" + >; + permissionService: Pick; + licenseService: Pick; + membershipDAL: Pick; + membershipRoleDAL: Pick; +}; + +export type TSubOrgServiceFactory = ReturnType; + +export const subOrgServiceFactory = ({ + orgDAL, + permissionService, + licenseService, + membershipDAL, + membershipRoleDAL +}: TSubOrgServiceFactoryDep) => { + const createSubOrg = async ({ name, permissionActor }: TCreateSubOrgDTO) => { + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.orgId, + actorOrgId: permissionActor.orgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ParentOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionSubOrgActions.Create, + OrgPermissionSubjects.SubOrganization + ); + + const orgLicensePlan = await licenseService.getPlan(permissionActor.rootOrgId); + if (!orgLicensePlan.subOrganization) { + throw new BadRequestError({ + message: "Sub-organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: permissionActor.orgId, + name + }); + if (existingSubOrg) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.create( + { name, slug: name, rootOrgId: permissionActor.rootOrgId, parentOrgId: permissionActor.orgId }, + tx + ); + const membership = await membershipDAL.create( + { + scope: AccessScope.Organization, + [permissionActor.type === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"]: permissionActor.id, + scopeOrgId: org.id, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + return org; + }); + + return { + organization + }; + }; + + const listSubOrgs = async ({ permissionActor, data }: TListSubOrgDTO) => { + await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.rootOrgId, + actorOrgId: permissionActor.rootOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.Any + }); + + const organizations = await orgDAL.listSubOrganizations({ + actorId: permissionActor.id, + actorType: permissionActor.type, + orgId: permissionActor.rootOrgId, + isAccessible: data?.isAccessible, + limit: data?.limit, + offset: data?.offset + }); + + return { + organizations + }; + }; + + const updateSubOrg = async ({ subOrgId, name, permissionActor }: TUpdateSubOrgDTO) => { + const subOrg = await orgDAL.findOne({ + rootOrgId: permissionActor.rootOrgId, + id: subOrgId + }); + if (!subOrg) { + throw new BadRequestError({ message: "Sub-organization not found" }); + } + + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: subOrgId, + actorOrgId: subOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: subOrg.parentOrgId, + slug: name + }); + + if (existingSubOrg && existingSubOrg.id !== subOrgId) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.updateById(subOrgId, { name, slug: name }); + + return { + organization + }; + }; + + return { + createSubOrg, + listSubOrgs, + updateSubOrg + }; +}; diff --git a/backend/src/ee/services/sub-org/sub-org-types.ts b/backend/src/ee/services/sub-org/sub-org-types.ts new file mode 100644 index 000000000..a1af9878e --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-types.ts @@ -0,0 +1,22 @@ +import { OrgServiceActor } from "@app/lib/types"; + +export type TCreateSubOrgDTO = { + name: string; + permissionActor: OrgServiceActor; +}; + +export type TListSubOrgDTO = { + permissionActor: OrgServiceActor; + data: Partial<{ + limit?: number; + offset?: number; + search?: string; + isAccessible?: boolean; + }>; +}; + +export type TUpdateSubOrgDTO = { + subOrgId: string; + name: string; + permissionActor: OrgServiceActor; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d9012c99e..6b032c6f0 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -33,6 +33,7 @@ export enum ApiDocsTags { LdapAuth = "LDAP Auth", Groups = "Groups", Organizations = "Organizations", + SubOrganizations = "Sub Organizations", Projects = "Projects", ProjectUsers = "Project Users", ProjectGroups = "Project Groups", @@ -717,6 +718,21 @@ export const ORGANIZATIONS = { } } as const; +export const SUB_ORGANIZATIONS = { + CREATE: { + name: "The name of the sub organization to create." + }, + UPDATE: { + name: "The name of the sub organization to update.", + subOrgId: "The id of the sub organization to update." + }, + LIST: { + limit: "The number of sub organizations to return.", + offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.", + isAccessible: "Filter to only return sub organizations that the actor has access to." + } +} as const; + export const PROJECTS = { CREATE: { organizationSlug: "The slug of the organization to create the project in.", diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 15f878323..2da7a245a 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { QueueWorkerProfile } from "@app/lib/types"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; @@ -8,6 +9,7 @@ import { BadRequestError } from "../errors"; import { removeTrailingSlash } from "../fn"; import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; export const GITLAB_URL = "https://gitlab.com"; @@ -363,11 +365,6 @@ const envSchema = z /* INTERNAL ----------------------------------------------------------------------------- */ INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional()) }) - // To ensure that basic encryption is always possible. - .refine( - (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), - "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." - ) .refine( (data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS) || Boolean(data.REDIS_CLUSTER_HOSTS), "Either REDIS_URL, REDIS_SENTINEL_HOSTS or REDIS_CLUSTER_HOSTS must be defined." @@ -453,7 +450,12 @@ export const getConfig = () => envCfg; export const getOriginalConfig = () => originalEnvConfig; // cannot import singleton logger directly as it needs config to load various transport -export const initEnvConfig = async (superAdminDAL?: TSuperAdminDALFactory, logger?: CustomLogger) => { +export const initEnvConfig = async ( + hsmService: THsmServiceFactory, + kmsRootConfigDAL: TKmsRootConfigDALFactory, + superAdminDAL?: TSuperAdminDALFactory, + logger?: CustomLogger +) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { (logger ?? console).error("Invalid environment variables. Check the error below"); @@ -469,7 +471,7 @@ export const initEnvConfig = async (superAdminDAL?: TSuperAdminDALFactory, logge } if (superAdminDAL) { - const fipsEnabled = await crypto.initialize(superAdminDAL); + const fipsEnabled = await crypto.initialize(superAdminDAL, hsmService, kmsRootConfigDAL); if (fipsEnabled) { const newEnvCfg = { @@ -532,6 +534,22 @@ export const getDatabaseCredentials = (logger?: CustomLogger) => { }; }; +export const getHsmConfig = (logger?: CustomLogger) => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + (logger ?? console).error("Invalid environment variables. Check the error below"); + (logger ?? console).error(parsedEnv.error.issues); + process.exit(-1); + } + return { + isHsmConfigured: parsedEnv.data.isHsmConfigured, + HSM_PIN: parsedEnv.data.HSM_PIN, + HSM_SLOT: parsedEnv.data.HSM_SLOT, + HSM_LIB_PATH: parsedEnv.data.HSM_LIB_PATH, + HSM_KEY_LABEL: parsedEnv.data.HSM_KEY_LABEL + }; +}; + // A list of environment variables that can be overwritten export const overwriteSchema: { [key: string]: { diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts index 45c7a1986..6e2a15740 100644 --- a/backend/src/lib/crypto/cryptography/crypto.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -9,7 +9,11 @@ import nacl from "tweetnacl"; import naclUtils from "tweetnacl-util"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; +import { isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; @@ -106,49 +110,73 @@ const cryptographyFactory = () => { } }; - const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick) => { + const $setFipsModeEnabled = async ( + enabled: boolean, + hsmService: THsmServiceFactory, + kmsRootConfigDAL: TKmsRootConfigDALFactory, + envCfg?: Pick + ) => { // If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key. if (enabled) { crypto.setFips(true); const appCfg = envCfg || getConfig(); - if (appCfg.ENCRYPTION_KEY) { - // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key + const hsmStatus = await isHsmActiveAndEnabled({ + hsmService, + kmsRootConfigDAL + }); - // note(daniel): for some reason this resolves as true for some hex-encoded strings. - if (!isBase64(appCfg.ENCRYPTION_KEY)) { + // if the encryption strategy is software - user needs to provide an encryption key + // if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key + const needsEncryptionKey = + hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software || + (hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured); + + // only perform encryption key validation if it's actually required. + if (needsEncryptionKey) { + if (appCfg.ENCRYPTION_KEY) { + // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key + + // note(daniel): for some reason this resolves as true for some hex-encoded strings. + if (!isBase64(appCfg.ENCRYPTION_KEY)) { + throw new CryptographyError({ + message: + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a base64 encoded 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + }); + } + + if (bytesToBits(Buffer.from(appCfg.ENCRYPTION_KEY, "base64").length) !== 256) { + throw new CryptographyError({ + message: + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + }); + } + } else { throw new CryptographyError({ message: - "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a base64 encoded 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not set.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" }); } - - if (bytesToBits(Buffer.from(appCfg.ENCRYPTION_KEY, "base64").length) !== 256) { - throw new CryptographyError({ - message: - "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" - }); - } - } else { - throw new CryptographyError({ - message: - "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not set.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" - }); } } $fipsEnabled = enabled; $isInitialized = true; }; - const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick) => { + const initialize = async ( + superAdminDAL: TSuperAdminDALFactory, + hsmService: THsmServiceFactory, + kmsRootConfigDAL: TKmsRootConfigDALFactory, + envCfg?: Pick + ) => { if ($isInitialized) { return isFipsModeEnabled(); } if (process.env.FIPS_ENABLED !== "true") { logger.info("Cryptography module initialized in normal operation mode."); - $setFipsModeEnabled(false, envCfg); + await $setFipsModeEnabled(false, hsmService, kmsRootConfigDAL, envCfg); return false; } @@ -158,11 +186,11 @@ const cryptographyFactory = () => { if (serverCfg) { if (serverCfg.fipsEnabled) { logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled."); - $setFipsModeEnabled(true, envCfg); + await $setFipsModeEnabled(true, hsmService, kmsRootConfigDAL, envCfg); return true; } logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS."); - $setFipsModeEnabled(false, envCfg); + await $setFipsModeEnabled(false, hsmService, kmsRootConfigDAL, envCfg); return false; } @@ -171,7 +199,7 @@ const cryptographyFactory = () => { // TODO(daniel): check if it's an enterprise deployment // if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true. - $setFipsModeEnabled(true, envCfg); + await $setFipsModeEnabled(true, hsmService, kmsRootConfigDAL, envCfg); return true; }; @@ -258,6 +286,13 @@ const cryptographyFactory = () => { const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; const encryptionKey = appCfg.ENCRYPTION_KEY; + // Sanity check + if (!rootEncryptionKey && !encryptionKey) { + throw new CryptographyError({ + message: "Tried to encrypt with instance root encryption key, but no root encryption key is set." + }); + } + if (rootEncryptionKey) { const { iv, tag, ciphertext } = encrypt({ plaintext: data, @@ -303,6 +338,14 @@ const cryptographyFactory = () => { // the or gate is used used in migration const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY; + + // Sanity check + if (!rootEncryptionKey && !encryptionKey) { + throw new CryptographyError({ + message: "Tried to decrypt with instance root encryption key, but no root encryption key is set." + }); + } + if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) { const data = symmetric().decrypt({ key: rootEncryptionKey, diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index a7a60349f..5fbe8ca79 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -5,7 +5,7 @@ export type TGenericPermission = { actor: ActorType; actorId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; }; /** @@ -78,6 +78,15 @@ export type OrgServiceActor = { id: string; authMethod: ActorAuthMethod; orgId: string; + rootOrgId: string; + parentOrgId: string; +}; + +export type ProjectServiceActor = { + type: ActorType; + id: string; + authMethod: ActorAuthMethod; + orgId: string; }; export enum QueueWorkerProfile { diff --git a/backend/src/main.ts b/backend/src/main.ts index 7be9f43ec..400804804 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,14 +9,16 @@ import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; +import { hsmServiceFactory } from "./ee/services/hsm/hsm-service"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, getDatabaseCredentials, initEnvConfig } from "./lib/config/env"; +import { formatSmtpConfig, getDatabaseCredentials, getHsmConfig, initEnvConfig } from "./lib/config/env"; import { buildRedisFromConfig } from "./lib/config/redis"; import { removeTemporaryBaseDirectory } from "./lib/files"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; import { bootstrapCheck } from "./server/boot-strap-check"; +import { kmsRootConfigDALFactory } from "./services/kms/kms-root-config-dal"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; import { superAdminDALFactory } from "./services/super-admin/super-admin-dal"; @@ -26,6 +28,18 @@ const run = async () => { const logger = initLogger(); await removeTemporaryBaseDirectory(); + const hsmConfig = getHsmConfig(logger); + + const hsmModule = initializeHsmModule(hsmConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig: hsmConfig + }); + + await hsmService.startService(); + const databaseCredentials = getDatabaseCredentials(logger); const db = initDbConnection({ @@ -35,7 +49,8 @@ const run = async () => { }); const superAdminDAL = superAdminDALFactory(db); - const envConfig = await initEnvConfig(superAdminDAL, logger); + const kmsRootConfigDAL = kmsRootConfigDALFactory(db); + const envConfig = await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger); const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ @@ -59,14 +74,12 @@ const run = async () => { const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL); const redis = buildRedisFromConfig(envConfig); - const hsmModule = initializeHsmModule(envConfig); - hsmModule.initialize(); - const server = await main({ db, auditLogDb, superAdminDAL, - hsmModule: hsmModule.getModule(), + kmsRootConfigDAL, + hsmService, smtp, logger, queue, diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 7f45e3821..9d8c472f4 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -77,7 +77,8 @@ export enum QueueName { DailyReminders = "daily-reminders", SecretReminderMigration = "secret-reminder-migration", UserNotification = "user-notification", - HealthAlert = "health-alert" + HealthAlert = "health-alert", + PamAccountRotation = "pam-account-rotation" } export enum QueueJobs { @@ -126,7 +127,8 @@ export enum QueueJobs { DailyReminders = "daily-reminders", SecretReminderMigration = "secret-reminder-migration", UserNotification = "user-notification-job", - HealthAlert = "health-alert" + HealthAlert = "health-alert", + PamAccountRotation = "pam-account-rotation" } export type TQueueJobTypes = { @@ -357,6 +359,10 @@ export type TQueueJobTypes = { name: QueueJobs.HealthAlert; payload: undefined; }; + [QueueName.PamAccountRotation]: { + name: QueueJobs.PamAccountRotation; + payload: undefined; + }; }; const SECRET_SCANNING_JOBS = [ diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 8cf23f703..f1176b932 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -15,12 +15,13 @@ import fastify from "fastify"; import { Cluster, Redis } from "ioredis"; import { Knex } from "knex"; -import { HsmModule } from "@app/ee/services/hsm/hsm-types"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env"; import { CustomLogger } from "@app/lib/logger/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; @@ -42,16 +43,16 @@ type TMain = { logger?: CustomLogger; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; - hsmModule: HsmModule; redis: Redis | Cluster; envConfig: TEnvConfig; superAdminDAL: TSuperAdminDALFactory; + hsmService: THsmServiceFactory; + kmsRootConfigDAL: TKmsRootConfigDALFactory; }; // Run the server! export const main = async ({ db, - hsmModule, auditLogDb, smtp, logger, @@ -59,7 +60,9 @@ export const main = async ({ keyStore, redis, envConfig, - superAdminDAL + superAdminDAL, + hsmService, + kmsRootConfigDAL }: TMain) => { const appCfg = getConfig(); @@ -148,9 +151,10 @@ export const main = async ({ db, auditLogDb, keyStore, - hsmModule, + hsmService, envConfig, - superAdminDAL + superAdminDAL, + kmsRootConfigDAL }); await server.register(registerServeUI, { diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 1bff11879..b33f2fbe6 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,6 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; +import { slugSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -20,6 +21,8 @@ export type TAuthMode = tokenVersionId: string; // the session id of token used user: TUsers; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: AuthMethod; isMfaVerified?: boolean; token: AuthModeJwtTokenPayload; @@ -31,6 +34,8 @@ export type TAuthMode = userId: string; user: TUsers; orgId: string; + rootOrgId: string; + parentOrgId: string; token: string; } | { @@ -39,6 +44,8 @@ export type TAuthMode = actor: ActorType.SERVICE; serviceTokenId: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; token: string; } @@ -48,6 +55,8 @@ export type TAuthMode = identityId: string; identityName: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; isInstanceAdmin?: boolean; token: TIdentityAccessTokenJwtPayload; @@ -57,6 +66,8 @@ export type TAuthMode = actor: ActorType.SCIM_CLIENT; scimTokenId: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; }; @@ -136,17 +147,26 @@ export const injectIdentity = fp( if (!authMode) return; + const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; + if (subOrganizationSelector) { + await slugSchema().parseAsync(subOrganizationSelector); + } + switch (authMode) { case AuthMode.JWT: { - const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } = + await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); requestContext.set("orgId", orgId); + req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, - orgId: orgId as string, + orgId, + rootOrgId, + parentOrgId, authMethod: token.authMethod, isMfaVerified: token.isMfaVerified, token @@ -154,13 +174,19 @@ export const injectIdentity = fp( break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { - const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( + token, + subOrganizationSelector, + req.realIp + ); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, + rootOrgId: identity.rootOrgId, + parentOrgId: identity.parentOrgId, identityId: identity.identityId, identityName: identity.name, authMethod: null, @@ -190,8 +216,14 @@ export const injectIdentity = fp( case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); requestContext.set("orgId", serviceToken.orgId); + + if (subOrganizationSelector) + throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); + req.auth = { orgId: serviceToken.orgId, + rootOrgId: serviceToken.rootOrgId, + parentOrgId: serviceToken.parentOrgId, authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, serviceTokenId: serviceToken.id, @@ -202,22 +234,27 @@ export const injectIdentity = fp( 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, - 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, - token: token as string - }; - break; + throw new BadRequestError({ + message: "API key authentication is not supported anymore. Please switch to identity authentication." + }); } case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); requestContext.set("orgId", orgId); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; + + if (subOrganizationSelector) + throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` }); + + req.auth = { + authMode: AuthMode.SCIM_TOKEN, + actor, + scimTokenId, + orgId, + authMethod: null, + // scim cannot be done for sub organization + rootOrgId: orgId, + parentOrgId: orgId + }; break; } default: diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 11a94657b..827a055d3 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -14,7 +14,9 @@ export const injectPermission = fp(async (server) => { 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 + authMethod: req.auth.authMethod, // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -25,7 +27,9 @@ export const injectPermission = fp(async (server) => { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: req.auth.orgId, - authMethod: null + authMethod: null, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -36,6 +40,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; @@ -47,6 +53,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a805fa1be..b42d01850 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -46,8 +46,8 @@ import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/gi import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; -import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; -import { HsmModule } from "@app/ee/services/hsm/hsm-types"; +import { isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; @@ -131,12 +131,14 @@ import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login- import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal"; import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { subOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, TEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -235,8 +237,9 @@ import { integrationAuthDALFactory } from "@app/services/integration-auth/integr import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; -import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { membershipDALFactory } from "@app/services/membership/membership-dal"; import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; @@ -254,11 +257,11 @@ import { userNotificationDALFactory } from "@app/services/notification/user-noti import { offlineUsageReportDALFactory } from "@app/services/offline-usage-report/offline-usage-report-dal"; import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; -import { orgBotDALFactory } from "@app/services/org/org-bot-dal"; import { orgDALFactory } from "@app/services/org/org-dal"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue"; import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -363,20 +366,22 @@ export const registerRoutes = async ( auditLogDb, superAdminDAL, db, - hsmModule, smtp: smtpService, queue: queueService, keyStore, - envConfig + envConfig, + hsmService, + kmsRootConfigDAL }: { auditLogDb?: Knex; superAdminDAL: TSuperAdminDALFactory; db: Knex; - hsmModule: HsmModule; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; envConfig: TEnvConfig; + hsmService: THsmServiceFactory; + kmsRootConfigDAL: TKmsRootConfigDALFactory; } ) => { const appCfg = getConfig(); @@ -391,7 +396,6 @@ export const registerRoutes = async ( const authTokenDAL = tokenDALFactory(db); const orgDAL = orgDALFactory(db); const orgMembershipDAL = orgMembershipDALFactory(db); - const orgBotDAL = orgBotDALFactory(db); const incidentContactDAL = incidentContactDALFactory(db); const rateLimitDAL = rateLimitDALFactory(db); const apiKeyDAL = apiKeyDALFactory(db); @@ -508,7 +512,6 @@ export const registerRoutes = async ( const kmsDAL = kmskeyDALFactory(db); const internalKmsDAL = internalKmsDALFactory(db); const externalKmsDAL = externalKmsDALFactory(db); - const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const slackIntegrationDAL = slackIntegrationDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db); @@ -568,11 +571,11 @@ export const registerRoutes = async ( orgDAL, licenseDAL, keyStore, - identityOrgMembershipDAL, - projectDAL + projectDAL, + envConfig }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL }); const membershipUserService = membershipUserServiceFactory({ licenseService, @@ -592,6 +595,7 @@ export const registerRoutes = async ( }); const membershipIdentityService = membershipIdentityServiceFactory({ + identityDAL, membershipIdentityDAL, membershipRoleDAL, orgDAL, @@ -623,11 +627,6 @@ export const registerRoutes = async ( permissionService }); - const hsmService = hsmServiceFactory({ - hsmModule, - envConfig - }); - const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, @@ -900,7 +899,6 @@ export const registerRoutes = async ( smtpService, userDAL, groupDAL, - orgBotDAL, oidcConfigDAL, ldapConfigDAL, loginService, @@ -912,6 +910,15 @@ export const registerRoutes = async ( userGroupMembershipDAL, additionalPrivilegeDAL }); + + const subOrgService = subOrgServiceFactory({ + licenseService, + membershipDAL, + membershipRoleDAL, + orgDAL, + permissionService + }); + const signupService = authSignupServiceFactory({ tokenService, smtpService, @@ -1594,10 +1601,12 @@ export const registerRoutes = async ( permissionService, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }); const identityService = identityServiceFactory({ + additionalPrivilegeDAL, permissionService, identityDAL, identityOrgMembershipDAL, @@ -1628,10 +1637,12 @@ export const registerRoutes = async ( identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }); const identityTokenAuthService = identityTokenAuthServiceFactory({ + identityDAL, identityTokenAuthDAL, identityAccessTokenDAL, permissionService, @@ -1641,6 +1652,7 @@ export const registerRoutes = async ( }); const identityUaService = identityUaServiceFactory({ + identityDAL, permissionService, identityAccessTokenDAL, identityUaClientSecretDAL, @@ -1652,6 +1664,7 @@ export const registerRoutes = async ( }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ + identityDAL, identityKubernetesAuthDAL, identityAccessTokenDAL, permissionService, @@ -1665,6 +1678,7 @@ export const registerRoutes = async ( membershipIdentityDAL }); const identityGcpAuthService = identityGcpAuthServiceFactory({ + identityDAL, identityGcpAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1674,6 +1688,7 @@ export const registerRoutes = async ( }); const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAliCloudAuthDAL, @@ -1683,6 +1698,7 @@ export const registerRoutes = async ( }); const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, licenseService, @@ -1692,6 +1708,7 @@ export const registerRoutes = async ( }); const identityAwsAuthService = identityAwsAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAwsAuthDAL, @@ -1701,6 +1718,7 @@ export const registerRoutes = async ( }); const identityAzureAuthService = identityAzureAuthServiceFactory({ + identityDAL, identityAzureAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1710,6 +1728,7 @@ export const registerRoutes = async ( }); const identityOciAuthService = identityOciAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityOciAuthDAL, @@ -1733,6 +1752,7 @@ export const registerRoutes = async ( }); const identityOidcAuthService = identityOidcAuthServiceFactory({ + identityDAL, identityOidcAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1743,6 +1763,7 @@ export const registerRoutes = async ( }); const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityDAL, identityJwtAuthDAL, orgDAL, permissionService, @@ -2238,7 +2259,13 @@ export const registerRoutes = async ( pamSessionDAL, permissionService, projectDAL, - userDAL + userDAL, + auditLogService + }); + + const pamAccountRotation = pamAccountRotationServiceFactory({ + queueService, + pamAccountService }); const pamSessionService = pamSessionServiceFactory({ @@ -2272,16 +2299,38 @@ export const registerRoutes = async ( // Start HSM service if it's configured/enabled. await hsmService.startService(); + const hsmStatus = await isHsmActiveAndEnabled({ + hsmService, + kmsRootConfigDAL, + licenseService + }); + + // if the encryption strategy is software - user needs to provide an encryption key + // if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key + const needsEncryptionKey = + hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software || + (hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured); + + if (needsEncryptionKey) { + if (!envConfig.ROOT_ENCRYPTION_KEY && !envConfig.ENCRYPTION_KEY) { + throw new BadRequestError({ + message: + "Root KMS encryption strategy is set to software. Please set the ENCRYPTION_KEY environment variable and restart your deployment.\nYou can enable HSM encryption in the Server Console." + }); + } + } + await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startAggregatedEventsJob(); await dailyResourceCleanUp.init(); await healthAlert.init(); await pkiSyncCleanup.init(); + await pamAccountRotation.init(); await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); await pkiSubscriberQueue.startDailyAutoRenewalJob(); - await kmsService.startService(); + await kmsService.startService(hsmStatus); await microsoftTeamsService.start(); await dynamicSecretQueueService.init(); await eventBusService.init(); @@ -2296,6 +2345,7 @@ export const registerRoutes = async ( groupProject: groupProjectService, permission: permissionService, org: orgService, + subOrganization: subOrgService, oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 911979b60..48939844e 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { decodedToken.userId, decodedToken.organizationId, decodedToken.authMethod, + decodedToken.organizationId, decodedToken.organizationId ); if (org && org.userTokenExpiration) { diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index 3645a8bb6..8f64d3b23 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -73,12 +73,12 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi } }, handler: async (req) => { - const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAliCloudAuth, accessToken, identityAccessToken, identity } = await server.services.identityAliCloudAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 59526899c..3cfb19895 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -40,12 +40,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAwsAuth, accessToken, identityAccessToken, identity } = await server.services.identityAwsAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_AWS_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 2649655bd..cdab7af02 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAzureAuth, accessToken, identityAccessToken, identity } = await server.services.identityAzureAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_AZURE_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index d65c46613..474999b2b 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityGcpAuth, accessToken, identityAccessToken, identity } = await server.services.identityGcpAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_GCP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 2a882471d..5d71b3781 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -111,7 +111,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityJwtAuth, accessToken, identityAccessToken, identity } = await server.services.identityJwtAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_JWT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 0794cf00d..28f611aba 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -56,7 +56,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } }, handler: async (req) => { - const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = await server.services.identityKubernetesAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index caf5708e3..dade20ea3 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -162,13 +162,13 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) const { identityId, user } = req.passportMachineIdentity; - const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({ + const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({ identityId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_LDAP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index 24d414286..003d9810b 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -52,12 +52,12 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityOciAuth, accessToken, identityAccessToken, identity } = await server.services.identityOciAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_OCI_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 48fa64bf4..6fad1f400 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -59,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } = + const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = await server.services.identityOidcAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_OIDC_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-org-membership-router.ts b/backend/src/server/routes/v1/identity-org-membership-router.ts new file mode 100644 index 000000000..c9b93965a --- /dev/null +++ b/backend/src/server/routes/v1/identity-org-membership-router.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedOrgIdentityMembershipSchema = z.object({ + id: z.string().uuid(), + orgId: z.string(), + identityId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + // this is hidden so not updating tags + tags: [ApiDocsTags.ProjectIdentities], + description: "Create org identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z + .literal(false) + .default(false) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }), + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryMode: z + .nativeEnum(TemporaryPermissionMode) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }) + ]) + ) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description) + .max(1) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: { + identityId: req.params.identityId, + roles: req.body.roles + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); + + server.route({ + method: "DELETE", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + tags: [ApiDocsTags.ProjectIdentities], + description: "Delete org identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + selector: { + identityId: req.params.identityId + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index d6a42c4a2..f8e6c78ee 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -249,7 +249,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()), activeLockoutAuthMethods: z.array(z.string()) }) @@ -393,7 +393,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }).array(), diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index d549160db..b7a44c62c 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -64,7 +64,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid throw new BadRequestError({ message: "Missing TLS certificate in header" }); } - const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = await server.services.identityTlsCertAuth.login({ identityId: req.body.identityId, clientCertificate: clientCertificate as string @@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index 9040d8909..aafffdfdb 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -319,7 +319,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTokenAuth, accessToken, identityAccessToken, identity } = await server.services.identityTokenAuth.createTokenAuthToken({ actor: req.permission.type, actorId: req.permission.id, @@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 0443d35dd..88a4cb775 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -52,14 +52,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { accessToken, identityAccessToken, validClientSecretInfo, - identityMembershipOrg, + identity, accessTokenTTL, accessTokenMaxTTL } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 589a79721..4300f5698 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -33,6 +33,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; +import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; @@ -90,6 +91,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { ); await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); + await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 872b7b157..76b3eae51 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -2,6 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; import { + AccessScope, AuditLogsSchema, GroupsSchema, IncidentContactsSchema, @@ -59,7 +60,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: sanitizedOrganizationSchema + organization: sanitizedOrganizationSchema.extend({ + subOrganization: z + .object({ + id: z.string(), + name: z.string() + }) + .optional() + }) }) } }, @@ -69,6 +77,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.permission.authMethod, + req.permission.rootOrgId, req.permission.orgId ); return { organization }; @@ -467,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { return { groups }; } }); + + server.route({ + method: "GET", + url: "/users/available", + schema: { + response: { + 200: z.object({ + users: z + .object({ + id: z.string().uuid(), + username: z.string(), + email: z.string().nullable().optional(), + firstName: z.string().nullable().optional(), + lastName: z.string().nullable().optional() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { users } = await server.services.membershipUser.listAvailableUsers({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { users }; + } + }); + + server.route({ + method: "GET", + url: "/identities/available", + schema: { + response: { + 200: z.object({ + identities: z + .object({ + id: z.string().uuid(), + name: z.string(), + hasDeleteProtection: z.boolean() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { identities } = await server.services.membershipIdentity.listAvailableIdentities({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { identities }; + } + }); }; diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 8680a2dca..630e09dda 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -60,7 +60,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }) diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index b919ba286..e9d868acc 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TAppConnections } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; @@ -218,13 +218,14 @@ export const appConnectionServiceFactory = ({ ) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -271,13 +272,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -321,13 +323,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -345,13 +348,14 @@ export const appConnectionServiceFactory = ({ { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (projectId) { const project = await projectDAL.findProjectById(projectId); @@ -480,13 +484,14 @@ export const appConnectionServiceFactory = ({ "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (appConnection.projectId) { const { permission } = await permissionService.getProjectPermission({ @@ -638,13 +643,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Delete, @@ -707,13 +713,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionAppConnectionActions.Connect, @@ -750,13 +757,14 @@ export const appConnectionServiceFactory = ({ }; const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); let availableProjectConnections: TAppConnections[] = []; @@ -808,13 +816,14 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index 38f97700c..425c9c4d2 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -25,6 +25,23 @@ import { THCVaultMountResponse } from "./hc-vault-connection-types"; +// HashiCorp Vault stores JSON data, so values can be any valid JSON type +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export const convertVaultValueToString = (value: JsonValue): string => { + if (value === null) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + // For objects and arrays, serialize as JSON + return JSON.stringify(value); +}; + // Concurrency limit for HC Vault API requests to avoid rate limiting const HC_VAULT_CONCURRENCY_LIMIT = 20; @@ -598,7 +615,7 @@ export const getHCVaultSecretsForPath = async ( // For KV v2: /v1/{mount}/data/{path} const { data } = await requestWithHCVaultGateway<{ data: { - data: Record; // KV v2 has nested data structure + data: Record; // KV v2 has nested data structure, supports all JSON types metadata: { created_time: string; deletion_time: string; @@ -620,7 +637,7 @@ export const getHCVaultSecretsForPath = async ( // For KV v1: /v1/{mount}/{path} const { data } = await requestWithHCVaultGateway<{ - data: Record; // KV v1 has flat data structure + data: Record; // KV v1 has flat data structure, supports all JSON types lease_duration: number; lease_id: string; renewable: boolean; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 82df0dcb1..28a986fe8 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -3,10 +3,11 @@ import { Knex } from "knex"; import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; + orgDAL: Pick; membershipUserDAL: Pick; }; @@ -80,7 +82,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA }; // to parse jwt identity in inject identity plugin - const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { + const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { const session = await tokenDAL.findOneTokenSession({ id: token.tokenVersionId, userId: token.userId @@ -207,22 +209,56 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA const user = await userDAL.findById(session.userId); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); + let orgId = ""; + let rootOrgId = ""; + let parentOrgId = ""; if (token.organizationId) { - const orgMembership = await membershipUserDAL.findOne({ - actorUserId: user.id, - scopeOrgId: token.organizationId, - scope: AccessScope.Organization - }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ + rootOrgId: token.organizationId, + slug: subOrganizationSelector + }); + if (!subOrganization) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); - if (!orgMembership) { - throw new ForbiddenRequestError({ message: "User not member of organization" }); - } - if (!orgMembership.isActive) { - throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: subOrganization.id, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + orgId = subOrganization.id; + rootOrgId = token.organizationId; + parentOrgId = subOrganization.parentOrgId as string; + } else { + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + + orgId = token.organizationId; + rootOrgId = token.organizationId; + parentOrgId = token.organizationId; } } - return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId }; + return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId }; }; return { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index a2e426a2e..14f4387b9 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -258,7 +258,13 @@ export const authSignupServiceFactory = ({ let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; if (organizationId) { - const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId); + const org = await orgService.findOrganizationById( + user.id, + organizationId, + authMethod, + organizationId, + organizationId + ); if (org && org.userTokenExpiration) { tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); refreshTokenExpiresIn = org.userTokenExpiration; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index a072544ca..e51d25ce3 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -26,13 +27,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); @@ -48,13 +50,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO, actor: OrgServiceActor ) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 4192ffcd5..bc2a24c07 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipRole } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { AuditLogInfo, EventType, @@ -16,6 +16,7 @@ import { AppConnection } from "../app-connection/app-connection-enums"; import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns"; import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; import { + convertVaultValueToString, getHCVaultAuthMounts, getHCVaultKubernetesAuthRoles, getHCVaultSecretsForPath, @@ -89,13 +90,14 @@ export const externalMigrationServiceFactory = ({ throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -136,13 +138,14 @@ export const externalMigrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TImportVaultDataDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); @@ -192,13 +195,14 @@ export const externalMigrationServiceFactory = ({ actorAuthMethod, provider }: THasCustomVaultMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); @@ -247,13 +251,14 @@ export const externalMigrationServiceFactory = ({ }; const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" }); @@ -298,13 +303,14 @@ export const externalMigrationServiceFactory = ({ connectionId, actor }: TUpdateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" }); @@ -332,13 +338,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" }); @@ -352,13 +359,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" }); @@ -380,13 +388,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault policies" }); @@ -422,13 +431,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" }); @@ -472,13 +482,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" }); @@ -531,13 +542,14 @@ export const externalMigrationServiceFactory = ({ vaultSecretPath: string; auditLogInfo: AuditLogInfo; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" }); @@ -581,7 +593,7 @@ export const externalMigrationServiceFactory = ({ projectId, secrets: Object.entries(vaultSecrets).map(([secretKey, secretValue]) => ({ secretKey, - secretValue + secretValue: convertVaultValueToString(secretValue) })) }); @@ -617,13 +629,14 @@ export const externalMigrationServiceFactory = ({ }; const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" }); @@ -653,13 +666,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; authType?: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault auth mounts" }); @@ -704,13 +718,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 19de362d8..ffdb78645 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -19,6 +19,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select(db.ref("name").withSchema(TableName.Identity)) + .select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId")) .first(); return doc; 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 1f6e4616b..02660a0ae 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 @@ -8,6 +8,7 @@ import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-to import { AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; @@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = { "updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache" >; membershipIdentityDAL: Pick; + orgDAL: Pick; }; export type TIdentityAccessTokenServiceFactory = ReturnType; @@ -27,7 +29,8 @@ export const identityAccessTokenServiceFactory = ({ identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const { @@ -181,7 +184,11 @@ export const identityAccessTokenServiceFactory = ({ return { revokedToken }; }; - const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { + const fnValidateIdentityAccessToken = async ( + token: TIdentityAccessTokenJwtPayload, + subOrganizationSelector?: string, + ipAddress?: string + ) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, isAccessTokenRevoked: false @@ -202,13 +209,40 @@ export const identityAccessTokenServiceFactory = ({ trustedIps: trustedIps as TIp[] }); } - const identityOrgMembership = await membershipIdentityDAL.findOne({ - scope: AccessScope.Organization, - actorIdentityId: identityAccessToken.identityId - }); + let orgId = ""; + let parentOrgId = ""; + const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); + const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; - if (!identityOrgMembership) { - throw new BadRequestError({ message: "Identity does not belong to any organization" }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector }); + if (!subOrganization) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); + + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: subOrganization.id + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + orgId = subOrganization.id; + parentOrgId = subOrganization.parentOrgId as string; + } else { + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: rootOrgId + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + + orgId = rootOrgId; + parentOrgId = rootOrgId; } let { accessTokenNumUses } = identityAccessToken; @@ -219,7 +253,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; + return { ...identityAccessToken, orgId, rootOrgId, parentOrgId }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index 43584a1af..c6f6f1376 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,11 +13,18 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -34,12 +41,13 @@ import { } from "./identity-alicloud-auth-types"; type TIdentityAliCloudAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAliCloudAuthDAL: Pick< TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -48,6 +56,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { export type TIdentityAliCloudAuthServiceFactory = ReturnType; export const identityAliCloudAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAliCloudAuthDAL, membershipIdentityDAL, @@ -63,12 +72,8 @@ export const identityAliCloudAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAliCloudAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAliCloudAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const requestUrl = new URL("https://sts.aliyuncs.com"); @@ -93,8 +98,8 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, lastLoginTime: new Date() @@ -135,7 +140,7 @@ export const identityAliCloudAuthServiceFactory = ({ identityAliCloudAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -162,6 +167,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -173,13 +181,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -238,6 +247,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new NotFoundError({ @@ -255,13 +267,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -304,6 +317,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -313,13 +329,14 @@ export const identityAliCloudAuthServiceFactory = ({ const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -339,27 +356,32 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts index d0fb4d323..c8b494e7b 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts @@ -2,7 +2,7 @@ interface PrincipalArnEntity { Partition: string; Service: "iam" | "sts"; AccountNumber: string; - Type: "user" | "role" | "instance-profile"; + Type: "user" | "role" | "instance-profile" | "assumed-role"; Path: string; FriendlyName: string; SessionInfo: string; // Only populated for assumed-role @@ -49,7 +49,7 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { } // assumed roles use a special format where the friendly name is the role name const [roleName, sessionId] = rest; - finalType = "role"; // treat assumed role case as role + finalType = "assumed-role"; friendlyName = roleName; sessionInfo = sessionId; break; @@ -87,5 +87,5 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { export const extractPrincipalArn = (arn: string) => { const entity = extractPrincipalArnEntity(arn); - return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; + return `arn:aws:${entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 8793c3a00..3bbe88824 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,10 +13,18 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -35,9 +43,10 @@ import { } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -80,6 +89,7 @@ function isValidAwsRegion(region: string | null): boolean { } export const identityAwsAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAwsAuthDAL, membershipIdentityDAL, @@ -93,11 +103,8 @@ export const identityAwsAuthServiceFactory = ({ throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAwsAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAwsAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -141,6 +148,8 @@ export const identityAwsAuthServiceFactory = ({ if (identityAwsAuth.allowedPrincipalArns) { // validate if Arn is in the list of allowed Principal ARNs + const formattedArn = extractPrincipalArn(Arn); + const isArnAllowed = identityAwsAuth.allowedPrincipalArns .split(",") .map((principalArn) => principalArn.trim()) @@ -149,18 +158,23 @@ export const identityAwsAuthServiceFactory = ({ // considers exact matches + wildcard matches // heavily validated in router const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); - return regex.test(extractPrincipalArn(Arn)); + return regex.test(formattedArn); }); - if (!isArnAllowed) + if (!isArnAllowed) { + logger.error( + `AWS Auth Login: AWS principal ARN not allowed [principal-arn=${formattedArn}] [raw-arn=${Arn}] [identity-id=${identity.id}]` + ); + throw new UnauthorizedError({ - message: "Access denied: AWS principal ARN not allowed." + message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]` }); + } } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, lastLoginTime: new Date() @@ -212,7 +226,7 @@ export const identityAwsAuthServiceFactory = ({ } ); - return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAwsAuth, identityAccessToken, identity }; }; const attachAwsAuth = async ({ @@ -240,6 +254,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -251,13 +268,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -320,6 +338,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new NotFoundError({ @@ -336,13 +357,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -387,6 +409,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -396,13 +421,14 @@ export const identityAwsAuthServiceFactory = ({ const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -422,27 +448,32 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index 098bdcf9a..4e3884e15 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -4,7 +4,10 @@ import { z } from "zod"; const twelveDigitRegex = new RE2(/^\d{12}$/); // akhilmhdh: change this to a normal function later. Checked no redosable at the moment -const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/); + +const arnRegex = new RE2( + /^arn:aws:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/ +); export const validateAccountIds = z .string() @@ -52,7 +55,7 @@ export const validatePrincipalArns = z }, { message: - "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*'." + "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'." } ) // Transform to normalize the spaces around commas diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index b3250ed56..f75aeba4f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -30,11 +37,12 @@ import { } from "./identity-azure-auth-types"; type TIdentityAzureAuthServiceFactoryDep = { + identityDAL: Pick; identityAzureAuthDAL: Pick< TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -44,6 +52,7 @@ type TIdentityAzureAuthServiceFactoryDep = { export type TIdentityAzureAuthServiceFactory = ReturnType; export const identityAzureAuthServiceFactory = ({ + identityDAL, identityAzureAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -57,11 +66,8 @@ export const identityAzureAuthServiceFactory = ({ throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAzureAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAzureAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const azureIdentity = await validateAzureIdentity({ tenantId: identityAzureAuth.tenantId, @@ -86,8 +92,8 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, lastLoginTime: new Date() @@ -125,7 +131,7 @@ export const identityAzureAuthServiceFactory = ({ } ); - return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAzureAuth, identityAccessToken, identity }; }; const attachAzureAuth = async ({ @@ -153,6 +159,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -163,13 +172,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -232,6 +242,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "Failed to update Azure Auth" @@ -247,13 +260,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -301,6 +315,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have Azure Auth attached" @@ -309,13 +326,14 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -336,27 +354,32 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index fe7b9b6d7..67adb6c1e 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -31,8 +38,9 @@ import { } from "./identity-gcp-auth-types"; type TIdentityGcpAuthServiceFactoryDep = { + identityDAL: Pick; identityGcpAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -42,6 +50,7 @@ type TIdentityGcpAuthServiceFactoryDep = { export type TIdentityGcpAuthServiceFactory = ReturnType; export const identityGcpAuthServiceFactory = ({ + identityDAL, identityGcpAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -55,13 +64,8 @@ export const identityGcpAuthServiceFactory = ({ throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityGcpAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); - } + const identity = await identityDAL.findById(identityGcpAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); let gcpIdentityDetails: TGcpIdentityDetails; switch (identityGcpAuth.type) { @@ -125,8 +129,8 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, lastLoginTime: new Date() @@ -164,7 +168,7 @@ export const identityGcpAuthServiceFactory = ({ } ); - return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityGcpAuth, identityAccessToken, identity }; }; const attachGcpAuth = async ({ @@ -193,6 +197,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -204,13 +211,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -274,6 +282,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -290,13 +301,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -345,6 +357,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -354,13 +369,14 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -381,28 +397,33 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index a99c8ad78..debd90933 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -3,7 +3,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -24,6 +24,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -43,8 +44,9 @@ import { } from "./identity-jwt-auth-types"; type TIdentityJwtAuthServiceFactoryDep = { + identityDAL: Pick; identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityJwtAuthServiceFactoryDep = { export type TIdentityJwtAuthServiceFactory = ReturnType; export const identityJwtAuthServiceFactory = ({ + identityDAL, identityJwtAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityJwtAuthServiceFactory = ({ throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityJwtAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityJwtAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); @@ -211,12 +207,9 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -251,7 +244,7 @@ export const identityJwtAuthServiceFactory = ({ } ); - return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityJwtAuth, identityAccessToken, identity }; }; const attachJwtAuth = async ({ @@ -284,6 +277,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ message: "Failed to add JWT Auth to already configured identity" @@ -294,13 +290,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -387,6 +384,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -403,13 +403,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -491,6 +492,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -498,13 +502,14 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); @@ -539,6 +544,9 @@ export const identityJwtAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -546,23 +554,25 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 952b1e31d..49fb597f5 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -3,7 +3,12 @@ import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { + AccessScope, + IdentityAuthMethod, + OrganizationActionScope, + TIdentityKubernetesAuthsUpdate +} from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; @@ -21,13 +26,20 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -48,12 +60,13 @@ import { } from "./identity-kubernetes-auth-types"; type TIdentityKubernetesAuthServiceFactoryDep = { + identityDAL: Pick; identityKubernetesAuthDAL: Pick< TIdentityKubernetesAuthDALFactory, "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -69,6 +82,7 @@ export type TIdentityKubernetesAuthServiceFactory = ReturnType { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -475,7 +479,7 @@ export const identityKubernetesAuthServiceFactory = ({ } ); - return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; }; const attachKubernetesAuth = async ({ @@ -508,6 +512,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -519,13 +526,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -560,13 +568,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -633,6 +642,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -650,13 +662,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -692,13 +705,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -779,6 +793,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { @@ -791,13 +808,14 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -841,28 +859,33 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 1a8ea3ed6..272e45c4e 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -56,11 +57,11 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; - identityDAL: TIdentityDALFactory; + identityDAL: Pick; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; keyStore: Pick< TKeyStoreFactory, @@ -150,17 +151,6 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); if (!identityLdapAuth) { @@ -169,7 +159,10 @@ export const identityLdapAuthServiceFactory = ({ }); } - const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); + const identity = await identityDAL.findById(identityLdapAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + + const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ message: @@ -178,12 +171,9 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -217,7 +207,7 @@ export const identityLdapAuthServiceFactory = ({ } ); - return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityLdapAuth, identityAccessToken, identity }; }; const attachLdapAuth = async ({ @@ -254,6 +244,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -265,13 +258,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); if (templateId) { @@ -425,6 +419,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new NotFoundError({ @@ -441,13 +438,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); if (templateId) { @@ -588,6 +586,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -597,13 +598,14 @@ export const identityLdapAuthServiceFactory = ({ const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -635,27 +637,32 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ message: "The identity does not have LDAP Auth attached" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -785,13 +792,14 @@ export const identityLdapAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index bfac3d158..6d7f0c4d3 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,11 +14,18 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -35,9 +42,10 @@ import { } from "./identity-oci-auth-types"; type TIdentityOciAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -46,6 +54,7 @@ type TIdentityOciAuthServiceFactoryDep = { export type TIdentityOciAuthServiceFactory = ReturnType; export const identityOciAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityOciAuthDAL, membershipIdentityDAL, @@ -59,11 +68,8 @@ export const identityOciAuthServiceFactory = ({ throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOciAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityOciAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -98,12 +104,9 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -140,7 +143,7 @@ export const identityOciAuthServiceFactory = ({ identityOciAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -168,6 +171,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -179,13 +185,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -246,6 +253,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new NotFoundError({ @@ -262,13 +272,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -312,6 +323,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -321,13 +335,14 @@ export const identityOciAuthServiceFactory = ({ const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -347,27 +362,32 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ message: "The identity does not have OCI auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 1218d8e1c..628b69f14 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,7 +4,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { AccessScope, IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -25,6 +25,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -43,8 +44,9 @@ import { } from "./identity-oidc-auth-types"; type TIdentityOidcAuthServiceFactoryDep = { + identityDAL: Pick; identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityOidcAuthServiceFactoryDep = { export type TIdentityOidcAuthServiceFactory = ReturnType; export const identityOidcAuthServiceFactory = ({ + identityDAL, identityOidcAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityOidcAuthServiceFactory = ({ throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOidcAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityOidcAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityOidcAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); let caCert = ""; @@ -182,12 +178,9 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -226,7 +219,7 @@ export const identityOidcAuthServiceFactory = ({ } ); - return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg, oidcTokenData: tokenData }; + return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData }; }; const attachOidcAuth = async ({ @@ -259,6 +252,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ message: "Failed to add OIDC Auth to already configured identity" @@ -269,13 +265,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -351,6 +348,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -367,13 +367,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -440,6 +441,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -447,13 +451,14 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -481,6 +486,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -488,23 +496,25 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 3dba6210d..adcdd8be8 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -25,11 +25,12 @@ import { buildAuthMethods } from "../identity/identity-fns"; export type TIdentityProjectDALFactory = ReturnType; export const identityProjectDALFactory = (db: TDbClient) => { - const findByIdentityId = async (identityId: string, tx?: Knex) => { + const findByIdentityId = async (identityId: string, orgId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.Membership) .where(`${TableName.Membership}.actorIdentityId`, identityId) .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index 625b9b328..24c82ccac 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -11,10 +11,17 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -25,12 +32,13 @@ import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; type TIdentityTlsCertAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityTlsCertAuthDAL: Pick< TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -46,6 +54,7 @@ const parseSubjectDetails = (data: string) => { }; export const identityTlsCertAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, membershipIdentityDAL, @@ -61,20 +70,12 @@ export const identityTlsCertAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityTlsCertAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityTlsCertAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const caCertificate = decryptor({ @@ -119,12 +120,9 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -161,7 +159,7 @@ export const identityTlsCertAuthServiceFactory = ({ identityTlsCertAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -189,6 +187,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -200,13 +201,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -271,6 +273,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new NotFoundError({ @@ -288,13 +293,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -350,6 +356,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -359,13 +368,14 @@ export const identityTlsCertAuthServiceFactory = ({ const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -394,28 +404,32 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ message: "The identity does not have TLS Certificate auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, memberships } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index eb9f4ab5d..cf35bb5ee 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -1,4 +1,4 @@ -import { TIdentityAccessTokens, TIdentityTlsCertAuths, TMemberships } from "@app/db/schemas"; +import { TIdentities, TIdentityAccessTokens, TIdentityTlsCertAuths } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { @@ -40,7 +40,7 @@ export type TIdentityTlsCertAuthServiceFactory = { identityTlsCertAuth: TIdentityTlsCertAuths; accessToken: string; identityAccessToken: TIdentityAccessTokens; - identityMembershipOrg: TMemberships; + identity: TIdentities; }>; attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise; updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 2ae05cb97..2d3e11cd8 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod, TableName } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -32,11 +39,12 @@ import { } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { + identityDAL: Pick; identityTokenAuthDAL: Pick< TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" @@ -49,8 +57,8 @@ type TIdentityTokenAuthServiceFactoryDep = { export type TIdentityTokenAuthServiceFactory = ReturnType; export const identityTokenAuthServiceFactory = ({ + identityDAL, identityTokenAuthDAL, - // identityDAL, membershipIdentityDAL, identityAccessTokenDAL, permissionService, @@ -79,6 +87,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -90,13 +101,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -155,6 +167,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -172,13 +187,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -223,6 +239,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -232,13 +251,14 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -262,28 +282,33 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -341,22 +366,26 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -379,13 +408,13 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + const identity = await identityDAL.findById(identityTokenAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -420,7 +449,7 @@ export const identityTokenAuthServiceFactory = ({ } ); - return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityTokenAuth, identityAccessToken, identity }; }; const getTokenAuthTokens = async ({ @@ -449,13 +478,14 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( @@ -501,22 +531,24 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -580,13 +612,14 @@ export const identityTokenAuthServiceFactory = ({ throw new NotFoundError({ message: `Failed to find identity with ID ${identityAccessToken.identityId}` }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const [revokedToken] = await identityAccessTokenDAL.update( diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 563a3f897..00ab1610d 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,6 +13,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -22,6 +23,7 @@ import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; @@ -42,6 +44,7 @@ import { } from "./identity-ua-types"; type TIdentityUaServiceFactoryDep = { + identityDAL: Pick; identityUaDAL: TIdentityUaDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; @@ -70,7 +73,8 @@ export const identityUaServiceFactory = ({ permissionService, licenseService, orgDAL, - keyStore + keyStore, + identityDAL }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { const identityUa = await identityUaDAL.findOne({ clientId }); @@ -100,16 +104,6 @@ export const identityUaServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityUa.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecretInfo = await identityUaClientSecretDAL.find({ identityUAId: identityUa.id, @@ -227,10 +221,11 @@ export const identityUaServiceFactory = ({ accessTokenMaxTTL: 1000000000 }; + const identity = await identityDAL.findById(identityUa.identityId); const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, lastLoginTime: new Date() @@ -276,7 +271,7 @@ export const identityUaServiceFactory = ({ identityUa, validClientSecretInfo, identityAccessToken, - identityMembershipOrg, + identity, ...accessTokenTTLParams }; }; @@ -315,18 +310,23 @@ export const identityUaServiceFactory = ({ message: "Failed to add universal auth to already configured identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -423,6 +423,10 @@ export const identityUaServiceFactory = ({ }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + if ( (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 && (accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) @@ -430,13 +434,14 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -512,14 +517,18 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -545,22 +554,27 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -611,23 +625,28 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -692,23 +711,28 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -761,6 +785,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -768,22 +795,24 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -828,6 +857,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -835,22 +867,24 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -900,14 +934,18 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 65aee561c..66556f5fa 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -163,7 +163,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.Membership), db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("hasDeleteProtection").withSchema(TableName.Identity) + db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity) ) .where(filter) .as("paginatedIdentity"); @@ -257,6 +258,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), db.ref("scopeOrgId").withSchema("paginatedIdentity").as("orgId"), db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), + db.ref("orgId").withSchema("paginatedIdentity").as("identityOrgId"), db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), @@ -309,6 +311,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { roleId, id, orgId, + identityOrgId, uaId, alicloudId, awsId, @@ -348,6 +351,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, @@ -515,6 +519,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("actorIdentityId").withSchema(TableName.Membership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -566,6 +571,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { crPermission, crName, identityId, + identityOrgId, identityName, hasDeleteProtection, role, @@ -611,6 +617,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 721844070..f6ec60e9e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -12,6 +12,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; @@ -40,6 +41,7 @@ type TIdentityServiceFactoryDep = { licenseService: Pick; keyStore: Pick; orgDAL: Pick; + additionalPrivilegeDAL: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -54,7 +56,8 @@ export const identityServiceFactory = ({ keyStore, orgDAL, membershipIdentityDAL, - membershipRoleDAL + membershipRoleDAL, + additionalPrivilegeDAL }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -67,7 +70,14 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], orgId); @@ -104,7 +114,7 @@ export const identityServiceFactory = ({ } const identity = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); + const newIdentity = await identityDAL.create({ name, hasDeleteProtection, orgId }, tx); const membership = await membershipIdentityDAL.create( { scope: AccessScope.Organization, @@ -172,13 +182,14 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); let customRole: TRoles | undefined; @@ -208,11 +219,12 @@ export const identityServiceFactory = ({ if (isCustomRole) customRole = rolePermissionDetails?.role; } + const identityDetails = await identityDAL.findById(id); const identity = await identityDAL.transaction(async (tx) => { const newIdentity = - name || hasDeleteProtection + identityDetails.orgId === actorOrgId && (name || hasDeleteProtection) ? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx) - : await identityDAL.findById(id, tx); + : identityDetails; if (role) { await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); @@ -264,16 +276,16 @@ export const identityServiceFactory = ({ const identity = doc[0]; if (!identity) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identity.orgId, + orgId: identity.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - // TODO(namespace): check this in identity service const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); @@ -314,23 +326,56 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); if (identityOrgMembership.identity.hasDeleteProtection) throw new BadRequestError({ message: "Identity has delete protection" }); - const deletedIdentity = await identityDAL.deleteById(id); + if (identityOrgMembership.identity.identityOrgId === actorOrgId) { + const deletedIdentity = await identityDAL.deleteById(id); + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; + } - await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + await membershipIdentityDAL.transaction(async (tx) => { + await identityMetadataDAL.delete( + { + identityId: id, + orgId: actorOrgId + }, + tx + ); + const identityProjectMembership = await membershipIdentityDAL.find( + { + actorIdentityId: id, + scope: AccessScope.Project, + scopeOrgId: actorOrgId + }, + { tx } + ); + await additionalPrivilegeDAL.delete( + { + actorIdentityId: id, + $in: { + projectId: identityProjectMembership.map((el) => el.scopeProjectId) + } + }, + tx + ); + const doc = await membershipIdentityDAL.delete({ actorIdentityId: id, scopeOrgId: actorOrgId }, tx); + return doc; + }); + const deletedIdentity = await identityDAL.findById(id); return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; }; @@ -346,7 +391,14 @@ export const identityServiceFactory = ({ orderDirection, search }: TListOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ @@ -379,7 +431,14 @@ export const identityServiceFactory = ({ orderDirection, searchFilter = {} }: TSearchOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({ @@ -408,16 +467,17 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); + const identityMemberships = await identityProjectDAL.findByIdentityId(identityId, actorOrgId); return identityMemberships; }; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 248488e9f..1d1f2cbb6 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -112,7 +112,7 @@ export const integrationAuthServiceFactory = ({ }; const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { - const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); + const authorizations = await integrationAuthDAL.getByOrg(actorOrgId); const filteredAuthorizations = await Promise.all( authorizations.map(async (auth) => { diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 4e5b48006..b665df4ed 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -12,6 +12,7 @@ import { TExternalKmsProviderFns } from "@app/ee/services/external-kms/providers/model"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { THsmStatus } from "@app/ee/services/hsm/hsm-types"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { TEnvConfig } from "@app/lib/config/env"; import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher"; @@ -1077,17 +1078,22 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; - const startService = async () => { + const startService = async (hsmStatus: THsmStatus) => { const kmsRootConfig = await kmsRootConfigDAL.transaction(async (tx) => { await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.KmsRootKeyInit]); // check if KMS root key was already generated and saved in DB const existingRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); if (existingRootConfig) return existingRootConfig; - logger.info("KMS: Generating new ROOT Key"); - const newRootKey = crypto.randomBytes(32); - const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { - logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); + const isHsmActive = hsmStatus.isHsmConfigured; + + logger.info(`KMS: Generating new ROOT Key with ${isHsmActive ? "HSM" : "software"} encryption`); + const newRootKey = isHsmActive ? await hsmService.randomBytes(32) : crypto.randomBytes(32); + + const encryptionStrategy = isHsmActive ? RootKeyEncryptionStrategy.HSM : RootKeyEncryptionStrategy.Software; + + const encryptedRootKey = await $encryptRootKey(newRootKey, encryptionStrategy).catch((err) => { + logger.error({ hsmEnabled: isHsmActive, encryptionStrategy }, "KMS: Failed to encrypt ROOT Key"); throw err; }); @@ -1095,7 +1101,7 @@ export const kmsServiceFactory = ({ // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition id: KMS_ROOT_CONFIG_UUID, encryptedRootKey, - encryptionStrategy: RootKeyEncryptionStrategy.Software + encryptionStrategy }); return newRootConfig; }); @@ -1117,6 +1123,15 @@ export const kmsServiceFactory = ({ return; } + if (strategy === RootKeyEncryptionStrategy.Software) { + if (!envConfig.ROOT_ENCRYPTION_KEY && !envConfig.ENCRYPTION_KEY) { + throw new BadRequestError({ + message: + "Root KMS encryption strategy is set to software. Please set the ENCRYPTION_KEY environment variable and restart your deployment before trying to update the encryption strategy to software mode." + }); + } + } + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy); diff --git a/backend/src/services/membership-group/org/org-membership-group-factory.ts b/backend/src/services/membership-group/org/org-membership-group-factory.ts index 1e87ee3ca..d69db9c08 100644 --- a/backend/src/services/membership-group/org/org-membership-group-factory.ts +++ b/backend/src/services/membership-group/org/org-membership-group-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -45,13 +45,14 @@ export const newOrgMembershipGroupFactory = ({ }; const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -89,26 +90,28 @@ export const newOrgMembershipGroupFactory = ({ }; const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); }; const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); }; diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 64e508fb8..682bfef3e 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -91,6 +91,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { .select( db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), db.ref("slug").withSchema(TableName.Role).as("roleSlug"), @@ -132,6 +133,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { parentMapper: (el) => { const { identityId: actorIdentityId, + identityOrgId, identityHasDeleteProtection, identityName, uaId, @@ -153,6 +155,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { name: identityName, id: actorIdentityId, hasDeleteProtection: identityHasDeleteProtection, + identityOrgId, authMethods: buildAuthMethods({ uaId, awsId, @@ -353,5 +356,34 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { } }; - return { ...orm, findIdentities, getIdentityById }; + // this right now only support sub organization + const listAvailableIdentities = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorIdentityId"); + + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNotIn(`${TableName.Membership}.actorIdentityId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Identity), + db.ref("name").withSchema(TableName.Identity), + db.ref("hasDeleteProtection").withSchema(TableName.Identity) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableIdentities" }); + } + }; + + return { ...orm, findIdentities, getIdentityById, listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index 4dd3da064..16292ea82 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -6,6 +6,7 @@ import { ms } from "@app/lib/ms"; import { SearchResourceOperators } from "@app/lib/search-resource/search"; import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TRoleDALFactory } from "../role/role-dal"; @@ -31,6 +32,7 @@ type TMembershipIdentityServiceFactoryDep = { >; orgDAL: Pick; additionalPrivilegeDAL: Pick; + identityDAL: Pick; }; export type TMembershipIdentityServiceFactory = ReturnType; @@ -41,12 +43,14 @@ export const membershipIdentityServiceFactory = ({ membershipRoleDAL, permissionService, orgDAL, - additionalPrivilegeDAL + additionalPrivilegeDAL, + identityDAL }: TMembershipIdentityServiceFactoryDep) => { const scopeFactory = { [AccessScope.Organization]: newOrgMembershipIdentityFactory({ orgDAL, - permissionService + permissionService, + identityDAL }), [AccessScope.Project]: newProjectMembershipIdentityFactory({ membershipIdentityDAL, @@ -305,7 +309,7 @@ export const membershipIdentityServiceFactory = ({ [SearchResourceOperators.$contains]: dto.data.identityName } : undefined, - role: dto.data.roles.length + role: dto.data?.roles?.length ? { [SearchResourceOperators.$in]: dto.data.roles } @@ -329,11 +333,29 @@ export const membershipIdentityServiceFactory = ({ return membership; }; + const listAvailableIdentities = async (dto: TListMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipIdentityGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { identities: [] }; + + const identities = await membershipIdentityDAL.listAvailableIdentities( + organizationDetails.id, + organizationDetails.rootOrgId + ); + + return { identities }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByIdentityId + getMembershipByIdentityId, + listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-types.ts b/backend/src/services/membership-identity/membership-identity-types.ts index adce10237..78923cb14 100644 --- a/backend/src/services/membership-identity/membership-identity-types.ts +++ b/backend/src/services/membership-identity/membership-identity-types.ts @@ -54,14 +54,11 @@ export type TUpdateMembershipIdentityDTO = { export type TListMembershipIdentityDTO = { permission: OrgServiceActor; scopeData: AccessScopeData; - selector: { - identityId: string; - }; data: { limit?: number; offset?: number; identityName?: string; - roles: string[]; + roles?: string[]; }; }; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts index 06789e274..8b3bdf6d5 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -8,6 +8,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; @@ -16,11 +17,13 @@ import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; type TOrgMembershipIdentityScopeFactoryDep = { permissionService: Pick; orgDAL: Pick; + identityDAL: Pick; }; export const newOrgMembershipIdentityFactory = ({ permissionService, - orgDAL + orgDAL, + identityDAL }: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -38,23 +41,66 @@ export const newOrgMembershipIdentityFactory = ({ const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); - const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = - async () => { - throw new BadRequestError({ - message: "Organization membership cannot be created for organization scoped identity" - }); - }; + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.data.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identities from parent organization can be invited" }); + } + + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -85,35 +131,54 @@ export const newOrgMembershipIdentityFactory = ({ } }; - const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = - async () => { - throw new BadRequestError({ - message: "Organization membership cannot be deleted for organization scoped identity" - }); - }; + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.selector.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identities from parent organization can do this operation" }); + } + + if (identityDetails.orgId === dto.permission.orgId) { + throw new BadRequestError({ message: "Identity cannot exist as orphan" }); + } + }; const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); }; const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); }; diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 7882b9639..17970f16f 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -291,5 +291,37 @@ export const membershipUserDALFactory = (db: TDbClient) => { } }; - return { ...orm, findUsers, getUserById }; + // this right now only support sub organization + const listAvailableUsers = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorUserId"); + + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Users}.isGhost`, false) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNotIn(`${TableName.Membership}.actorUserId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Users), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableUsers" }); + } + }; + + return { ...orm, findUsers, getUserById, listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index 82dca0159..4b14ee771 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -40,7 +40,7 @@ import { newProjectMembershipUserFactory } from "./project/project-membership-us type TMembershipUserServiceFactoryDep = { membershipUserDAL: TMembershipUserDALFactory; membershipRoleDAL: Pick; - orgDAL: Pick; + orgDAL: Pick; roleDAL: Pick; userDAL: TUserDALFactory; permissionService: Pick< @@ -83,7 +83,8 @@ export const membershipUserServiceFactory = ({ orgDAL, tokenService, userDAL, - userGroupMembershipDAL + userGroupMembershipDAL, + membershipUserDAL }), [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), [AccessScope.Project]: newProjectMembershipUserFactory({ @@ -404,7 +405,7 @@ export const membershipUserServiceFactory = ({ const membershipDoc = await membershipUserDAL.transaction(async (tx) => { if (dto.scopeData.scope === AccessScope.Organization) { const [doc] = await deleteOrgMembershipsFn({ - orgMembershipIds: [], + orgMembershipIds: [existingMembership.id], orgId: dto.permission.orgId, orgDAL, projectKeyDAL, @@ -471,11 +472,26 @@ export const membershipUserServiceFactory = ({ return membership; }; + // Should only be used for sub organization as of now + const listAvailableUsers = async (dto: TListMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipUserGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { users: [] }; + + const users = await membershipUserDAL.listAvailableUsers(organizationDetails.id, organizationDetails.rootOrgId); + return { users }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByUserId + getMembershipByUserId, + listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts index b8761671c..15982bb6a 100644 --- a/backend/src/services/membership-user/membership-user-types.ts +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -93,3 +93,8 @@ export type TGetMembershipUserByUserIdDTO = { userId: string; }; }; + +export type TListAvailableUsersDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; +}; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 523e85bae..d21b27b69 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; @@ -15,6 +15,7 @@ import { isCustomOrgRole } from "@app/services/org/org-role-fns"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TMembershipUserDALFactory } from "../membership-user-dal"; import { TMembershipUserScopeFactory } from "../membership-user-types"; type TOrgMembershipUserScopeFactoryDep = { @@ -25,6 +26,7 @@ type TOrgMembershipUserScopeFactoryDep = { orgDAL: Pick; userGroupMembershipDAL: Pick; licenseService: Pick; + membershipUserDAL: Pick; }; export const newOrgMembershipUserFactory = ({ @@ -33,7 +35,8 @@ export const newOrgMembershipUserFactory = ({ userDAL, orgDAL, smtpService, - licenseService + licenseService, + membershipUserDAL }: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -51,14 +54,18 @@ export const newOrgMembershipUserFactory = ({ const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); - const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async ( + dto, + newMembers + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); const plan = await licenseService.getPlan(dto.permission.orgId); @@ -77,6 +84,25 @@ export const newOrgMembershipUserFactory = ({ message: "Failed to invite user due to org-level auth enforced for organization" }); } + + if (org.rootOrgId) { + const rootOrgMembership = await membershipUserDAL.find({ + scope: AccessScope.Organization, + $in: { + actorUserId: newMembers.map((el) => el.id) + }, + scopeOrgId: org.rootOrgId + }); + if (rootOrgMembership.length !== newMembers.length) { + const emails = newMembers + .filter((user) => !rootOrgMembership.find((i) => i.actorUserId === user.id)) + .map((el) => el.email) + .join(","); + throw new BadRequestError({ + message: `Users with email ${emails} doesn't have membership in root organization` + }); + } + } }; const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( @@ -95,87 +121,103 @@ export const newOrgMembershipUserFactory = ({ const signUpTokens: { email: string; link: string }[] = []; const orgDetails = await orgDAL.findById(dto.permission.orgId); + if (orgDetails.rootOrgId) { + const emails = newUsers.map((el) => el.email).filter(Boolean); + await smtpService.sendMail({ + template: SmtpTemplates.SubOrgInvite, + subjectLine: "Infisical sub-organization invitation", + recipients: emails as string[], + substitutions: { + subOrganizationName: orgDetails.slug, + callback_url: `${appCfg.SITE_URL}/organization/projects?subOrganization=${orgDetails.slug}` + } + }); + } else { + await Promise.allSettled( + newUsers.map(async (el) => { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: el.id, + orgId: dto.permission.orgId + }); - await Promise.allSettled( - newUsers.map(async (el) => { - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: el.id, - orgId: dto.permission.orgId - }); + if (el.email) { + if (!appCfg.isSmtpConfigured) { + signUpTokens.push({ + email: el.email, + link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}` + }); + } - if (el.email) { - if (!appCfg.isSmtpConfigured) { - signUpTokens.push({ - email: el.email, - link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}` + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [el.email], + substitutions: { + inviterFirstName: actorDetails?.firstName, + inviterUsername: actorDetails?.email, + organizationName: orgDetails?.name, + email: el.email, + organizationId: orgDetails?.id.toString(), + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } }); } - - await smtpService.sendMail({ - template: SmtpTemplates.OrgInvite, - subjectLine: "Infisical organization invitation", - recipients: [el.email], - substitutions: { - inviterFirstName: actorDetails?.firstName, - inviterUsername: actorDetails?.email, - organizationName: orgDetails?.name, - email: el.email, - organizationId: orgDetails?.id.toString(), - token, - callback_url: `${appCfg.SITE_URL}/signupinvite` - } - }); - } - }) - ); + }) + ); + } return { signUpTokens }; }; const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); }; const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); }; const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); }; const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( dto ) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); }; diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index 23ed61402..ff17daa75 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -9,6 +9,7 @@ import { import { CronJob } from "cron"; import { FastifyReply, FastifyRequest } from "fastify"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; @@ -208,13 +209,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -282,13 +284,14 @@ export const microsoftTeamsServiceFactory = ({ description, redirectUri }: TCreateMicrosoftTeamsIntegrationDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -393,13 +396,14 @@ export const microsoftTeamsServiceFactory = ({ }); }; const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -427,13 +431,14 @@ export const microsoftTeamsServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetMicrosoftTeamsIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -463,13 +468,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -495,13 +501,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -549,13 +556,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -577,13 +585,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 4c080717d..4f4a9b08c 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -44,13 +44,14 @@ export const orgAdminServiceFactory = ({ actorOrgId, actorAuthMethod }: TListOrgProjectsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole @@ -76,13 +77,14 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole diff --git a/backend/src/services/org/org-bot-dal.ts b/backend/src/services/org/org-bot-dal.ts deleted file mode 100644 index b2ee54758..000000000 --- a/backend/src/services/org/org-bot-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TOrgBotDALFactory = ReturnType; - -export const orgBotDALFactory = (db: TDbClient) => { - const orgBotOrm = ormify(db, TableName.OrgBot); - return orgBotOrm; -}; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d987c890c..c2565f36b 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -26,6 +26,7 @@ import { } from "@app/lib/knex"; import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; +import { ActorType } from "../auth/auth-type"; import { OrgAuthMethod } from "./org-types"; export type TOrgDALFactory = ReturnType; @@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => { const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { return db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereIn(`${TableName.Organization}.id`, orgIdSubquery) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) @@ -154,11 +156,49 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const listSubOrganizations = async (dto: { + actorId: string; + actorType: ActorType; + orgId: string; + isAccessible?: boolean; + limit?: number; + offset?: number; + }) => { + try { + // TODO(sub-org:group): check this when implement group support + const query = db + .replicaNode()(TableName.Organization) + .where(`${TableName.Organization}.rootOrgId`, dto.orgId) + .select(selectAllTableCols(TableName.Organization)); + + if (dto.isAccessible) { + void query + .leftJoin(`${TableName.Membership}`, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .where((qb) => { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + if (dto.actorType === ActorType.IDENTITY) { + void qb.andWhere(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb.andWhere(`${TableName.Membership}.actorUserId`, dto.actorId); + } + }); + } + if (dto.limit) void query.limit(dto.limit); + if (dto.offset) void query.offset(dto.offset); + + const orgs = await query; + return orgs; + } catch (error) { + throw new DatabaseError({ error, name: "List sub organization" }); + } + }; + const findOrgById = async (orgId: string) => { try { const org = (await db .replicaNode()(TableName.Organization) .where({ [`${TableName.Organization}.id` as "id"]: orgId }) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -195,6 +235,7 @@ export const orgDALFactory = (db: TDbClient) => { try { const org = (await db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( @@ -240,6 +281,7 @@ export const orgDALFactory = (db: TDbClient) => { .whereNotNull(`${TableName.Membership}.actorUserId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -337,6 +379,7 @@ export const orgDALFactory = (db: TDbClient) => { } }; + // TODO(sub-org): updated this logic later const countAllOrgMembers = async (orgId: string) => { try { interface CountResult { @@ -610,6 +653,7 @@ export const orgDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) @@ -648,6 +692,7 @@ export const orgDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Membership) .where({ actorIdentityId: identityId }) .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) @@ -662,11 +707,30 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findRootOrgDetails = async (orgId: string, tx?: Knex): Promise => { + try { + const org = await (tx ?? db.replicaNode())(TableName.Organization) + .select(selectAllTableCols(TableName.Organization)) + .where( + "id", + db(TableName.Organization) + .select(db.raw(`CASE WHEN "rootOrgId" IS NULL THEN id ELSE "rootOrgId" END`)) + .where("id", orgId) + ) + .first(); + + return org; + } catch (error) { + throw new DatabaseError({ error, name: "FindRootOrgDetails" }); + } + }; + return withTransaction(db, { ...orgOrm, findOrgByProjectId, findAllOrgMembers, countAllOrgMembers, + listSubOrganizations, findOrgById, findOrgBySlug, findAllOrgsByUserId, @@ -684,6 +748,7 @@ export const orgDALFactory = (db: TDbClient) => { deleteMembershipById, deleteMembershipsById, updateMembership, - findIdentityOrganization + findIdentityOrganization, + findRootOrgDetails }); }; diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 78d52e816..b887eeea1 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -13,7 +13,7 @@ import { TMembershipUserDALFactory } from "../membership-user/membership-user-da type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick; membershipUserDAL: Pick; membershipRoleDAL: Pick; @@ -34,19 +34,9 @@ export const deleteOrgMembershipsFn = async ({ userId, membershipUserDAL, userGroupMembershipDAL, - membershipRoleDAL, additionalPrivilegeDAL }: TDeleteOrgMemberships) => { const deletedMemberships = await orgDAL.transaction(async (tx) => { - await membershipRoleDAL.delete( - { - $in: { - membershipId: orgMembershipIds - } - }, - tx - ); - const orgMemberships = await membershipUserDAL.delete( { scopeOrgId: orgId, @@ -83,12 +73,13 @@ export const deleteOrgMembershipsFn = async ({ ); // Get all the project memberships of the users in the organization + const childOrgs = await orgDAL.find({ rootOrgId: orgId }, { tx }); // Delete all the project memberships of the users in the organization const otherMemberships = await membershipUserDAL.delete( { - scopeOrgId: orgId, $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)), actorUserId: membershipUserIds } }, @@ -96,7 +87,9 @@ export const deleteOrgMembershipsFn = async ({ ); const orgGroups = await membershipUserDAL.find({ - scopeOrgId: orgId, + $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)) + }, $notNull: ["actorGroupId"] }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5b98b44b1..51d907e05 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -4,6 +4,7 @@ import { Knex } from "knex"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -57,7 +58,6 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge- import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; -import { TOrgBotDALFactory } from "./org-bot-dal"; import { TOrgDALFactory } from "./org-dal"; import { deleteOrgMembershipsFn } from "./org-fns"; import { @@ -81,7 +81,6 @@ type TOrgServiceFactoryDep = { secretV2BridgeDAL: Pick; folderDAL: Pick; orgDAL: TOrgDALFactory; - orgBotDAL: TOrgBotDALFactory; roleDAL: TRoleDALFactory; userDAL: TUserDALFactory; groupDAL: TGroupDALFactory; @@ -135,7 +134,6 @@ export const orgServiceFactory = ({ projectKeyDAL, orgMembershipDAL, tokenService, - orgBotDAL, licenseService, samlConfigDAL, oidcConfigDAL, @@ -156,16 +154,31 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + rootOrgId: string, + actorOrgId: string ) => { - await permissionService.getOrgPermission(ActorType.USER, userId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, + orgId, + actorAuthMethod, + actorOrgId: rootOrgId, + scope: OrganizationActionScope.Any + }); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - if (!org.userTokenExpiration) { - return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME }; + + const hasSubOrg = actorOrgId !== rootOrgId; + let subOrg; + if (hasSubOrg) { + subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); } - return org; + + if (!org.userTokenExpiration) { + return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; + } + return { ...org, subOrganization: subOrg }; }; /* * Get all organization a user part of @@ -192,15 +205,16 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); @@ -208,7 +222,14 @@ export const orgServiceFactory = ({ }; const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; @@ -222,7 +243,14 @@ export const orgServiceFactory = ({ orgId, emails }: TFindOrgMembersByEmailDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findOrgMembersByUsername(orgId, emails); @@ -309,13 +337,14 @@ export const orgServiceFactory = ({ actorAuthMethod, orgId }: TUpgradePrivilegeSystemDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, actorId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ @@ -380,7 +409,14 @@ export const orgServiceFactory = ({ } }: TUpdateOrgDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); if (allowSecretSharingOutsideOrganization !== undefined) { @@ -477,6 +513,12 @@ export const orgServiceFactory = ({ } } + if (slug) { + const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null }); + if (existingOrg && existingOrg?.id !== orgId) + throw new BadRequestError({ message: `Organization with slug ${slug} already exist` }); + } + if (googleSsoAuthEnforced) { if (googleSsoAuthEnforced && currentOrg.authEnforced) { throw new BadRequestError({ @@ -567,23 +609,6 @@ export const orgServiceFactory = ({ }, trx?: Knex ) => { - const { privateKey, publicKey } = await crypto.encryption().asymmetric().generateKeyPair(); - const key = crypto.randomBytes(32).toString("base64"); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(key); - const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail); const createOrg = async (tx: Knex) => { @@ -611,30 +636,13 @@ export const orgServiceFactory = ({ tx ); } - await orgBotDAL.create( - { - name: org.name, - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: org.id, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + return org; }; const organization = await (trx ? createOrg(trx) : orgDAL.transaction(createOrg)); - await licenseService.updateSubscriptionOrgMemberCount(organization.id); + await licenseService.updateSubscriptionOrgMemberCount(organization.id, trx); return organization; }; @@ -656,15 +664,16 @@ export const orgServiceFactory = ({ ipAddress: string; orgId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", @@ -744,13 +753,14 @@ export const orgServiceFactory = ({ actorOrgId, metadata }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); const foundMembership = await membershipUserDAL.findOne({ @@ -831,7 +841,14 @@ export const orgServiceFactory = ({ membershipId }: TResendOrgMemberInvitationDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -967,7 +984,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(membershipId); @@ -988,13 +1012,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); const [deletedMembership] = await deleteOrgMembershipsFn({ @@ -1021,13 +1046,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipsDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); if (membershipIds.includes(userId)) { @@ -1059,7 +1085,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TListProjectMembershipsByOrgMembershipIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId); @@ -1080,15 +1113,16 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; @@ -1099,15 +1133,16 @@ export const orgServiceFactory = ({ orgId: string, email: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -1126,15 +1161,16 @@ export const orgServiceFactory = ({ orgId: string, id: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); 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 1a27d131f..48680456c 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -8,7 +8,7 @@ export type TUpdateOrgMembershipDTO = { membershipId: string; role?: string; isActive?: boolean; - actorOrgId: string | undefined; + actorOrgId: string; metadata?: { key: string; value: string }[]; actorAuthMethod: ActorAuthMethod; }; @@ -21,7 +21,7 @@ export type TDeleteOrgMembershipDTO = { userId: string; orgId: string; membershipId: string; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; }; @@ -29,7 +29,7 @@ export type TDeleteOrgMembershipsDTO = { userId: string; orgId: string; membershipIds: string[]; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; }; @@ -54,7 +54,7 @@ export type TVerifyUserToOrgDTO = { export type TFindOrgMembersByEmailDTO = { actor: ActorType; - actorOrgId: string | undefined; + actorOrgId: string; actorId: string; actorAuthMethod: ActorAuthMethod; orgId: string; @@ -64,7 +64,7 @@ export type TFindOrgMembersByEmailDTO = { export type TFindAllWorkspacesDTO = { actor: ActorType; actorId: string; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; orgId: string; }; diff --git a/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts b/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts new file mode 100644 index 000000000..6ed78f665 --- /dev/null +++ b/backend/src/services/pam-account-rotation/pam-account-rotation-queue.ts @@ -0,0 +1,61 @@ +import { TPamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +type TPamAccountRotationServiceFactoryDep = { + queueService: TQueueServiceFactory; + pamAccountService: Pick; +}; + +export type TPamAccountRotationServiceFactory = ReturnType; + +export const pamAccountRotationServiceFactory = ({ + queueService, + pamAccountService +}: TPamAccountRotationServiceFactoryDep) => { + const appCfg = getConfig(); + + const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + + await queueService.stopRepeatableJob( + QueueName.PamAccountRotation, + QueueJobs.PamAccountRotation, + { pattern: "0 * * * *", utc: true }, + QueueName.PamAccountRotation // job id + ); + + await queueService.startPg( + QueueJobs.PamAccountRotation, + async () => { + try { + logger.info(`${QueueName.PamAccountRotation}: pam account rotation task started`); + await pamAccountService.rotateAllDueAccounts(); + logger.info(`${QueueName.PamAccountRotation}: pam account rotation task completed`); + } catch (error) { + logger.error(error, `${QueueName.PamAccountRotation}: pam account rotation failed`); + throw error; + } + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: 5 * 60 + } + ); + + await queueService.schedulePg( + QueueJobs.PamAccountRotation, + "0 * * * *", // Schedule to run every hour + undefined, + { tz: "UTC" } + ); + }; + + return { + init + }; +}; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 2abdebdbc..11d4239db 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -413,10 +413,12 @@ export const projectDALFactory = (db: TDbClient) => { const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => { try { + const subOrgProjects = db.replicaNode()(TableName.Organization).where({ rootOrgId: orgId }).select("id"); + const doc = await (tx || db.replicaNode())(TableName.Project) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where({ orgId }).orWhereIn("orgId", subOrgProjects); } }) .count(); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 58b3c5395..e29f18404 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -5,6 +5,7 @@ import slugify from "@sindresorhus/slugify"; import { AccessScope, ActionProjectType, + OrganizationActionScope, ProjectMembershipRole, ProjectType, ProjectVersion, @@ -245,13 +246,14 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - organization.id, + orgId: organization.id, actorAuthMethod, actorOrgId - ); + }); if ( permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace) && @@ -513,13 +515,14 @@ export const projectServiceFactory = ({ : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); // `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -1822,13 +1825,14 @@ export const projectServiceFactory = ({ projectIds }: TSearchProjectsDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + scope: OrganizationActionScope.Any, + actorOrgId: permission.orgId + }); return projectDAL.searchProjects({ limit, @@ -1846,13 +1850,14 @@ export const projectServiceFactory = ({ const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId, + scope: OrganizationActionScope.Any + }); const projectMember = await permissionService .getProjectPermission({ diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 74c7e95f4..18ae74350 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -41,7 +41,7 @@ export type TCreateProjectDTO = { actor: ActorType; actorAuthMethod: ActorAuthMethod; actorId: string; - actorOrgId?: string; + actorOrgId: string; projectName: string; projectDescription?: string; slug?: string; diff --git a/backend/src/services/role/org/org-role-factory.ts b/backend/src/services/role/org/org-role-factory.ts index 50ffa5e43..f91dabccb 100644 --- a/backend/src/services/role/org/org-role-factory.ts +++ b/backend/src/services/role/org/org-role-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { orgAdminPermissions, orgMemberPermissions, @@ -34,35 +34,38 @@ export const newOrgRoleFactory = ({ const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); }; const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); }; const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ @@ -78,35 +81,38 @@ export const newOrgRoleFactory = ({ }; const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { - const { permission } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 41c825b2e..3387dc96b 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, ActionProjectType, TableName } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, OrganizationActionScope, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -214,13 +214,14 @@ export const roleServiceFactory = ({ const getUserPermission = async (dto: TGetUserPermissionDTO) => { if (dto.scopeData.scope === AccessScope.Organization) { - const { permission, memberships } = await permissionService.getOrgPermission( - dto.permission.type, - dto.permission.id, - dto.permission.orgId, - dto.permission.authMethod, - dto.permission.orgId - ); + const { permission, memberships } = await permissionService.getOrgPermission({ + actorId: dto.permission.id, + actor: dto.permission.type, + orgId: dto.permission.orgId, + actorOrgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + scope: OrganizationActionScope.Any + }); return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails: undefined }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 4cbfcdc7f..87dd207f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,4 +1,4 @@ -import { TSecretSharing } from "@app/db/schemas"; +import { OrganizationActionScope, TSecretSharing } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -81,12 +81,21 @@ export const secretSharingServiceFactory = ({ }: TCreateSharedSecretDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); - const org = await orgDAL.findOrgById(orgId); - if (!org.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { + const rootOrg = await orgDAL.findRootOrgDetails(orgId); + if (!rootOrg) throw new BadRequestError({ message: `Organization with id ${orgId} not found` }); + + if (!rootOrg.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); @@ -100,13 +109,16 @@ export const secretSharingServiceFactory = ({ const expiresAtTimestamp = new Date(expiresAt).getTime(); const lifetime = expiresAtTimestamp - new Date().getTime(); - // org.maxSharedSecretLifetime is in seconds - if (org.maxSharedSecretLifetime && lifetime / 1000 > org.maxSharedSecretLifetime) { + // rootOrg.maxSharedSecretLifetime is in seconds + if (rootOrg.maxSharedSecretLifetime && lifetime / 1000 > rootOrg.maxSharedSecretLifetime) { throw new BadRequestError({ message: "Secret lifetime exceeds organization limit" }); } // Check max view count is within org allowance - if (org.maxSharedSecretViewLimit && (!expiresAfterViews || expiresAfterViews > org.maxSharedSecretViewLimit)) { + if ( + rootOrg.maxSharedSecretViewLimit && + (!expiresAfterViews || expiresAfterViews > rootOrg.maxSharedSecretViewLimit) + ) { throw new BadRequestError({ message: "Secret max views parameter exceeds organization limit" }); } @@ -122,7 +134,10 @@ export const secretSharingServiceFactory = ({ if (allOrgMembers.some((v) => v.user.email === email)) { orgEmails.push(email); // If the email is not part of the org, but access type / org settings require it - } else if (!org.allowSecretSharingOutsideOrganization || accessType === SecretSharingAccessType.Organization) { + } else if ( + !rootOrg.allowSecretSharingOutsideOrganization || + accessType === SecretSharingAccessType.Organization + ) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); @@ -196,7 +211,14 @@ export const secretSharingServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateSecretRequestDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); @@ -228,7 +250,14 @@ export const secretSharingServiceFactory = ({ throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); } - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) { @@ -267,13 +296,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); } @@ -316,13 +346,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); const user = await userDAL.findById(actorId); @@ -415,13 +446,14 @@ export const secretSharingServiceFactory = ({ }: TGetSharedSecretsDTO) => { if (!actorOrgId) throw new ForbiddenRequestError(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const secrets = await secretSharingDAL.find( @@ -563,7 +595,14 @@ export const secretSharingServiceFactory = ({ const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const sharedSecret = isUuidV4(sharedSecretId) diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 2aa495673..081b99208 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -29,6 +30,7 @@ import { type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; userDAL: TUserDALFactory; + orgDAL: Pick; permissionService: Pick; projectEnvDAL: Pick; projectDAL: Pick; @@ -45,7 +47,8 @@ export const serviceTokenServiceFactory = ({ projectEnvDAL, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -184,7 +187,15 @@ export const serviceTokenServiceFactory = ({ if (!isMatch) throw new UnauthorizedError({ message: "Invalid service token" }); await accessTokenQueue.updateServiceTokenStatus(serviceToken.id); - return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; + const serviceTokenOrgDetails = await orgDAL.findById(project.orgId); + + return { + ...serviceToken, + lastUsed: new Date(), + orgId: project.orgId, + parentOrgId: serviceTokenOrgDetails.parentOrgId || serviceTokenOrgDetails.id, + rootOrgId: serviceTokenOrgDetails.rootOrgId || serviceTokenOrgDetails.id + }; }; const notifyExpiringTokens = async () => { diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index c8aa8aaf6..e4110ac11 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { InstallProvider } from "@slack/oauth"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; @@ -230,13 +231,14 @@ export const slackServiceFactory = ({ }: TGetSlackInstallUrlDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -264,13 +266,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -293,13 +296,14 @@ export const slackServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetSlackIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -324,13 +328,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -351,13 +356,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -389,13 +395,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -432,13 +439,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx new file mode 100644 index 000000000..da93fc045 --- /dev/null +++ b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx @@ -0,0 +1,50 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface SubOrganizationInvitationTemplateProps extends Omit { + callback_url: string; + subOrganizationName: string; +} + +export const SubOrganizationInvitationTemplate = ({ + callback_url, + subOrganizationName, + siteUrl +}: SubOrganizationInvitationTemplateProps) => { + return ( + + + You've been invited to join a sub-organization on Infisical + +
+ + You've been invited to join the sub-organization {subOrganizationName}. + +
+
+ Join Sub-Organization +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default SubOrganizationInvitationTemplate; + +SubOrganizationInvitationTemplate.PreviewProps = { + subOrganizationName: "Example Project", + siteUrl: "https://infisical.com", + callback_url: "https://app.infisical.com" +} as SubOrganizationInvitationTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 06ac31ab6..692cacbaf 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -31,4 +31,5 @@ export * from "./SecretScanningSecretsDetectedTemplate"; export * from "./SecretSyncFailedTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./SignupEmailVerificationTemplate"; +export * from "./SubOrganizationInvitationTemplate"; export * from "./UnlockAccountTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 652f56567..cef22009a 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,6 +40,7 @@ import { SecretSyncFailedTemplate, ServiceTokenExpiryNoticeTemplate, SignupEmailVerificationTemplate, + SubOrganizationInvitationTemplate, UnlockAccountTemplate } from "./emails"; @@ -65,6 +66,7 @@ export enum SmtpTemplates { // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", + SubOrgInvite = "subOrganizationInvitation", OrgAssignment = "organizationAssignment", OAuthPasswordReset = "oAuthPasswordReset", ResetPassword = "passwordReset", @@ -102,6 +104,7 @@ export enum SmtpHost { // eslint-disable-next-line @typescript-eslint/no-explicit-any const EmailTemplateMap: Record> = { [SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate, + [SmtpTemplates.SubOrgInvite]: SubOrganizationInvitationTemplate, [SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate, [SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate, [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 84a53f407..63bab2666 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -592,7 +592,7 @@ export const superAdminServiceFactory = ({ }); const { identity, credentials } = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity", orgId: organization.id }, tx); const membership = await membershipIdentityDAL.create( { actorIdentityId: newIdentity.id, diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b54eab8ef..56d7ee635 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto"; @@ -458,13 +458,14 @@ export const userServiceFactory = ({ // This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission. if (user.id !== actorId) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); } diff --git a/backend/src/services/workflow-integration/workflow-integration-service.ts b/backend/src/services/workflow-integration/workflow-integration-service.ts index cb7f7a325..8fea0e240 100644 --- a/backend/src/services/workflow-integration/workflow-integration-service.ts +++ b/backend/src/services/workflow-integration/workflow-integration-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -23,13 +24,14 @@ export const workflowIntegrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetWorkflowIntegrationsByOrg) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 00dc19a46..e60ef1ba5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -77,6 +77,7 @@ services: - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src + - softhsm_tokens:/etc/softhsm2/tokens # SoftHSM tokens are stored in a volume to persist across container restarts extra_hosts: - "host.docker.internal:host-gateway" @@ -198,3 +199,5 @@ volumes: ldap_data: ldap_config: grafana_storage: + softhsm_tokens: + driver: local \ No newline at end of file diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index f93e3b4b2..1f7a08350 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -9,22 +9,93 @@ infisical login ### Description -The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. +The CLI uses authentication to verify your identity. You can authenticate using: +- **Browser Login** (default): Opens a browser for authentication +- **Direct Login**: Provide email and password via flags or environment variables for non-interactive workflows +- **Interactive CLI Login**: Use the `--interactive` flag to enter credentials via CLI prompts + +When authenticated, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. To change where the login credentials are stored, visit the [vaults command](./vault). If you have added multiple users, you can switch between the users by using the [user command](./user). - When you authenticate with **any other method than `user`**, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. - - Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method. - + **JWT Token Output:** + - For **user authentication** with the `--plain --silent` flags: outputs only the JWT access token (useful for scripting) + - For **machine identity authentication**: an access token is always printed to the console + + Use the `--plain` flag to print only the token in plain text and the `--silent` flag to disable update alerts. + + Both flags are ideal for capturing the token in environment variables or CI/CD pipelines. ### Authentication Methods -The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. +The Infisical CLI supports two main categories of authentication: User Authentication and Machine Identity Authentication. + +#### User Authentication + +User authentication is designed for individual developers and supports multiple login flows. + + + + The User authentication method allows you to log in with your email and password. This method supports three different login flows: + + - **Browser Login** (default): Opens a browser for authentication + - **Direct Login**: Provide credentials via flags or environment variables for CI/CD + - **Interactive CLI Login**: Enter credentials via CLI prompts using `--interactive` + + + + + Your email address. Required for direct login along with `--password`. + + + Your password. Required for direct login along with `--email`. + + + Force interactive CLI login instead of browser-based authentication. + + + Output only the JWT token (useful for scripting and CI/CD). + + + + + + + ```bash + infisical login + ``` + + + ```bash + infisical login --email=user@example.com --password=your-password + + # Or using environment variables + export INFISICAL_EMAIL="user@example.com" + export INFISICAL_PASSWORD="your-password" + infisical login + ``` + + + ```bash + infisical login --interactive + ``` + + + ```bash + export INFISICAL_TOKEN=$(infisical login --email=user@example.com --password=your-password --plain --silent) + ``` + + + + + +#### Machine Identity Authentication + +Machine identity authentication methods are designed for automated systems, services, and CI/CD pipelines. @@ -237,7 +308,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Run the `login` command with the following flags to obtain an access token: ```bash - infisical login --method=jwt-auth --jwt= --machine-identity-id= + infisical login --method=jwt-auth --jwt= --machine-identity-id= ``` @@ -262,7 +333,8 @@ The login command supports a number of flags that you can use for different auth - `gcp-id-token`: Login using a GCP ID token native auth. - `gcp-iam`: Login using a GCP IAM. - `aws-iam`: Login using an AWS IAM native auth. - - `oidc-auth`: Login using oidc auth. + - `oidc-auth`: Login using OIDC auth. + - `jwt-auth`: Login using a plain JWT token. @@ -330,22 +402,153 @@ The login command supports a number of flags that you can use for different auth - - - + ```bash - infisical login --oidc-jwt= + infisical login --email= --password= ``` #### Description - The JWT provided by an identity provider for OIDC authentication. + User email address. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--password` flag. - The `oidc-jwt` flag can be substituted with the `INFISICAL_OIDC_AUTH_JWT` environment variable. + You can omit the **--method=user** if you want as it's the default method. + + + + The `email` flag can be substituted with the `INFISICAL_EMAIL` environment variable. + + ```bash + infisical login --email= --password= + ``` + #### Description + User password. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` flag. + + + For security in CI/CD environments, prefer using the `INFISICAL_PASSWORD` environment variable instead of passing the password as a command-line flag. + + + + You can omit the **--method=user** if you want as it's the default method. + + + + The `password` flag can be substituted with the `INFISICAL_PASSWORD` environment variable. + + + + + ```bash + infisical login --interactive + ``` + + #### Description + Forces interactive CLI login where you'll be prompted to enter your email and password in the terminal, instead of opening a browser. + + + + ```bash + infisical login --email= --password= --plain + ``` + + #### Description + When used with direct user login or machine identity authentication, outputs only the JWT access token without any additional formatting. This is useful for scripting and CI/CD pipelines where you need to capture the token. + + ```bash + # Example: Capture token in a variable + export INFISICAL_TOKEN=$(infisical login --email= --password= --plain --silent) + ``` + + + Use it alongside the `silent` flag to disable all messages in the console except from the access token. + + + + + ```bash + infisical login --jwt= --machine-identity-id= + ``` + + #### Description + The JWT provided by an identity provider for OIDC or plain JWT authentication. This is required if the `--method` flag is set to `oidc-auth` or `jwt-auth`. + + + The `jwt` flag can be substituted with the `INFISICAL_JWT` environment variable. + + + + + +### User Authentication Examples + +The following examples demonstrate different ways to authenticate as a user with the Infisical CLI. + + + + By default, running `infisical login` without any flags opens your browser for authentication. + + ```bash + # Opens browser for authentication + infisical login + ``` + + The browser will open to the Infisical login page, and upon successful authentication, the CLI will be automatically authenticated. + + + + + Direct login is ideal for CI/CD pipelines and automation scripts where browser-based authentication is not possible. + + #### Using Command-Line Flags + + ```bash + # Basic direct login (defaults to US Cloud) + infisical login --email user@example.com --password "your-password" + + # EU Cloud (Custom domain) + infisical login --email user@example.com --password "your-password" --domain https://eu.infisical.com + + # Output only JWT token for scripting + export INFISICAL_TOKEN=$(infisical login --email user@example.com --password "your-password" --plain --silent) + ``` + + #### Using Environment Variables (Recommended for CI/CD) + + ```bash + # Set credentials as environment variables + export INFISICAL_EMAIL="user@example.com" + export INFISICAL_PASSWORD="your-password" + + # Login without additional flags + infisical login + + # Or with plain output for token capture + export INFISICAL_TOKEN=$(infisical login --plain --silent) + ``` + + + + Interactive login prompts you to enter credentials in the terminal instead of opening a browser. + + ```bash + # Force interactive CLI login + infisical login --interactive + ``` + + You'll be prompted to enter: + - Email address + - Password + + + + + + +If you have SSO enabled, we recommend using the default browser login. + ### Machine Identity Authentication Quick Start @@ -367,9 +570,9 @@ In this example we'll be using the `universal-auth` method to login to obtain an ``` - + ```bash - infisical secrets --projectId= --env=dev --recursive ``` This command will fetch all secrets from the `dev` environment in your project, including all secrets in subfolders. diff --git a/docs/contributing/getting-started/overview.mdx b/docs/contributing/getting-started/overview.mdx index 35912fc8c..1784b77e8 100644 --- a/docs/contributing/getting-started/overview.mdx +++ b/docs/contributing/getting-started/overview.mdx @@ -7,20 +7,20 @@ To set a strong foundation, this section outlines how we, the community and memb should approach the development and contribution process. ## Code-bases + Infisical has two major code-bases. One for the platform code, and one for SDKs. The contribution process has some key differences between the two, so we've split the documentation into two sections: - The [Infisical Platform](https://github.com/Infisical/infisical), the Infisical platform itself. -- The [Infisical SDK](https://infisical.com/docs/sdks/overview), the official Infisical client SDKs. - - - - - The Infisical platform is the core of the Infisical ecosystem. - - - The SDKs are the official Infisical client libraries, used by developers to easily interact with the Infisical platform. - - +- The Infisical SDKs, please refer to each individual SDK repositories for more information. + - [Node.js SDK](https://github.com/Infisical/node-sdk-v2) + - [Python SDK](https://github.com/Infisical/python-sdk-official) + - [Java SDK](https://github.com/Infisical/java-sdk) + - [.NET SDK](https://github.com/Infisical/infisical-dotnet-sdk) + - [Go SDK](https://github.com/Infisical/go-sdk) + - [C++ SDK](https://github.com/Infisical/infisical-cpp-sdk) + - [PHP SDK](https://github.com/Infisical/php-sdk) + - [Rust SDK](https://github.com/Infisical/rust-sdk) + - [Ruby SDK](https://github.com/infisical/sdk) ## Community @@ -45,15 +45,12 @@ If you're ever in doubt about whether or not a proposed feature aligns with Infi ## Writing and submitting code -Anyone can contribute code to Infisical. To get started, check out the local development guides for each language. - -- Local development guide for Platform is [here](/contributing/platform/developing). -- Local development guide for SDK is [here](/contributing/sdk/developing). +Anyone can contribute code to Infisical. To get started, check out the local development guide for the platform: +- Local development guide for Platform is [here](/contributing/platform/developing). ## Licensing Most of Infisical's code is under the MIT license, though some paid feature restrictions are covered by a proprietary license. Any third party components incorporated into our code are licensed under the original license provided by the applicable component owner. - diff --git a/docs/docs.json b/docs/docs.json index d0ae7cfb5..7ff7f6ce3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -778,6 +778,15 @@ ] } ] + }, + { + "item": "Infisical PAM", + "groups": [ + { + "group": "Infisical PAM", + "pages": ["documentation/platform/pam/overview"] + } + ] } ] }, diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index f773019ec..e10d594da 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -38,3 +38,4 @@ Infisical consists of several tightly integrated products, each designed to solv - [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. - [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. - [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. +- [Infisical PAM](/documentation/platform/pam/overview): Manage access to resources like databases, servers, and accounts with policy-based controls and approvals. diff --git a/docs/documentation/getting-started/overview.mdx b/docs/documentation/getting-started/overview.mdx index 769990987..f51136278 100644 --- a/docs/documentation/getting-started/overview.mdx +++ b/docs/documentation/getting-started/overview.mdx @@ -40,6 +40,12 @@ description: "The open source platform for managing secrets, certificates, and s > Replace static SSH keys with short-lived SSH certificates to simplify access and improve security. + + Manage access to resources like databases, servers, and accounts with policy-based controls and approvals. + diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index 7e40f85f9..d7b7663a9 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -13,7 +13,7 @@ Each identity must authenticate with the Infisical API using a supported authent Key Features: -- Role Assignment: Identities must be assigned [roles](/documentation/platform/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level. +- Role Assignment: Identities must be assigned [roles](/documentation/platform/access-controls/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level. - Auth/Token Configuration: Identities must be configured with corresponding authentication methods and access token properties to securely interact with the Infisical API. ## Workflow diff --git a/docs/documentation/platform/pam/overview.mdx b/docs/documentation/platform/pam/overview.mdx new file mode 100644 index 000000000..a6e0094f5 --- /dev/null +++ b/docs/documentation/platform/pam/overview.mdx @@ -0,0 +1,45 @@ +--- +title: "Infisical PAM" +sidebarTitle: "Overview" +description: "Learn how to manage access to resources like databases, servers, and accounts with policy-based controls and approvals." +--- + +Infisical Privileged Access Management (PAM) provides a centralized way to manage and secure access to your critical infrastructure. It allows you to enforce fine-grained, policy-based controls over resources like databases, servers, and more, ensuring that only authorized users can access sensitive systems, and only when they need to. + +### How it Works + +Infisical PAM employs a resource-based model to organize and manage access. This model is designed to be intuitive and scalable. + +#### 1. Create a Resource + +The first step is to define a resource you want to manage. A resource represents a target system, such as a PostgreSQL database. When creating a resource, you'll provide the necessary connection details, like the host and port. + +![Create Resource](/images/pam/overview/create-resource.png) + +#### 2. Add Accounts to the Resource + +Once a resource is created, you can add accounts to it. An account represents a specific set of credentials (e.g., a username and password) that can be used to access the resource. This allows you to manage multiple sets of credentials for a single database or server from one place. + +![Create Account](/images/pam/overview/create-account.png) + +### Infisical PAM Features + +#### Session Logging and Auditing + +- **Session Logging**: All user sessions are extensively logged, providing a detailed and searchable record of activities performed during a session. +- **Audit Logging**: Every significant event, such as a user starting a session or accessing an account's credentials, is recorded in audit logs. This gives you complete visibility over your project. + +![Session Page](/images/pam/overview/session-page.png) + +#### Automated Credential Rotation + +Infisical PAM can automatically rotate account credentials to enhance your security posture. + +Here’s how it works: +1. **Add a Rotation Account**: On the resource level, you configure a "rotation account." This is a master or privileged account that has the necessary permissions to change the passwords of other accounts on that same resource. +![Credential Rotation Account](/images/pam/overview/credential-rotation-account.png) + +2. **Configure Rotation on Accounts**: For each individual account you want to rotate, you can simply enable rotation and set a desired interval (e.g., every 30 days). +![Rotate Credentials Account](/images/pam/overview/rotate-credentials-account.png) + +Infisical will then use the rotation account on the resource to automatically update the credentials of the target account at the specified interval, eliminating credential staleness. diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index 7d0df2e22..f2570f290 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -22,6 +22,7 @@ The supported project types are: - [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. - [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. - [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. +- [Infisical PAM](/documentation/platform/pam/overview): Manage access to resources like databases, servers, and accounts with policy-based controls and approvals. ## Roles and Access Control diff --git a/docs/images/pam/overview/create-account.png b/docs/images/pam/overview/create-account.png new file mode 100644 index 000000000..34f1c7434 Binary files /dev/null and b/docs/images/pam/overview/create-account.png differ diff --git a/docs/images/pam/overview/create-resource.png b/docs/images/pam/overview/create-resource.png new file mode 100644 index 000000000..ac34b9dca Binary files /dev/null and b/docs/images/pam/overview/create-resource.png differ diff --git a/docs/images/pam/overview/credential-rotation-account.png b/docs/images/pam/overview/credential-rotation-account.png new file mode 100644 index 000000000..5e379eccc Binary files /dev/null and b/docs/images/pam/overview/credential-rotation-account.png differ diff --git a/docs/images/pam/overview/rotate-credentials-account.png b/docs/images/pam/overview/rotate-credentials-account.png new file mode 100644 index 000000000..3c908cd49 Binary files /dev/null and b/docs/images/pam/overview/rotate-credentials-account.png differ diff --git a/docs/images/pam/overview/session-page.png b/docs/images/pam/overview/session-page.png new file mode 100644 index 000000000..5c2fa41cf Binary files /dev/null and b/docs/images/pam/overview/session-page.png differ diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 3c9e743ff..e3f72f61b 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -24,7 +24,7 @@ const SCOPE_NAME: Record, { label: string; icon: Ico [ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.SecretScanning]: { label: "Project", icon: faCube }, - namespace: { label: "Namespace", icon: faCubes }, + namespace: { label: "Sub-Organization", icon: faCubes }, instance: { label: "Server", icon: faServer } }; diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index a37b38cb6..16b8b4f45 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -24,6 +24,8 @@ apiRequest.interceptors.request.use((config) => { const token = getAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken(); + const params = new URLSearchParams(window.location.search); + if (config.headers) { if (signupTempToken) { // eslint-disable-next-line no-param-reassign @@ -38,6 +40,17 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } + + const rootOrgHeader = config.headers.get("x-root-org"); + + if (rootOrgHeader) { + config.headers.delete("x-root-org"); + } else { + const subOrganization = params.get("subOrganization"); + if (subOrganization) { + config.headers.set("x-infisical-org", subOrganization); + } + } } return config; diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 2e835a1db..410df7440 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -33,10 +33,6 @@ export const ROUTE_PATHS = Object.freeze({ "/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ), - SecretSharingSettings: setRoute( - "/organization/secret-sharing/settings", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" - ), SettingsPage: setRoute( "/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/settings/" diff --git a/frontend/src/consts/pam.ts b/frontend/src/consts/pam.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index bcab6169e..87dc40263 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -62,7 +62,8 @@ export enum OrgPermissionSubjects { SecretShare = "secret-share", GithubOrgSync = "github-org-sync", GithubOrgSyncManual = "github-org-sync-manual", - MachineIdentityAuthTemplate = "machine-identity-auth-template" + MachineIdentityAuthTemplate = "machine-identity-auth-template", + SubOrganization = "sub-organization" } export enum OrgPermissionAdminConsoleAction { @@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions { RemoveMembers = "remove-members" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -151,6 +157,7 @@ export type OrgPermissionSet = | OrgPermissionSubjects.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 26865b5bf..bfc17d143 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,5 +1,6 @@ +import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { useRouteContext } from "@tanstack/react-router"; +import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; @@ -9,11 +10,29 @@ export const useOrganization = () => { select: (el) => el.organizationId }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); + const { data: currentOrg } = useSuspenseQuery({ - queryKey: organizationKeys.getOrgById(organizationId), + queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), queryFn: () => fetchOrganizationById(organizationId), staleTime: Infinity }); - return { currentOrg }; + const org = useMemo( + () => ({ + currentOrg: { + ...currentOrg, + id: currentOrg?.subOrganization?.id || currentOrg?.id, + parentOrgId: currentOrg.id + }, + isSubOrganization: Boolean(currentOrg.subOrganization), + isRootOrganization: !currentOrg.subOrganization + }), + [currentOrg, subOrganization] + ); + + return org; }; diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index 529a74930..762b067c2 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -24,7 +24,7 @@ export const withPermission = ( // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied - if (permission.cannot(action as any, subject)) { + if (permission.cannot(action as any, subject as any)) { return (
{ + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}`, + { roles } + ); + return data.identityMembership; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); + } + }); +}; + +export const useDeleteOrgIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}` + ); + return data.identityMembership; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); + } + }); +}; diff --git a/frontend/src/hooks/api/orgIdentityMembership/types.ts b/frontend/src/hooks/api/orgIdentityMembership/types.ts new file mode 100644 index 000000000..95fa06b82 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/types.ts @@ -0,0 +1,32 @@ +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export type TOrgIdentityMembership = { + id: string; + orgId: string; + identityId: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateOrgIdentityMembershipDTO = { + identityId: string; + roles: Array< + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: TemporaryPermissionMode; + temporaryRange: string; + temporaryAccessStartTime: string; + } + >; +}; + +export type TDeleteOrgIdentityMembershipDTO = { + identityId: string; +}; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index f4627a614..7c283691e 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -6,6 +6,7 @@ export { useDeleteOrgById, useDeleteOrgPmtMethod, useDeleteOrgTaxId, + useGetAvailableOrgIdentities, useGetIdentityMembershipOrgs, useGetOrganizationGroups, useGetOrganizations, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 15e1b861c..bbf73dd25 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -42,7 +42,9 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgById: (orgId: string) => ["organization", { orgId }] + getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }], + getAvailableIdentities: () => ["available-identities"], + getAvailableUsers: () => ["available-users"] }; export const fetchOrganizations = async () => { @@ -64,7 +66,9 @@ export const useGetOrganizations = () => { export const fetchOrganizationById = async (id: string) => { const { data: { organization } - } = await apiRequest.get<{ organization: Organization }>(`/api/v1/organization/${id}`); + } = await apiRequest.get<{ + organization: Organization & { subOrganization?: { id: string; name: string } }; + }>(`/api/v1/organization/${id}`); return organization; }; @@ -572,3 +576,29 @@ export const useGetOrgIntegrationAuths = ( select }); }; + +export const useGetAvailableOrgIdentities = (enabled = true) => + useQuery({ + queryKey: organizationKeys.getAvailableIdentities(), + queryFn: async () => { + const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>( + "/api/v1/organization/identities/available" + ); + + return data.identities; + }, + enabled + }); + +export const useGetAvailableOrgUsers = (enabled = true) => + useQuery({ + queryKey: organizationKeys.getAvailableUsers(), + queryFn: async () => { + const { data } = await apiRequest.get<{ + users: { username: string; id: string; firstName: string; lastName: string }[]; + }>("/api/v1/organization/users/available"); + + return data.users; + }, + enabled + }); diff --git a/frontend/src/hooks/api/pam/constants.ts b/frontend/src/hooks/api/pam/constants.ts new file mode 100644 index 000000000..8cdbd3324 --- /dev/null +++ b/frontend/src/hooks/api/pam/constants.ts @@ -0,0 +1 @@ +export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__"; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 288d65ab9..6339b4761 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -3,6 +3,7 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; +import { PamResourceType } from "./enums"; import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; export const pamKeys = { @@ -12,6 +13,12 @@ export const pamKeys = { session: () => [...pamKeys.all, "session"] as const, listResourceOptions: () => [...pamKeys.resource(), "options"] as const, listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], + getResource: (resourceType: string, resourceId: string) => [ + ...pamKeys.resource(), + "get", + resourceType, + resourceId + ], listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] @@ -68,6 +75,28 @@ export const useListPamResources = ( }); }; +export const useGetPamResourceById = ( + resourceType?: PamResourceType, + resourceId?: string, + options?: Omit< + UseQueryOptions>, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: pamKeys.getResource(resourceType || "", resourceId || ""), + queryFn: async () => { + const { data } = await apiRequest.get<{ resource: TPamResource }>( + `/api/v1/pam/resources/${resourceType}/${resourceId}` + ); + + return data.resource; + }, + enabled: !!resourceId && !!resourceType && (options?.enabled ?? true), + ...options + }); +}; + // Accounts export const useListPamAccounts = ( projectId: string, diff --git a/frontend/src/hooks/api/pam/types/base-account.ts b/frontend/src/hooks/api/pam/types/base-account.ts index 9f45b1a4a..20cb7aa60 100644 --- a/frontend/src/hooks/api/pam/types/base-account.ts +++ b/frontend/src/hooks/api/pam/types/base-account.ts @@ -9,9 +9,13 @@ export interface TBasePamAccount { id: string; name: string; resourceType: PamResourceType; + rotationCredentialsConfigured: boolean; }; name: string; description?: string | null; + rotationEnabled: boolean; + rotationIntervalSeconds?: number | null; + lastRotatedAt?: string | null; createdAt: string; updatedAt: string; } diff --git a/frontend/src/hooks/api/pam/types/postgres-resource.ts b/frontend/src/hooks/api/pam/types/postgres-resource.ts index 513610be1..b1b5b7487 100644 --- a/frontend/src/hooks/api/pam/types/postgres-resource.ts +++ b/frontend/src/hooks/api/pam/types/postgres-resource.ts @@ -6,6 +6,7 @@ import { TBasePamResource } from "./base-resource"; // Resources export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & { connectionDetails: TBaseSqlConnectionDetails; + rotationAccountCredentials?: TBaseSqlCredentials | null; }; // Accounts diff --git a/frontend/src/hooks/api/subOrganizations/index.tsx b/frontend/src/hooks/api/subOrganizations/index.tsx new file mode 100644 index 000000000..480377464 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/index.tsx @@ -0,0 +1,8 @@ +export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations"; +export { subOrganizationsQuery } from "./queries"; +export type { + TCreateSubOrganizationDTO, + TListSubOrganizationsDTO, + TSubOrganization, + TUpdateSubOrganizationDTO +} from "./types"; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx new file mode 100644 index 000000000..81369a62e --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { subOrganizationsQuery } from "./queries"; +import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types"; + +export const useCreateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto: TCreateSubOrganizationDTO) => { + const { data } = await apiRequest.post<{ organization: TSubOrganization }>( + "/api/v1/sub-organizations", + dto, + { + headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header + } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; + +export const useUpdateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => { + const { data } = await apiRequest.patch<{ organization: TSubOrganization }>( + `/api/v1/sub-organizations/${subOrgId}`, + { name } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/subOrganizations/queries.tsx b/frontend/src/hooks/api/subOrganizations/queries.tsx new file mode 100644 index 000000000..99ccb7770 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/queries.tsx @@ -0,0 +1,28 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TListSubOrganizationsDTO, TSubOrganization } from "./types"; + +export const subOrganizationsQuery = { + allKey: () => ["sub-organizations"] as const, + listKey: (params?: TListSubOrganizationsDTO) => + [...subOrganizationsQuery.allKey(), "list", params] as const, + list: (params: TListSubOrganizationsDTO) => + queryOptions({ + queryKey: subOrganizationsQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ organizations: TSubOrganization[] }>( + "/api/v1/sub-organizations", + { + params: { + limit: params.limit, + offset: params.offset, + isAccessible: params.isAccessible + } + } + ); + return data.organizations; + } + }) +}; diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts new file mode 100644 index 000000000..e9b3f2f01 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -0,0 +1,23 @@ +export type TSubOrganization = { + id: string; + name: string; + slug: string; + createdAt: string; + updatedAt: string; + parentOrgId: string; +}; + +export type TCreateSubOrganizationDTO = { + name: string; +}; + +export type TListSubOrganizationsDTO = { + limit?: number; + offset?: number; + isAccessible?: boolean; +}; + +export type TUpdateSubOrganizationDTO = { + subOrgId: string; + name: string; +}; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 5c0fa687b..98daf3ec1 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -13,6 +13,7 @@ export type SubscriptionPlan = { customRateLimits: boolean; pitRecovery: boolean; githubOrgSync: boolean; + subOrganization?: boolean; ipAllowlisting: boolean; rbac: boolean; secretVersioning: boolean; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index f3182ab83..f9ef51133 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -6,12 +6,15 @@ import { faBook, faCaretDown, faCheck, + faChevronRight, + faCubes, faEnvelope, faExclamationTriangle, faGlobe, faInfinity, faInfo, faInfoCircle, + faPlus, faServer, faSignOut, faToolbox, @@ -19,7 +22,7 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; @@ -34,6 +37,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, IconButton, Modal, ModalContent, @@ -44,15 +50,22 @@ import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; -import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; +import { + projectKeys, + subOrganizationsQuery, + useGetOrganizations, + useGetOrgTrialUrl, + useLogoutUser +} from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; -import { SubscriptionPlan } from "@app/hooks/api/types"; +import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; +import { NewSubOrganizationForm } from "./NewSubOrganizationForm"; import { NotificationDropdown } from "./NotificationDropdown"; const getPlan = (subscription: SubscriptionPlan) => { @@ -119,10 +132,18 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); + const [showAdminsModal, setShowAdminsModal] = useState(false); + const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true }); + const { data: subOrganizations = [] } = useQuery({ + ...subOrgQuery, + enabled: Boolean(subscription.subOrganization) + }); + useEffect(() => { if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { sessionStorage.setItem("paymentFailed", "true"); @@ -137,6 +158,7 @@ export const Navbar = () => { const [shouldShowMfa, toggleShowMfa] = useToggle(false); const router = useRouter(); const queryClient = useQueryClient(); + const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false); const location = useLocation(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -161,6 +183,7 @@ export const Navbar = () => { } await router.invalidate(); await navigateUserToOrg(navigate, orgId); + queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); }; const { mutateAsync } = useGetOrgTrialUrl(); @@ -206,6 +229,33 @@ export const Navbar = () => { const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate? + const handleOrgNav = async (org: Organization) => { + if (currentOrg?.id === org.id) return; + + if (org.authEnforced) { + // org has an org-level auth method enabled (e.g. SAML) + // -> logout + redirect to SAML SSO + + await logout.mutateAsync(); + if (org.orgAuthMethod === AuthMethod.OIDC) { + window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`); + } else { + window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`); + } + window.close(); + return; + } + + if (org.googleSsoAuthEnforced) { + await logout.mutateAsync(); + window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); + window.close(); + return; + } + + handleOrgChange(org?.id); + }; + return (
@@ -235,38 +285,45 @@ export const Navbar = () => { ) : ( <>
- - -
- - -

{currentOrg?.name}

-
-
- {getPlan(subscription)} -
- {subscription.cardDeclined && ( - -
- -
-
+ +
+ { + navigate({ + to: "/organization/projects", + search: (search) => ({ ...search, subOrganization: undefined }) + }); + if (isSubOrganization) { + await router.invalidate({ sync: true }).catch(() => null); + } + }} + variant="org" + className={twMerge( + "max-w-full min-w-0 cursor-pointer text-sm", + (!isOrgScope || isSubOrganization) && + "bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" )} + > + +

{currentOrg?.name}

+
+
+ {getPlan(subscription)}
- + {subscription.cardDeclined && ( + +
+ +
+
+ )} +
{
- organizations + Organizations
{orgs?.map((org) => { + if ( + subscription.subOrganization && + (org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId) + ) { + return ( + + { + setIsOrgSelectOpen(false); + handleOrgNav(org); + }} + className="cursor-pointer font-normal" + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+ +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + ); + } + return ( - - + handleOrgNav(org)} + className="cursor-pointer font-normal" + key={org.id} + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+
); })} @@ -345,6 +439,78 @@ export const Navbar = () => {
+ {currentOrg.subOrganization && ( + <> +

/

+ + + + +

{currentOrg.subOrganization.name}

+
+ + +
+ + + +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + + )} {!isOrgScope && ( <>

/

@@ -517,6 +683,7 @@ export const Navbar = () => { + {
+ + +
+ { + setShowSubOrgForm(false); + }} + /> +
+
+
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx new file mode 100644 index 000000000..75041a69e --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -0,0 +1,94 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useCreateSubOrganization } from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +type ContentProps = { + onClose: () => void; +}; + +const AddOrgSchema = z.object({ + name: slugSchema() +}); + +type FormData = z.infer; + +export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { + const createSubOrg = useCreateSubOrganization(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: "" + }, + resolver: zodResolver(AddOrgSchema) + }); + + const navigate = useNavigate(); + const router = useRouter(); + + const onSubmit = async ({ name }: FormData) => { + try { + const { organization } = await createSubOrg.mutateAsync({ + name + }); + + createNotification({ + type: "success", + text: "Successfully created sub organization" + }); + onClose(); + + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: organization.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + } catch { + createNotification({ + text: "Failed to create sub organization", + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + control={control} + name="name" + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index ba34877f9..564970904 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -3,6 +3,7 @@ import { motion } from "framer-motion"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Tab, TabList, Tabs } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { usePopUp } from "@app/hooks"; type Props = { @@ -10,10 +11,13 @@ type Props = { }; export const OrgNavBar = ({ isHidden }: Props) => { + const { isRootOrganization } = useOrganization(); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { pathname } = useLocation(); + const variant = isRootOrganization ? "org" : "namespace"; + return ( <> {!isHidden && ( @@ -30,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( - + Overview )} {({ isActive }) => ( - + App Connections )} {({ isActive }) => ( - + Networking )} {({ isActive }) => ( - + Secret Sharing )} @@ -59,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( { {({ isActive }) => ( - + Audit Logs )} - - {({ isActive }) => ( - - Usage & Billing - - )} - + {isRootOrganization && ( + + {({ isActive }) => ( + + Usage & Billing + + )} + + )} {({ isActive }) => ( - + Settings )} diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index 4e26a7393..72430fec6 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -171,9 +171,21 @@ export const ProjectSelect = () => { to: getProjectHomePage(workspace.type, workspace.environments), params: { projectId: workspace.id + }, + search: { + subOrganization: currentOrg?.subOrganization?.name } }); - window.location.assign(url.to.replaceAll("$projectId", workspace.id)); + const urlInstance = new URL( + `${window.location.origin}/${url.to.replaceAll("$projectId", workspace.id)}` + ); + if (currentOrg?.subOrganization) { + urlInstance.searchParams.set( + "subOrganization", + currentOrg.subOrganization.name + ); + } + window.location.assign(urlInstance); }} icon={ currentWorkspace?.id === workspace.id && ( diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index a002a1716..ce873f70e 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -138,7 +138,7 @@ export const SignupInvitePage = () => { // Step 4 of the sign up process (download the emergency kit pdf) const stepConfirmEmail = ( -
+

Confirm your email

@@ -179,7 +179,7 @@ export const SignupInvitePage = () => { // Because this is the invite signup - we directly go to the last step of signup (email is already verified) const main = ( -
+

Almost there!

diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 8059f71ce..d3e93bcea 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from " export const AccessManagementPage = () => { const { t } = useTranslation(); const { permission } = useOrgPermission(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const navigate = useNavigate({ from: ROUTE_PATHS.Organization.AccessControlPage.path @@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => { {tabSections .filter((el) => !el.isHidden) .map((el) => ( - + {el.label} ))} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx new file mode 100644 index 000000000..b0977437b --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx @@ -0,0 +1,129 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FilterableSelect, FormControl } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api"; +import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership"; + +const schema = z + .object({ + identity: z.object({ name: z.string(), id: z.string() }), + role: z.object({ name: z.string(), slug: z.string() }) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + onClose: () => void; +}; + +export const IdentityLinkForm = ({ onClose }: Props) => { + const navigate = useNavigate(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { data: roles } = useGetOrgRoles(orgId); + + const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership(); + const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: {} + }); + + const onFormSubmit = async ({ identity, role }: FormData) => { + try { + await createMutateAsync({ + identityId: identity.id, + roles: [{ role: role.slug, isTemporary: false }] + }); + createNotification({ + text: "Successfully linked identity", + type: "success" + }); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: identity.id + } + }); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to link identity"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+ ( + + option.id} + getOptionLabel={(option) => option.name} + isLoading={isRootOrgLoading} + /> + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + // menuPortalTarget={document.body} + /> + + )} + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 224af9d5e..dacbba428 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -53,6 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const orgId = currentOrg?.id || ""; const { data: roles } = useGetOrgRoles(orgId); + const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true; const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); @@ -113,6 +114,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { name: string; role: string; hasDeleteProtection: boolean; + orgId: string; }; if (identity) { @@ -196,16 +198,23 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`} >
- ( - - - - )} - /> + {isOrgIdentity && ( + ( + + + + )} + /> + )} { label={`${popUp?.identity?.data ? "Update" : ""} Role`} errorText={error?.message} isError={Boolean(error)} - className="mt-4" > { )} /> - ( - - -

Delete Protection {value ? "Enabled" : "Disabled"}

-
-
- )} - /> + {isOrgIdentity && ( + ( + + +

Delete Protection {value ? "Enabled" : "Disabled"}

+
+
+ )} + /> + )}
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 9280ab5d9..5c15a0cdd 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -1,10 +1,15 @@ -import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowUpRightFromSquare, + faBookOpen, + faLink, + faPlus +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; +import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, @@ -19,6 +24,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal"; import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable"; +import { IdentityLinkForm } from "./IdentityLinkForm"; import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; @@ -27,7 +33,7 @@ import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal export const IdentitySection = withPermission( () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); @@ -43,7 +49,8 @@ export const IdentitySection = withPermission( "createTemplate", "editTemplate", "deleteTemplate", - "viewUsages" + "viewUsages", + "linkIdentity" ] as const); const isMoreIdentitiesAllowed = subscription?.identityLimit @@ -105,8 +112,8 @@ export const IdentitySection = withPermission( return (
-
-
+
+

Identities

+ {isSubOrganization && ( + + {(isAllowed) => ( + + )} + + )} - {/* */} - {/* */} + handlePopUpToggle("linkIdentity", isOpen)} + > + + handlePopUpClose("linkIdentity")} /> + + { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { offset, @@ -286,15 +287,18 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
+ {isSubOrganization && Managed By} {isFetching ? : null} - {isPending && } + {isPending && ( + + )} {!isPending && data?.identities?.map( ({ - identity: { id, name }, + identity: { id, name, orgId }, role, customRole, lastLoginAuthMethod, @@ -362,6 +366,14 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { }} + {isSubOrganization && ( + +

+ + {currentOrg.id === orgId ? "Sub Organization" : "Root Organization"} +

+ + )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx new file mode 100644 index 000000000..290f8db4b --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx @@ -0,0 +1,280 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { RoleOption } from "@app/components/roles"; +import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; +import { + useAddUsersToOrg, + useAddUserToWsNonE2EE, + useGetOrgRoles, + useGetUserProjects +} from "@app/hooks/api"; +import { useGetAvailableOrgUsers } from "@app/hooks/api/organization/queries"; +import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member"; + +const addMemberFormSchema = z.object({ + users: z + .array( + z.object({ + username: z.string().trim(), + email: z.string().trim() + }) + ) + .min(1), + projects: z + .array( + z.object({ + name: z.string(), + id: z.string(), + slug: z.string(), + version: z.nativeEnum(ProjectVersion) + }) + ) + .default([]), + projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG), + organizationRole: z.object({ + name: z.string(), + slug: z.string(), + description: z.string().optional() + }) +}); + +type TAddMemberForm = z.infer; + +type Props = { + onClose: () => void; +}; + +export const AddSubOrgMemberModal = ({ onClose }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); + const { data: members = [], isPending: isMembersPending } = useGetAvailableOrgUsers(); + + const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); + const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); + + const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({ + includeRoles: true + }); + + const { + control, + handleSubmit, + watch, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(addMemberFormSchema) + }); + + // set initial form role based off org default role + useEffect(() => { + if (organizationRoles) { + reset({ + organizationRole: findOrgMembershipRole(organizationRoles, currentOrg.defaultMembershipRole) + }); + } + }, [organizationRoles]); + + const onAddMembers = async ({ + users, + organizationRole, + projects: selectedProjects, + projectRoleSlug + }: TAddMemberForm) => { + if (!currentOrg?.id) return; + + if (selectedProjects?.length) { + // eslint-disable-next-line no-restricted-syntax + for (const project of selectedProjects) { + if (project.version !== ProjectVersion.V3) { + createNotification({ + type: "error", + text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.` + }); + return; + } + } + } + + try { + const usernames = users.map((el) => el.username); + await addUsersMutateAsync({ + organizationId: currentOrg?.id, + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug + }); + + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + onClose(); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to add user to suborganization", + type: "error" + }); + } + }; + + const getGroupHeaderLabel = (type: ProjectType) => { + switch (type) { + case ProjectType.SecretManager: + return "Secrets"; + case ProjectType.CertificateManager: + return "PKI"; + case ProjectType.KMS: + return "KMS"; + case ProjectType.SSH: + return "SSH"; + default: + return "Other"; + } + }; + + return ( + + ( + + option.username} + getOptionLabel={(option) => option.username} + /* eslint-disable-next-line react/no-unstable-nested-components */ + noOptionsMessage={() => ( +

All root organization users are already assigned to this project

+ )} + /> +
+ )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + value={value} + onChange={onChange} + components={{ Option: RoleOption }} + /> + + )} + /> + +
+
+ ( + + project.name} + getOptionValue={(project) => project.id} + options={projects} + groupBy="type" + getGroupHeaderLabel={getGroupHeaderLabel} + placeholder="Select projects..." + /> + + )} + /> +
+
+ ( + +
+ +
+
+ )} + /> +
+
+ +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 010a12b38..a8f5ea059 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -11,6 +11,8 @@ import { Button, DeleteActionModal, EmailServiceSetupModal, + Modal, + ModalContent, Tooltip } from "@app/components/v2"; import { @@ -26,11 +28,12 @@ import { OrgUser } from "@app/hooks/api/users/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { AddOrgMemberModal } from "./AddOrgMemberModal"; +import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal"; import { OrgMembersTable } from "./OrgMembersTable"; export const OrgMembersSection = () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id ?? ""; const { user } = useUser(); const userId = user?.id || ""; @@ -41,6 +44,7 @@ export const OrgMembersSection = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addMember", + "addMemberToSubOrg", "removeMember", "deactivateMember", "upgradePlan", @@ -210,7 +214,9 @@ export const OrgMembersSection = () => { colorSchema="secondary" type="submit" leftIcon={} - onClick={() => handleAddMemberModal()} + onClick={() => + isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal() + } isDisabled={!isAllowed} > Add Member @@ -230,6 +236,14 @@ export const OrgMembersSection = () => { completeInviteLinks={completeInviteLinks} setCompleteInviteLinks={setCompleteInviteLinks} /> + handlePopUpToggle("addMemberToSubOrg", isOpen)} + > + + handlePopUpClose("addMemberToSubOrg")} /> + + { const navigate = useNavigate(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { user } = useUser(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -586,6 +586,7 @@ export const OrgMembersTable = ({ {isActive && (status === "invited" || status === "verified") && email && + !isSubOrganization && serverDetails?.emailConfigured && ( { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -200,7 +200,9 @@ export const OrgRoleTable = () => { return (
-

Organization Roles

+

+ {isSubOrganization ? "Sub-" : ""}Organization Roles +

{(isAllowed) => (
{ const { hasOrgRole } = useOrgPermission(); + const { isSubOrganization } = useOrganization(); return (
- - + {isSubOrganization ? : } + {!isSubOrganization && } {hasOrgRole(OrgMembershipRole.Admin) && }
); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index 9b6323c2e..766a22158 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -41,21 +41,19 @@ export const OrgNameChangeSection = (): JSX.Element => { const [isFormInitialized, setIsFormInitialized] = useState(false); useEffect(() => { - if (currentOrg) { - reset({ - name: currentOrg.name, - slug: currentOrg.slug, - ...(canReadOrgRoles && - roles?.length && { - // will always be present, can't remove role if default - defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) - ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" - : currentOrg.defaultMembershipRole - }) - }); - setIsFormInitialized(true); - } - }, [currentOrg, roles]); + reset({ + name: currentOrg.name, + slug: currentOrg.slug, + ...(canReadOrgRoles && + roles?.length && { + // will always be present, can't remove role if default + defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) + ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" + : currentOrg.defaultMembershipRole + }) + }); + setIsFormInitialized(true); + }, [roles]); const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => { try { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx new file mode 100644 index 000000000..ca625be6c --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -0,0 +1,101 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { useUpdateSubOrganization } from "@app/hooks/api"; + +const formSchema = z.object({ + name: z + .string() + .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") +}); + +type FormData = z.infer; + +export const SubOrgNameChangeSection = (): JSX.Element => { + const { currentOrg } = useOrganization(); + const { permission } = useOrgPermission(); + const navigate = useNavigate(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: currentOrg?.subOrganization?.name || "" + } + }); + const { mutateAsync, isPending } = useUpdateSubOrganization(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + await mutateAsync({ + name, + subOrgId: currentOrg.id + }); + + navigate({ to: "/organization/settings", search: { subOrganization: name } }); + queryClient.invalidateQueries(); + await router.invalidate({ sync: true }); + createNotification({ + text: "Successfully updated sub-organization details", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to update sub-organization details", + type: "error" + }); + } + }; + + return ( +
+
+

Organization Name

+ ( + + + + )} + control={control} + name="name" + /> +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx index 4d86fcddb..70fe29455 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx @@ -1 +1,2 @@ export { OrgNameChangeSection } from "./OrgNameChangeSection"; +export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx index 61ee0c6e9..5be15edad 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx @@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => { })); } }); - }, [currentOrg]); + }, [currentOrg?.id]); const onProductToggle = async (value: boolean, key: string) => { setIsLoading(true); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 66af7df1b..10eb0f6c0 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; +import { useOrganization } from "@app/context"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ExternalMigrationsTab } from "../ExternalMigrationsTab"; @@ -19,20 +20,33 @@ export const OrgTabGroup = () => { const search = useSearch({ from: ROUTE_PATHS.Organization.SettingsPage.id }); + const { isSubOrganization } = useOrganization(); + const tabs = [ { name: "General", key: "tab-org-general", component: OrgGeneralTab }, { name: "SSO", key: "sso-settings", - component: OrgSsoTab + component: OrgSsoTab, + isHidden: isSubOrganization }, { name: "Provisioning", key: "provisioning-settings", - component: OrgProvisioningTab + component: OrgProvisioningTab, + isHidden: isSubOrganization + }, + { + name: "Security", + key: "tab-org-security", + component: OrgSecurityTab, + isHidden: isSubOrganization + }, + { + name: "Encryption", + key: "tab-org-encryption", + component: OrgEncryptionTab }, - { name: "Security", key: "tab-org-security", component: OrgSecurityTab }, - { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Workflow Integrations", key: "workflow-integrations", @@ -57,17 +71,21 @@ export const OrgTabGroup = () => { return ( - {tabs.map((tab) => ( - - {tab.name} - - ))} + {tabs + .filter((el) => !el.isHidden) + .map((tab) => ( + + {tab.name} + + ))} - {tabs.map(({ key, component: Component }) => ( - - - - ))} + {tabs + .filter((el) => !el.isHidden) + .map(({ key, component: Component }) => ( + + + + ))} ); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx index 85580da07..d65f7d785 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx @@ -1,6 +1,5 @@ import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -70,154 +69,129 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTempla return (
- - {popUp?.editRole.isOpen ? ( - - handlePopUpClose("editRole")} - projectTemplate={projectTemplate} - role={editRole} - isDisabled={ - permission.cannot( - OrgPermissionActions.Edit, - OrgPermissionSubjects.ProjectTemplates - ) || - (editRole && !isCustomProjectRole(editRole.slug)) - } - /> -
- - ) : ( - -
-
-
-

Project Roles

-

- {isInfisicalTemplate - ? "Click a role to view the associated permissions" - : "Add, edit and remove roles for this project template"} -

-
- {!isInfisicalTemplate && ( - handlePopUpClose("editRole")} + projectTemplate={projectTemplate} + role={editRole} + isDisabled={ + permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates) || + (editRole && !isCustomProjectRole(editRole.slug)) + } + /> + ) : ( +
+
+
+

Project Roles

+

+ {isInfisicalTemplate + ? "Click a role to view the associated permissions" + : "Add, edit and remove roles for this project template"} +

+
+ {!isInfisicalTemplate && ( + + {(isAllowed) => ( + - )} - + Add Role + )} -
-
- - - - - - - - - - {roles.length ? ( - roles.map((role) => { - return ( - { - if (evt.key === "Enter") { - handlePopUpOpen("editRole", role); - } - }} - onClick={() => handlePopUpOpen("editRole", role)} - > - - - + + + )} + +
NameSlug -
{role.name}{role.slug} - {isCustomProjectRole(role.slug) && ( -
- + )} +
+
+ + + + + + + + + + {roles.length ? ( + roles.map((role) => { + return ( + { + if (evt.key === "Enter") { + handlePopUpOpen("editRole", role); + } + }} + onClick={() => handlePopUpOpen("editRole", role)} + > + + + - - ); - }) - ) : ( - - - )} - -
NameSlug +
{role.name}{role.slug} + {isCustomProjectRole(role.slug) && ( +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + e.preventDefault(); + handlePopUpOpen("removeRole", role); + }} > - {(isAllowed) => ( - { - e.stopPropagation(); - e.preventDefault(); - handlePopUpOpen("removeRole", role); - }} - > - - - )} - -
- )} -
- + + + )} + + + )}
-
-
- handlePopUpToggle("removeRole", isOpen)} - onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} - /> - -
- - )} - + ); + }) + ) : ( +
+ +
+
+
+ handlePopUpToggle("removeRole", isOpen)} + onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} + /> +
+ )}
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx index c542410bd..8d524c41c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -25,93 +24,70 @@ export const ProjectTemplatesSection = () => { return (
- - {editTemplate ? ( - - setEditTemplate(null)} - /> - - ) : ( - -
-

- Create and configure templates with predefined roles and environments to streamline - project setup -

-
-
-

Project Templates

- -
- - Docs - -
-
- - {(isAllowed) => ( - - )} - + {editTemplate ? ( + setEditTemplate(null)} /> + ) : ( +
+

+ Create and configure templates with predefined roles and environments to streamline + project setup +

+
+ + + + {(isAllowed) => ( + + )} +
- - )} - + + setEditTemplate(template)} + isOpen={popUp.addTemplate.isOpen} + onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can create project templates if you switch to Infisical's Enterprise plan." + /> +
+
+ )}
); }; diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index 4ef07e81a..2d433bba8 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -46,7 +46,7 @@ const Page = withPermission( }); const membershipId = search.membershipId as string; const { user } = useUser(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -131,7 +131,7 @@ const Page = withPermission( Users
{userId !== membership.user.id && ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index 680795e2e..bef98e875 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -35,7 +35,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => }); const { user } = useUser(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -214,7 +214,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>

-

)}
- {membership.isActive && + {!isSubOrganization && + membership.isActive && (membership.status === "invited" || membership.status === "verified") && membership.user.email && serverDetails?.emailConfigured && ( diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 79bdbf216..0caf8286c 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,7 +1,14 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, retainSearchParams } from "@tanstack/react-router"; +import { z } from "zod"; import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ - component: OrganizationLayout + component: OrganizationLayout, + validateSearch: z.object({ + subOrganization: z.string().optional() + }), + search: { + middlewares: [retainSearchParams(["subOrganization"])] + } }); diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx index ead2b06e1..8b553e656 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx @@ -34,10 +34,11 @@ const CreateForm = ({ }: CreateFormProps) => { const createPamAccount = useCreatePamAccount(); - console.log({ folderId }); - const onSubmit = async ( - formData: DiscriminativePick + formData: DiscriminativePick< + TPamAccount, + "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" + > ) => { try { const account = await createPamAccount.mutateAsync({ @@ -64,7 +65,13 @@ const CreateForm = ({ switch (resourceType) { case PamResourceType.Postgres: - return ; + return ( + + ); default: throw new Error(`Unhandled resource: ${resourceType}`); } @@ -74,7 +81,10 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => { const updatePamAccount = useUpdatePamAccount(); const onSubmit = async ( - formData: DiscriminativePick + formData: DiscriminativePick< + TPamAccount, + "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" + > ) => { try { const updatedAccount = await updatePamAccount.mutateAsync({ diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index 5bcd459aa..3e5994344 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -1,26 +1,31 @@ +import { useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; -import { TPostgresAccount } from "@app/hooks/api/pam"; +import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; +import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; import { SqlAccountFields } from "./shared/SqlAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; +import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; type Props = { account?: TPostgresAccount; + resourceId?: string; + resourceType?: PamResourceType; onSubmit: (formData: FormData) => Promise; }; -const formSchema = genericAccountFieldsSchema.extend({ +const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({ credentials: BaseSqlAccountSchema }); type FormData = z.infer; -export const PostgresAccountForm = ({ account, onSubmit }: Props) => { +export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmit }: Props) => { const isUpdate = Boolean(account); const form = useForm({ @@ -30,7 +35,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => { ...account, credentials: { ...account.credentials, - password: "******" + password: UNCHANGED_PASSWORD_SENTINEL } } : undefined @@ -41,6 +46,20 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => { formState: { isSubmitting, isDirty } } = form; + const [rotationCredentialsConfigured, setRotationCredentialsConfigured] = useState(false); + + const { data: resource } = useGetPamResourceById(resourceType, resourceId, { + enabled: !account && !!resourceId && !!resourceType + }); + + useEffect(() => { + if (account) { + setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured); + } else { + setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials); + } + }, [account, resource]); + return (
{ > +
diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx index a3aba3b67..7e96fffda 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PostgresResourceForm.tsx @@ -5,9 +5,12 @@ import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam"; +import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; +import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas"; import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas"; import { SqlResourceFields } from "./shared/SqlResourceFields"; +import { SqlRotateAccountFields } from "./shared/SqlRotateAccountFields"; import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields"; type Props = { @@ -17,7 +20,8 @@ type Props = { const formSchema = genericResourceFieldsSchema.extend({ resourceType: z.literal(PamResourceType.Postgres), - connectionDetails: BaseSqlResourceSchema + connectionDetails: BaseSqlResourceSchema, + rotationAccountCredentials: BaseSqlAccountSchema.nullable().optional() }); type FormData = z.infer; @@ -28,17 +32,27 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => { const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: resource ?? { - resourceType: PamResourceType.Postgres, - connectionDetails: { - host: "", - port: 5432, - database: "default", - sslEnabled: true, - sslRejectUnauthorized: true, - sslCertificate: undefined - } - } + defaultValues: resource + ? { + ...resource, + rotationAccountCredentials: resource.rotationAccountCredentials + ? { + ...resource.rotationAccountCredentials, + password: UNCHANGED_PASSWORD_SENTINEL + } + : resource.rotationAccountCredentials + } + : { + resourceType: PamResourceType.Postgres, + connectionDetails: { + host: "", + port: 5432, + database: "default", + sslEnabled: true, + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } }); const { @@ -59,6 +73,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => { selectedTabIndex={selectedTabIndex} setSelectedTabIndex={setSelectedTabIndex} /> +
, - - ]} - > - {isNonConflictingUpload ? ( -
- Are you sure you want to import {createSecretCount} secret - {createSecretCount > 1 ? "s" : ""} to this environment? -
- ) : ( -
-
Your project already contains the following {updateSecretCount} secrets:
-
- {Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {}) - ?.map((key) => key) - .join(", ")} -
-
- Are you sure you want to overwrite these secrets - {createSecretCount > 0 - ? ` and import ${createSecretCount} new - one${createSecretCount > 1 ? "s" : ""}` - : ""} - ? -
-
- )} - - {/* Matrix Import Modal */} { - const cleanImportPath = importPath.replace("/__reserve_replication_", ""); + if (!importPath.includes("/__reserve_replication_")) return undefined; + const cleanImportPath = importPath.split("/__reserve_replication_")[1]; const replicatedFolder = items?.find(({ id }) => id === cleanImportPath); return replicatedFolder; }; diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 277baa912..853864c9b 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -56,7 +56,6 @@ import { Route as organizationAccessManagementPageRouteImport } from './pages/or import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route' import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page' import { Route as adminResourceOverviewPageRouteImport } from './pages/admin/ResourceOverviewPage/route' -import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route' import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route' import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route' import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' @@ -752,14 +751,6 @@ const adminResourceOverviewPageRouteRoute = getParentRoute: () => adminLayoutRoute, } as any) -const organizationSecretSharingSettingsPageRouteRoute = - organizationSecretSharingSettingsPageRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, - } as any) - const organizationRoleByIDPageRouteRoute = organizationRoleByIDPageRouteImport.update({ id: '/roles/$roleId', @@ -2625,13 +2616,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationRoleByIDPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' - path: '/settings' - fullPath: '/organization/secret-sharing/settings' - preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport - } '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': { id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' path: '/resources/overview' @@ -4033,15 +4017,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildr interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren { organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute - organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren = { organizationSecretSharingPageRouteRoute: organizationSecretSharingPageRouteRoute, - organizationSecretSharingSettingsPageRouteRoute: - organizationSecretSharingSettingsPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren = @@ -5094,7 +5075,6 @@ export interface FileRoutesByFullPath { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5328,7 +5308,6 @@ export interface FileRoutesByTo { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5568,7 +5547,6 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren @@ -5818,7 +5796,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6051,7 +6028,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6289,7 +6265,6 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId' - | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId' @@ -6814,8 +6789,7 @@ export const routeTree = rootRoute "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/organization", "children": [ - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" + "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ] }, "/_authenticate/_inject-org-details/_org-layout/organization/settings": { @@ -6865,10 +6839,6 @@ export const routeTree = rootRoute "filePath": "organization/RoleByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": { - "filePath": "organization/SecretSharingSettingsPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" - }, "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview": { "filePath": "admin/ResourceOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 860c677c1..8f48f6fe5 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -21,10 +21,7 @@ const organizationRoutes = route("/organization", [ route("/access-management", "organization/AccessManagementPage/route.tsx"), route("/audit-logs", "organization/AuditLogsPage/route.tsx"), route("/billing", "organization/BillingPage/route.tsx"), - route("/secret-sharing", [ - index("organization/SecretSharingPage/route.tsx"), - route("/settings", "organization/SecretSharingSettingsPage/route.tsx") - ]), + route("/secret-sharing", [index("organization/SecretSharingPage/route.tsx")]), route("/settings", [ index("organization/SettingsPage/route.tsx"), route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx")