diff --git a/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts b/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts new file mode 100644 index 000000000..7646b48a9 --- /dev/null +++ b/backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas/models"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.specificType("adminIdentityIds", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("adminIdentityIds"); + }); + } +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 2c0fd7dc4..01aac280b 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -25,7 +25,8 @@ export const SuperAdminSchema = z.object({ encryptedSlackClientId: zodBuffer.nullable().optional(), encryptedSlackClientSecret: zodBuffer.nullable().optional(), authConsentContent: z.string().nullable().optional(), - pageFrameContent: z.string().nullable().optional() + pageFrameContent: z.string().nullable().optional(), + adminIdentityIds: z.string().array().nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 910cff70f..7835ccfae 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType; const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login"; const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login"; -const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second +const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`; export const licenseServiceFactory = ({ @@ -142,7 +142,10 @@ export const licenseServiceFactory = ({ try { if (instanceType === InstanceType.Cloud) { const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId)); - if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet; + if (cachedPlan) { + logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`); + return JSON.parse(cachedPlan) as TFeatureSet; + } const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); @@ -170,6 +173,8 @@ export const licenseServiceFactory = ({ JSON.stringify(onPremFeatures) ); return onPremFeatures; + } finally { + logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`); } return onPremFeatures; }; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 5779b39f7..ad5291a13 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -9,6 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; 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"; export type TAuthMode = | { @@ -44,6 +45,7 @@ export type TAuthMode = identityName: string; orgId: string; authMethod: null; + isInstanceAdmin?: boolean; } | { authMode: AuthMode.SCIM_TOKEN; @@ -130,13 +132,15 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { } case AuthMode.IDENTITY_ACCESS_TOKEN: { const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const serverCfg = await getServerCfg(); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, identityId: identity.identityId, identityName: identity.name, - authMethod: null + authMethod: null, + isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId) }; if (token?.identityAuth?.oidc) { requestContext.set("identityAuthInfo", { diff --git a/backend/src/server/plugins/auth/superAdmin.ts b/backend/src/server/plugins/auth/superAdmin.ts index f5868f130..4ca9ba373 100644 --- a/backend/src/server/plugins/auth/superAdmin.ts +++ b/backend/src/server/plugins/auth/superAdmin.ts @@ -1,16 +1,18 @@ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify"; import { ForbiddenRequestError } from "@app/lib/errors"; -import { ActorType } from "@app/services/auth/auth-type"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const verifySuperAdmin = ( req: T, _res: FastifyReply, done: HookHandlerDoneFunction ) => { - if (req.auth.actor !== ActorType.USER || !req.auth.user.superAdmin) - throw new ForbiddenRequestError({ - message: "Requires elevated super admin privileges" - }); - done(); + if (isSuperAdmin(req.auth)) { + return done(); + } + + throw new ForbiddenRequestError({ + message: "Requires elevated super admin privileges" + }); }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b9f47cb7e..0f1303263 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -637,6 +637,9 @@ export const registerRoutes = async ( userDAL, identityDAL, userAliasDAL, + identityTokenAuthDAL, + identityAccessTokenDAL, + identityOrgMembershipDAL, authService: loginService, serverCfgDAL: superAdminDAL, kmsRootConfigDAL, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index a1c433650..6eb1804f1 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -98,7 +98,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -139,7 +139,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -171,12 +171,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { identities: IdentitiesSchema.pick({ name: true, id: true - }).array() + }) + .extend({ + isInstanceAdmin: z.boolean() + }) + .array() }) } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -206,7 +210,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -240,7 +244,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -265,7 +269,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }) }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -293,7 +297,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -316,7 +320,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }) }, onRequest: (req, res, done) => { - verifyAuth([AuthMode.JWT])(req, res, () => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { verifySuperAdmin(req, res, done); }); }, @@ -394,4 +398,141 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "DELETE", + url: "/identity-management/identities/:identityId/super-admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identity: IdentitiesSchema.pick({ + name: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const identity = await server.services.superAdmin.deleteIdentitySuperAdminAccess( + req.params.identityId, + req.permission.id + ); + + return { + identity + }; + } + }); + + server.route({ + method: "DELETE", + url: "/user-management/users/:userId/admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + userId: z.string() + }), + response: { + 200: z.object({ + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const user = await server.services.superAdmin.deleteUserSuperAdminAccess(req.params.userId); + + return { + user + }; + } + }); + + server.route({ + method: "POST", + url: "/bootstrap", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + email: z.string().email().trim().min(1), + password: z.string().trim().min(1), + organization: z.string().trim().min(1) + }), + response: { + 200: z.object({ + message: z.string(), + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true, + superAdmin: true + }), + organization: OrganizationsSchema.pick({ + id: true, + name: true, + slug: true + }), + identity: IdentitiesSchema.pick({ + id: true, + name: true + }).extend({ + credentials: z.object({ + token: z.string() + }) // would just be Token AUTH for now + }) + }) + } + }, + handler: async (req) => { + const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({ + ...req.body, + organizationName: req.body.organization + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.AdminInit, + distinctId: user.user.username ?? "", + properties: { + username: user.user.username, + email: user.user.email ?? "", + lastName: user.user.lastName || "", + firstName: user.user.firstName || "" + } + }); + + return { + message: "Successfully bootstrapped instance", + user: user.user, + organization, + identity: machineIdentity + }; + } + }); }; 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 414f8534c..82387a3cc 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 @@ -11,6 +11,7 @@ import { validateAccountIds, validatePrincipalArns } from "@app/services/identity-aws-auth/identity-aws-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -130,7 +131,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 f46fb57ca..1cf59e682 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -8,8 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators"; - -import {} from "../sanitizedSchemas"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -127,7 +126,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 057458bb2..d269072d9 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -8,6 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -121,7 +122,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 2950fc72d..bb09898a3 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -12,6 +12,7 @@ import { validateJwtAuthAudiencesField, validateJwtBoundClaimsField } from "@app/services/identity-jwt-auth/identity-jwt-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({ encryptedJwksCaCert: true, @@ -169,7 +170,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 263fa478e..952cfcdaf 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({ id: true, @@ -147,7 +148,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 f3098c851..4f1f75f5c 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -11,6 +11,7 @@ import { validateOidcAuthAudiencesField, validateOidcBoundClaimsField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({ id: true, @@ -148,7 +149,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 5ec688061..344da3383 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -118,6 +119,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, id: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -166,7 +168,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - id: req.params.identityId + id: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ 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 3d331403a..d6e7259be 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => { server.route({ @@ -74,7 +75,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -157,7 +159,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -257,7 +260,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); await server.services.auditLog.createAuditLog({ @@ -312,6 +316,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -370,6 +375,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.query }); @@ -421,6 +427,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, tokenId: req.params.tokenId, + isActorSuperAdmin: isSuperAdmin(req.auth), ...req.body }); @@ -470,7 +477,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - tokenId: req.params.tokenId + tokenId: req.params.tokenId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); return { 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 e48e1f442..5a9363f3d 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({ id: true, @@ -142,8 +143,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - identityId: req.params.identityId + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) }); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: identityUniversalAuth.orgId, 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 ddef0c553..8fa34f533 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 @@ -19,6 +19,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; import { extractPrincipalArn } from "./identity-aws-auth-fns"; import { @@ -152,8 +153,11 @@ export const identityAwsAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachAwsAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts index c24186ee0..785b37bbc 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts @@ -16,6 +16,7 @@ export type TAttachAwsAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateAwsAuthDTO = { 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 61f9ca23f..07a5c0d01 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 @@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal"; import { validateAzureIdentity } from "./identity-azure-auth-fns"; import { @@ -125,8 +126,11 @@ export const identityAzureAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachAzureAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts index ec03451db..485753b6f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts @@ -14,6 +14,7 @@ export type TAttachAzureAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateAzureAuthDTO = { 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 014e4619f..d86f925b7 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 @@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal"; import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns"; import { @@ -165,8 +166,11 @@ export const identityGcpAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachGcpAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 45e64b24b..063630c73 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -15,6 +15,7 @@ export type TAttachGcpAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateGcpAuthDTO = { 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 314db4ea5..82c9d06da 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 @@ -22,6 +22,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns"; import { @@ -253,8 +254,11 @@ export const identityJwtAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachJwtAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts index a6881f0e5..bc19aba83 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -19,6 +19,7 @@ export type TAttachJwtAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateJwtAuthDTO = { 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 a18de9ad8..c61612bfb 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 @@ -21,6 +21,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; import { @@ -104,7 +105,8 @@ export const identityKubernetesAuthServiceFactory = ({ "Content-Type": "application/json", Authorization: `Bearer ${tokenReviewerJwt}` }, - + signal: AbortSignal.timeout(10000), + timeout: 10000, // if ca cert, rejectUnauthorized: true httpsAgent: new https.Agent({ ca: caCert, @@ -230,8 +232,11 @@ export const identityKubernetesAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachKubernetesAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index f1cde2be9..c66ec8480 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -17,6 +17,7 @@ export type TAttachKubernetesAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateKubernetesAuthDTO = { 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 c22efa4d7..002616f45 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 @@ -23,6 +23,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { @@ -225,8 +226,10 @@ export const identityOidcAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachOidcAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts index 9727285e7..fc5da3e27 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -13,6 +13,7 @@ export type TAttachOidcAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateOidcAuthDTO = { 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 4079d0756..7cfbfc592 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 @@ -17,6 +17,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; import { TAttachTokenAuthDTO, @@ -62,8 +63,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -129,8 +133,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TUpdateTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -221,8 +228,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TRevokeTokenAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -285,8 +295,11 @@ export const identityTokenAuthServiceFactory = ({ actor, actorAuthMethod, actorOrgId, - name + name, + isActorSuperAdmin }: TCreateTokenAuthTokenDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -376,8 +389,11 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TGetTokenAuthTokensDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -412,7 +428,8 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TUpdateTokenAuthTokenDTO) => { const foundToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, @@ -424,6 +441,8 @@ export const identityTokenAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); } + + await validateIdentityUpdateForSuperAdminPrivileges(foundToken.identityId, isActorSuperAdmin); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ message: "The identity does not have Token Auth" @@ -483,18 +502,22 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TRevokeTokenAuthTokenDTO) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, [`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false, [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH }); + if (!identityAccessToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found or already revoked` }); + await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identityAccessToken.identityId }); diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 12c689728..16cd60db7 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -6,6 +6,7 @@ export type TAttachTokenAuthDTO = { accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateTokenAuthDTO = { @@ -14,6 +15,7 @@ export type TUpdateTokenAuthDTO = { accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; accessTokenTrustedIps?: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TGetTokenAuthDTO = { @@ -22,24 +24,29 @@ export type TGetTokenAuthDTO = { export type TRevokeTokenAuthDTO = { identityId: string; + isActorSuperAdmin?: boolean; } & Omit; export type TCreateTokenAuthTokenDTO = { identityId: string; name?: string; + isActorSuperAdmin?: boolean; } & Omit; export type TGetTokenAuthTokensDTO = { identityId: string; offset: number; limit: number; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateTokenAuthTokenDTO = { tokenId: string; name?: string; + isActorSuperAdmin?: boolean; } & Omit; export type TRevokeTokenAuthTokenDTO = { tokenId: string; + isActorSuperAdmin?: boolean; } & Omit; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index c057a656a..43abdf131 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -20,6 +20,7 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal"; import { TIdentityUaDALFactory } from "./identity-ua-dal"; import { @@ -153,8 +154,11 @@ export const identityUaServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TAttachUaDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 2045c2143..07b6a4810 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -7,6 +7,7 @@ export type TAttachUaDTO = { accessTokenNumUsesLimit: number; clientSecretTrustedIps: { ipAddress: string }[]; accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TUpdateUaDTO = { diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 0426e6f8c..763fbfa9c 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -11,6 +11,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal"; @@ -131,8 +132,11 @@ export const identityServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - metadata + metadata, + isActorSuperAdmin }: TUpdateIdentityDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); @@ -224,7 +228,16 @@ export const identityServiceFactory = ({ return identity; }; - const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => { + const deleteIdentity = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id, + isActorSuperAdmin + }: TDeleteIdentityDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index ceaf3ecfc..0eca6b7ee 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -12,10 +12,12 @@ export type TUpdateIdentityDTO = { role?: string; name?: string; metadata?: { key: string; value: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TDeleteIdentityDTO = { id: string; + isActorSuperAdmin?: boolean; } & Omit; export type TGetIdentityByIdDTO = { diff --git a/backend/src/services/super-admin/super-admin-fns.ts b/backend/src/services/super-admin/super-admin-fns.ts new file mode 100644 index 000000000..12ac0e7d7 --- /dev/null +++ b/backend/src/services/super-admin/super-admin-fns.ts @@ -0,0 +1,30 @@ +import { ForbiddenRequestError } from "@app/lib/errors"; +import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; + +import { ActorType } from "../auth/auth-type"; +import { getServerCfg } from "./super-admin-service"; + +export const isSuperAdmin = (auth: TAuthMode) => { + if (auth.actor === ActorType.USER && auth.user.superAdmin) { + return true; + } + + if (auth.actor === ActorType.IDENTITY && auth.isInstanceAdmin) { + return true; + } + + return false; +}; + +export const validateIdentityUpdateForSuperAdminPrivileges = async ( + identityId: string, + isActorSuperAdmin?: boolean +) => { + const serverCfg = await getServerCfg(); + if (serverCfg.adminIdentityIds?.includes(identityId) && !isActorSuperAdmin) { + throw new ForbiddenRequestError({ + message: + "You are attempting to modify an instance admin identity. This requires elevated instance admin privileges" + }); + } +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 1f343b75f..317348cac 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,16 +1,21 @@ import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; -import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; -import { getUserPrivateKey } from "@app/lib/crypto/srp"; +import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TAuthLoginFactory } from "../auth/auth-login-service"; -import { AuthMethod } from "../auth/auth-type"; +import { AuthMethod, AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal"; import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -20,10 +25,19 @@ import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { + LoginMethod, + TAdminBootstrapInstanceDTO, + TAdminGetIdentitiesDTO, + TAdminGetUsersDTO, + TAdminSignUpDTO +} from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { - identityDAL: Pick; + identityDAL: TIdentityDALFactory; + identityTokenAuthDAL: TIdentityTokenAuthDALFactory; + identityAccessTokenDAL: TIdentityAccessTokenDALFactory; + identityOrgMembershipDAL: TIdentityOrgDALFactory; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; userAliasDAL: Pick; @@ -60,7 +74,10 @@ export const superAdminServiceFactory = ({ keyStore, kmsRootConfigDAL, kmsService, - licenseService + licenseService, + identityAccessTokenDAL, + identityTokenAuthDAL, + identityOrgMembershipDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -274,6 +291,137 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; + const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => { + const appCfg = getConfig(); + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfg?.initialized) { + throw new BadRequestError({ message: "Instance has already been set up" }); + } + + const existingUser = await userDAL.findOne({ email }); + if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" }); + + const userInfo = await userDAL.transaction(async (tx) => { + const newUser = await userDAL.create( + { + firstName: "Admin", + lastName: "User", + username: email, + email, + superAdmin: true, + isGhost: false, + isAccepted: true, + authMethods: [AuthMethod.EMAIL], + isEmailVerified: true + }, + tx + ); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password); + const encKeys = await generateUserSrpKeys(email, password); + + const userEnc = await userDAL.createUserEncryption( + { + userId: newUser.id, + encryptionVersion: 2, + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + publicKey: encKeys.publicKey, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + salt: encKeys.salt, + verifier: encKeys.verifier, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext + }, + tx + ); + + return { user: newUser, enc: userEnc }; + }); + + const initialOrganizationName = organizationName ?? "Admin Org"; + + const organization = await orgService.createOrganization({ + userId: userInfo.user.id, + userEmail: userInfo.user.email, + orgName: initialOrganizationName + }); + + const { identity, credentials } = await identityDAL.transaction(async (tx) => { + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); + await identityOrgMembershipDAL.create( + { + identityId: newIdentity.id, + orgId: organization.id, + role: OrgMembershipRole.Admin + }, + tx + ); + + const tokenAuth = await identityTokenAuthDAL.create( + { + identityId: newIdentity.id, + accessTokenMaxTTL: 0, + accessTokenTTL: 0, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: JSON.stringify([ + { + type: "ipv4", + prefix: 0, + ipAddress: "0.0.0.0" + }, + { + type: "ipv6", + prefix: 0, + ipAddress: "::" + } + ]) + }, + tx + ); + + const newToken = await identityAccessTokenDAL.create( + { + identityId: newIdentity.id, + isAccessTokenRevoked: false, + accessTokenTTL: tokenAuth.accessTokenTTL, + accessTokenMaxTTL: tokenAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit, + name: "Instance Admin Token", + authMethod: IdentityAuthMethod.TOKEN_AUTH + }, + tx + ); + + const generatedAccessToken = jwt.sign( + { + identityId: newIdentity.id, + identityAccessTokenId: newToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET + ); + + return { identity: newIdentity, auth: tokenAuth, credentials: { token: generatedAccessToken } }; + }); + + await updateServerCfg({ initialized: true, adminIdentityIds: [identity.id] }, userInfo.user.id); + + return { + user: userInfo, + organization, + machineIdentity: { + ...identity, + credentials + } + }; + }; + const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => { return userDAL.getUsersByFilter({ limit, @@ -289,13 +437,46 @@ export const superAdminServiceFactory = ({ return user; }; - const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { - return identityDAL.getIdentitiesByFilter({ + const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => { + const identity = await identityDAL.findById(identityId); + if (!identity) { + throw new NotFoundError({ name: "Identity", message: "Identity not found" }); + } + + const currentAdminIdentityIds = (await getServerCfg()).adminIdentityIds ?? []; + if (!currentAdminIdentityIds?.includes(identityId)) { + throw new BadRequestError({ name: "Identity", message: "Identity does not have super admin access" }); + } + + await updateServerCfg({ adminIdentityIds: currentAdminIdentityIds.filter((id) => id !== identityId) }, actorId); + + return identity; + }; + + const deleteUserSuperAdminAccess = async (userId: string) => { + const user = await userDAL.findById(userId); + if (!user) { + throw new NotFoundError({ name: "User", message: "User not found" }); + } + + const updatedUser = userDAL.updateById(userId, { superAdmin: false }); + + return updatedUser; + }; + + const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { + const identities = await identityDAL.getIdentitiesByFilter({ limit, offset, searchTerm, sortBy: "name" }); + const serverCfg = await getServerCfg(); + + return identities.map((identity) => ({ + ...identity, + isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) + })); }; const grantServerAdminAccessToUser = async (userId: string) => { @@ -393,12 +574,15 @@ export const superAdminServiceFactory = ({ initServerCfg, updateServerCfg, adminSignUp, + bootstrapInstance, getUsers, deleteUser, getIdentities, getAdminSlackConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, - grantServerAdminAccessToUser + grantServerAdminAccessToUser, + deleteIdentitySuperAdminAccess, + deleteUserSuperAdminAccess }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 54a42c2ca..64ec92632 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -16,6 +16,12 @@ export type TAdminSignUpDTO = { userAgent: string; }; +export type TAdminBootstrapInstanceDTO = { + email: string; + password: string; + organizationName: string; +}; + export type TAdminGetUsersDTO = { offset: number; limit: number; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 454257405..ec92f2ad2 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -600,3 +600,23 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error { return nil } + +func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) { + var resBody map[string]interface{} + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain)) + + if err != nil { + return nil, fmt.Errorf("CallBootstrapInstance: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return resBody, nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 72dbbc97b..a7a797a0b 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -648,3 +648,10 @@ type ExchangeRelayCertResponseV1 struct { Certificate string `json:"certificate"` CertificateChain string `json:"certificateChain"` } + +type BootstrapInstanceRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Organization string `json:"organization"` + Domain string `json:"domain"` +} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go new file mode 100644 index 000000000..008debbac --- /dev/null +++ b/cli/packages/cmd/bootstrap.go @@ -0,0 +1,104 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var bootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Used to bootstrap your Infisical instance", + DisableFlagsInUseLine: true, + Example: "infisical bootstrap", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + email, _ := cmd.Flags().GetString("email") + if email == "" { + if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok { + email = envEmail + } + } + + if email == "" { + log.Error().Msg("email is required") + return + } + + password, _ := cmd.Flags().GetString("password") + if password == "" { + if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok { + password = envPassword + } + } + + if password == "" { + log.Error().Msg("password is required") + return + } + + organization, _ := cmd.Flags().GetString("organization") + if organization == "" { + if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok { + organization = envOrganization + } + } + + if organization == "" { + log.Error().Msg("organization is required") + return + } + + domain, _ := cmd.Flags().GetString("domain") + if domain == "" { + if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok { + domain = envDomain + } + } + + if domain == "" { + log.Error().Msg("domain is required") + return + } + + httpClient := resty.New(). + SetHeader("Accept", "application/json") + + bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{ + Domain: util.AppendAPIEndpoint(domain), + Email: email, + Password: password, + Organization: organization, + }) + + if err != nil { + log.Error().Msgf("Failed to bootstrap instance: %v", err) + return + } + + responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") + if err != nil { + log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + return + } + fmt.Println(string(responseJSON)) + }, +} + +func init() { + bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance") + bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin") + bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin") + bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance") + + rootCmd.AddCommand(bootstrapCmd) +} diff --git a/docs/cli/commands/bootstrap.mdx b/docs/cli/commands/bootstrap.mdx new file mode 100644 index 000000000..77f8b38f1 --- /dev/null +++ b/docs/cli/commands/bootstrap.mdx @@ -0,0 +1,132 @@ +--- +title: "infisical bootstrap" +description: "Automate the initial setup of a new Infisical instance for headless deployment and infrastructure-as-code workflows" +--- + +```bash +infisical bootstrap --domain= --email= --password= --organization= +``` + +## Description + +The `infisical bootstrap` command is used when deploying Infisical in automated environments where manual UI setup is not feasible. It's ideal for: + +- Containerized deployments in Kubernetes or Docker environments +- Infrastructure-as-code pipelines with Terraform or similar tools +- Continuous deployment workflows +- DevOps automation scenarios + +The command initializes a fresh Infisical instance by creating an admin user, organization, and instance admin machine identity, enabling subsequent programmatic configuration without human intervention. + + + This command creates an instance admin machine identity with the highest level + of privileges. The returned token should be treated with the utmost security, + similar to a root credential. Unauthorized access to this token could + compromise your entire Infisical instance. + + +## Flags + + + The URL of your Infisical instance. This can be set using the `INFISICAL_API_URL` environment variable. + +```bash +# Example +infisical bootstrap --domain=https://your-infisical-instance.com +``` + +This flag is required. + + + + + Email address for the admin user account that will be created. This can be set using the `INFISICAL_ADMIN_EMAIL` environment variable. + +```bash +# Example +infisical bootstrap --email=admin@example.com +``` + +This flag is required. + + + + + Password for the admin user account. This can be set using the `INFISICAL_ADMIN_PASSWORD` environment variable. + +```bash +# Example +infisical bootstrap --password=your-secure-password +``` + +This flag is required. + + + + + Name of the organization that will be created within the instance. This can be set using the `INFISICAL_ADMIN_ORGANIZATION` environment variable. + +```bash +# Example +infisical bootstrap --organization=your-org-name +``` + +This flag is required. + + + +## Response + +The command returns a JSON response with details about the created user, organization, and machine identity: + +```json +{ + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } +} +``` + +## Usage with Automation + +For automation purposes, you can extract just the machine identity token from the response: + +```bash +infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq ".identity.credentials.token" +``` + +This extracts only the token, which can be captured in a variable or piped to other commands. + +## Example: Capture Token in a Variable + +```bash +TOKEN=$(infisical bootstrap --domain=https://your-infisical-instance.com --email=admin@example.com --password=your-secure-password --organization=your-org-name | jq -r ".identity.credentials.token") + +# Now use the token for further automation +echo "Token has been captured and can be used for authentication" +``` + +## Notes + +- The bootstrap process can only be performed once on a fresh Infisical instance +- All flags are required for the bootstrap process to complete successfully +- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities +- The generated admin user account can be used to log in via the UI if needed diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 597978093..4d66e30b4 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -114,6 +114,13 @@ using the Universal Auth authentication method. that is to exchange the **Client ID** and **Client Secret** of the identity for an access token by making a request to the `/api/v1/auth/universal-auth/login` endpoint. + + Choose the correct base URL based on your region: + + - For Infisical Cloud US users: `https://app.infisical.com` + - For Infisical Cloud EU users: `https://eu.infisical.com` + + #### Sample request ```bash Request diff --git a/docs/documentation/platform/kms/hsm-integration.mdx b/docs/documentation/platform/kms/hsm-integration.mdx index 4f9efe49f..633377b3d 100644 --- a/docs/documentation/platform/kms/hsm-integration.mdx +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -66,7 +66,7 @@ For organizations that work with US government agencies, FIPS compliance is almo - Are you using Docker? If you are using Docker, please follow the instructions in the [Using HSM's with Docker](#using-hsms-with-docker) section. + Are you using Docker or Kubernetes for your deployment? If you are using Docker or Kubernetes, please follow the instructions in the [Using HSM's in your Deployment](#using-hsms-in-your-deployment) section. Configuring the HSM on Infisical requires setting a set of environment variables: @@ -94,165 +94,447 @@ For organizations that work with US government agencies, FIPS compliance is almo -## Using HSMs with Docker -When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker. +## Using HSMs In Your Deployment + - - - - When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`. + + When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker. - ```bash - mkdir /etc/luna-docker - ``` + + + + + When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`. - After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. + ```bash + mkdir /etc/luna-docker + ``` - A folder structure of a client folder will often look like this: - ``` - partition-ca-certificate.pem - partition-certificate.pem - server-certificate.pem - Chrystoki.conf - /plugins - libcloud.plugin - /lock - /libs - /64 - libCryptoki2.so - /jsp - LunaProvider.jar - /64 - libLunaAPI.so - /etc - openssl.cnf - /bin - /64 - ckdemo - lunacm - multitoken - vtl - ``` - - The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. + After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. - ```bash - cp -r / /etc/luna-docker - ``` + A folder structure of a client folder will often look like this: + ``` + partition-ca-certificate.pem + partition-certificate.pem + server-certificate.pem + Chrystoki.conf + /plugins + libcloud.plugin + /lock + /libs + /64 + libCryptoki2.so + /jsp + LunaProvider.jar + /64 + libLunaAPI.so + /etc + openssl.cnf + /bin + /64 + ckdemo + lunacm + multitoken + vtl + ``` + + The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. - + ```bash + cp -r / /etc/luna-docker + ``` - - The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. + - In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container. + + The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. - An example config file will look like this: + In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container. - ```Chrystoki.conf - Chrystoki2 = { - # This path points to the mounted path, /usr/safenet/lunaclient - LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so; - } + An example config file will look like this: - Luna = { - DefaultTimeOut = 500000; - PEDTimeout1 = 100000; - PEDTimeout2 = 200000; - PEDTimeout3 = 20000; - KeypairGenTimeOut = 2700000; - CloningCommandTimeOut = 300000; - CommandTimeOutPedSet = 720000; - } + ```Chrystoki.conf + Chrystoki2 = { + # This path points to the mounted path, /usr/safenet/lunaclient + LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so; + } - CardReader = { - LunaG5Slots = 0; - RemoteCommand = 1; - } + Luna = { + DefaultTimeOut = 500000; + PEDTimeout1 = 100000; + PEDTimeout2 = 200000; + PEDTimeout3 = 20000; + KeypairGenTimeOut = 2700000; + CloningCommandTimeOut = 300000; + CommandTimeOutPedSet = 720000; + } - Misc = { - # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. - PluginModuleDir = /usr/safenet/lunaclient/plugins; - MutexFolder = /usr/safenet/lunaclient/lock; - PE1746Enabled = 1; - ToolsDir = /usr/bin; + CardReader = { + LunaG5Slots = 0; + RemoteCommand = 1; + } - } + Misc = { + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + PluginModuleDir = /usr/safenet/lunaclient/plugins; + MutexFolder = /usr/safenet/lunaclient/lock; + PE1746Enabled = 1; + ToolsDir = /usr/bin; - Presentation = { - ShowEmptySlots = no; - } + } - LunaSA Client = { - ReceiveTimeout = 20000; - # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. - SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf; - ClientPrivKeyFile = ./etc/ClientNameKey.pem; - ClientCertFile = ./etc/ClientNameCert.pem; - ServerCAFile = ./etc/CAFile.pem; - NetClient = 1; - TCPKeepAlive = 1; - } + Presentation = { + ShowEmptySlots = no; + } + + LunaSA Client = { + ReceiveTimeout = 20000; + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf; + ClientPrivKeyFile = ./etc/ClientNameKey.pem; + ClientCertFile = ./etc/ClientNameCert.pem; + ServerCAFile = ./etc/CAFile.pem; + NetClient = 1; + TCPKeepAlive = 1; + } - REST = { - AppLogLevel = error - ServerName = ; - ServerPort = 443; - AuthTokenConfigURI = ; - AuthTokenClientId = ; - AuthTokenClientSecret = ; - RestClient = 1; - ClientTimeoutSec = 120; - ClientPoolSize = 32; - ClientEofRetryCount = 15; - ClientConnectRetryCount = 900; - ClientConnectIntervalMs = 1000; - } - XTC = { - Enabled = 1; - TimeoutSec = 600; - } - ``` + REST = { + AppLogLevel = error + ServerName = ; + ServerPort = 443; + AuthTokenConfigURI = ; + AuthTokenClientId = ; + AuthTokenClientSecret = ; + RestClient = 1; + ClientTimeoutSec = 120; + ClientPoolSize = 32; + ClientEofRetryCount = 15; + ClientConnectRetryCount = 900; + ClientConnectIntervalMs = 1000; + } + XTC = { + Enabled = 1; + TimeoutSec = 600; + } + ``` - Save the file after updating the paths. - + Save the file after updating the paths. + - - Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command. + + Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command. - We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables. + We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables. - - If no key is found with the provided key label, the HSM will create a new key with the provided label. - Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`. - + + If no key is found with the provided key label, the HSM will create a new key with the provided label. + Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`. + - ```bash - docker run -p 80:8080 \ - -v /etc/luna-docker:/usr/safenet/lunaclient \ - -e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \ - -e HSM_PIN="" \ - -e HSM_SLOT= \ - -e HSM_KEY_LABEL="" \ - - # The rest are unrelated to HSM setup... - -e ENCRYPTION_KEY="<>" \ - -e AUTH_SECRET="<>" \ - -e DB_CONNECTION_URI="<>" \ - -e REDIS_URL="<>" \ - -e SITE_URL="<>" \ - infisical/infisical-fips: # Replace with the version you want to use - ``` + ```bash + docker run -p 80:8080 \ + -v /etc/luna-docker:/usr/safenet/lunaclient \ + -e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \ + -e HSM_PIN="" \ + -e HSM_SLOT= \ + -e HSM_KEY_LABEL="" \ + + # The rest are unrelated to HSM setup... + -e ENCRYPTION_KEY="<>" \ + -e AUTH_SECRET="<>" \ + -e DB_CONNECTION_URI="<>" \ + -e REDIS_URL="<>" \ + -e SITE_URL="<>" \ + infisical/infisical-fips: # Replace with the version you want to use + ``` - We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical). + We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical). - - - After following these steps, your Docker setup will be ready to use HSM encryption. + + + After following these steps, your Docker setup will be ready to use HSM encryption. + + + + + When you are deploying Infisical with the [Kubernetes self-hosting option](/self-hosting/deployment-options/kubernetes-helm), you can still use HSM encryption, but you need to ensure that the HSM client files are present in the container. + + + + + This is only supported on helm chart version `1.4.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information. + + + + + When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. + + + ```bash + mkdir /etc/hsm-client + ``` + + After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. + + A folder structure of a client folder will often look like this: + ``` + partition-ca-certificate.pem + partition-certificate.pem + server-certificate.pem + Chrystoki.conf + /plugins + libcloud.plugin + /lock + /libs + /64 + libCryptoki2.so + /jsp + LunaProvider.jar + /64 + libLunaAPI.so + /etc + openssl.cnf + /bin + /64 + ckdemo + lunacm + multitoken + vtl + ``` + + The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. + + ```bash + cp -r / /etc/hsm-client + ``` + + + The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. + + In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers. + + An example config file will look like this: + + ```Chrystoki.conf + Chrystoki2 = { + # This path points to the mounted path, /hsm-client + LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so; + } + + Luna = { + DefaultTimeOut = 500000; + PEDTimeout1 = 100000; + PEDTimeout2 = 200000; + PEDTimeout3 = 20000; + KeypairGenTimeOut = 2700000; + CloningCommandTimeOut = 300000; + CommandTimeOutPedSet = 720000; + } + + CardReader = { + LunaG5Slots = 0; + RemoteCommand = 1; + } + + Misc = { + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + PluginModuleDir = /hsm-client/plugins; + MutexFolder = /hsm-client/lock; + PE1746Enabled = 1; + ToolsDir = /usr/bin; + + } + + Presentation = { + ShowEmptySlots = no; + } + + LunaSA Client = { + ReceiveTimeout = 20000; + # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. + SSLConfigFile = /hsm-client/etc/openssl.cnf; + ClientPrivKeyFile = ./etc/ClientNameKey.pem; + ClientCertFile = ./etc/ClientNameCert.pem; + ServerCAFile = ./etc/CAFile.pem; + NetClient = 1; + TCPKeepAlive = 1; + } + + + REST = { + AppLogLevel = error + ServerName = ; + ServerPort = 443; + AuthTokenConfigURI = ; + AuthTokenClientId = ; + AuthTokenClientSecret = ; + RestClient = 1; + ClientTimeoutSec = 120; + ClientPoolSize = 32; + ClientEofRetryCount = 15; + ClientConnectRetryCount = 900; + ClientConnectIntervalMs = 1000; + } + XTC = { + Enabled = 1; + TimeoutSec = 600; + } + ``` + + Save the file after updating the paths. + + + + You need to create a Persistent Volume Claim (PVC) to mount the HSM client files to the Infisical deployment. + + + ```bash + kubectl apply -f - < + + + Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`. + We need to update the secret with the following environment variables: + + - `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_ + - `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client + - `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client + - `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label. + + The following is an example of the secret that you should update: + + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: infisical-secrets + type: Opaque + stringData: + # ... Other environment variables ... + HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client + HSM_PIN: "" + HSM_SLOT: "" + HSM_KEY_LABEL: "" + ``` + + Save the file after updating the environment variables, and apply the secret changes + + ```bash + kubectl apply -f ./secret-file-name.yaml + ``` + + + + After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files. + + We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption. + + ```yaml + # ... The rest of the values.yaml file ... + + image: + repository: infisical/infisical-fips # Very important: Must use "infisical/infisical-fips" + tag: "v0.117.1-postgres" + pullPolicy: IfNotPresent + + extraVolumeMounts: + - name: hsm-data + mountPath: /hsm-client # The path we will mount the HSM client files to + subPath: ./hsm-client + + extraVolumes: + - name: hsm-data + persistentVolumeClaim: + claimName: infisical-data-pvc # The PVC we created in the previous step + + # ... The rest of the values.yaml file ... + ``` + + + + + + After updating the values.yaml file, you need to upgrade the Helm chart in order for the changes to take effect. + + ```bash + helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml + ``` + + + After upgrading the Helm chart, you need to restart the deployment in order for the changes to take effect. + + ```bash + kubectl rollout restart deployment/infisical-infisical + ``` + + + After following these steps, your Kubernetes setup will be ready to use HSM encryption. + + + ## Disabling HSM Encryption To disable HSM encryption, navigate to Infisical's Server Admin Console and set the KMS encryption strategy to `Software-based Encryption`. This will revert the encryption strategy back to the default software-based encryption. diff --git a/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png b/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png new file mode 100644 index 000000000..8d819e1fb Binary files /dev/null and b/docs/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png differ diff --git a/docs/mint.json b/docs/mint.json index d424a1e62..3b5d85f91 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -318,7 +318,8 @@ "group": "Guides", "pages": [ "self-hosting/guides/mongo-to-postgres", - "self-hosting/guides/custom-certificates" + "self-hosting/guides/custom-certificates", + "self-hosting/guides/automated-bootstrapping" ] }, { @@ -348,6 +349,7 @@ "cli/commands/dynamic-secrets", "cli/commands/ssh", "cli/commands/gateway", + "cli/commands/bootstrap", "cli/commands/export", "cli/commands/token", "cli/commands/service-token", diff --git a/docs/self-hosting/guides/automated-bootstrapping.mdx b/docs/self-hosting/guides/automated-bootstrapping.mdx new file mode 100644 index 000000000..ebc9c3c80 --- /dev/null +++ b/docs/self-hosting/guides/automated-bootstrapping.mdx @@ -0,0 +1,150 @@ +--- +title: "Programmatic Provisioning" +description: "Learn how to provision and configure Infisical instances programmatically without UI interaction" +--- + +Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential. + +## Overview + +The Automated Bootstrapping workflow automates the following processes: +- Creating an admin user account +- Initializing an organization for the entire instance +- Establishing an **instance admin machine identity** with full administrative permissions +- Returning the machine identity credentials for further automation + +## Key Concepts + +- **Instance Initialization**: Infisical requires [configuration variables](/self-hosting/configuration/envars) to be set during launch, after which the bootstrap process can be triggered. +- **Instance Admin Machine Identity**: The bootstrapping process creates a machine identity with instance-level admin privileges, which can be used to programmatically manage all aspects of the Infisical instance. + ![Instance Admin Identity](/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png) +- **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API. + +## Prerequisites + +- An Infisical instance launched with all required configuration variables +- Access to the Infisical CLI or the ability to make API calls to the instance +- Network connectivity to the Infisical instance + +## Bootstrap Methods + +You can bootstrap an Infisical instance using either the API or the CLI. + + + + Make a POST request to the bootstrap endpoint: + + ``` + POST: http://your-infisical-instance.com/api/v1/admin/bootstrap + { + "email": "admin@example.com", + "password": "your-secure-password", + "organization": "your-org-name" + } + ``` + + Example using curl: + + ```bash + curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \ + http://your-infisical-instance.com/api/v1/admin/bootstrap + ``` + + + Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation: + + ```bash + infisical bootstrap --domain="http://localhost:8080" --email="admin@example.com" --password="your-secure-password" --organization="your-org-name" | jq ".identity.credentials.token" + ``` + + This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform. + + + +## API Response Structure + +The bootstrap process returns a JSON response with details about the created user, organization, and machine identity: + +```json +{ + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } +} +``` + +## Using the Instance Admin Machine Identity Token + +The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential. + +The token enables full programmatic control of your Infisical instance and can be used in the following ways: + +### 1. Infrastructure Automation + +Store the token securely for use with infrastructure automation tools. Due to the sensitive nature of this token, ensure it's protected using appropriate secret management practices: + +#### Kubernetes Secret (with appropriate RBAC restrictions) + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: infisical-admin-credentials +type: Opaque +data: + token: +``` + +#### Environment Variable for Terraform + +```bash +export INFISICAL_TOKEN=your-access-token +terraform apply +``` + +### 2. Programmatic Resource Management + +Use the token to authenticate API calls for creating and managing Infisical resources. The token works exactly like any other Token Auth access token in the Infisical API: + +```bash +curl -X POST \ + -H "Authorization: Bearer ${INFISICAL_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "projectName": "New Project", + "projectDescription": "A project created via API", + "slug": "new-project-slug", + "template": "default", + "type": "SECRET_MANAGER" + }' \ + https://your-infisical-instance.com/api/v2/projects +``` + +## Important Notes + +- **Security Warning**: The instance admin machine identity has the highest level of privileges in your Infisical deployment. The token should be treated with the utmost security and handled like a root credential. Unauthorized access to this token could compromise your entire Infisical instance. +- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities +- The instance admin permission of the generated identity can be revoked later in the server admin panel if needed +- The generated admin user account can still be used for UI access if needed, or can be removed if you prefer to manage everything through the machine identity +- This process is designed to work with future Crossplane providers and the existing Terraform provider for full infrastructure-as-code capabilities +- All necessary configuration variables should be set during the initial launch of the Infisical instance diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 5eb6c6732..d43fbc080 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,7 +1,9 @@ export { useAdminDeleteUser, useAdminGrantServerAdminAccess, + useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, + useRemoveUserServerAdminAccess, useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 901c079a0..b3e1e37b4 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -70,6 +70,40 @@ export const useAdminDeleteUser = () => { }); }; +export const useAdminRemoveIdentitySuperAdminAccess = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (identityId: string) => { + await apiRequest.delete( + `/api/v1/admin/identity-management/identities/${identityId}/super-admin-access` + ); + + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getIdentities] + }); + } + }); +}; + +export const useRemoveUserServerAdminAccess = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (userId: string) => { + await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}/admin-access`); + + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getUsers] + }); + } + }); +}; + export const useAdminGrantServerAdminAccess = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 86d35e086..00dd13d55 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -15,6 +15,7 @@ export type Identity = { authMethods: IdentityAuthMethod[]; createdAt: string; updatedAt: string; + isInstanceAdmin?: boolean; }; export type IdentityAccessToken = { diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index 8d1e25bc5..3e6ca67f6 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -59,8 +59,8 @@ const formSchema = z.object({ trustLdapEmails: z.boolean(), trustOidcEmails: z.boolean(), defaultAuthOrgId: z.string(), - authConsentContent: z.string().optional(), - pageFrameContent: z.string().optional() + authConsentContent: z.string().optional().default(""), + pageFrameContent: z.string().optional().default("") }); type TDashboardForm = z.infer; @@ -86,8 +86,8 @@ export const OverviewPage = () => { trustLdapEmails: config.trustLdapEmails, trustOidcEmails: config.trustOidcEmails, defaultAuthOrgId: config.defaultAuthOrgId ?? "", - authConsentContent: config.authConsentContent, - pageFrameContent: config.pageFrameContent + authConsentContent: config.authConsentContent ?? "", + pageFrameContent: config.pageFrameContent ?? "" } }); @@ -165,8 +165,8 @@ export const OverviewPage = () => { Authentication Rate Limit Integrations - Users - Identities + User Identities + Machine Identities diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx index ee2166a4e..1df9a7cd3 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -1,9 +1,16 @@ import { useState } from "react"; -import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createNotification } from "@app/components/notifications"; import { + Badge, Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, EmptyState, Input, Table, @@ -15,10 +22,22 @@ import { THead, Tr } from "@app/components/v2"; -import { useDebounce } from "@app/hooks"; +import { useDebounce, usePopUp } from "@app/hooks"; +import { useAdminRemoveIdentitySuperAdminAccess } from "@app/hooks/api/admin"; import { useAdminGetIdentities } from "@app/hooks/api/admin/queries"; +import { UsePopUpState } from "@app/hooks/usePopUp"; -const IdentityPanelTable = () => { +const IdentityPanelTable = ({ + handlePopUpOpen +}: { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeServerAdmin"]>, + data?: { + name: string; + id: string; + } + ) => void; +}) => { const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); @@ -48,15 +67,48 @@ const IdentityPanelTable = () => { Name + {isPending && } {!isPending && data?.pages?.map((identities) => - identities.map(({ name, id }) => ( + identities.map(({ name, id, isInstanceAdmin }) => ( - {name} + + {name} + {isInstanceAdmin && ( + + Server Admin + + )} + + + {isInstanceAdmin && ( +
+ + +
+ +
+
+ + {isInstanceAdmin && ( + { + e.stopPropagation(); + handlePopUpOpen("removeServerAdmin", { name, id }); + }} + > + Remove Server Admin + + )} + +
+
+ )} + )) )} @@ -81,11 +133,49 @@ const IdentityPanelTable = () => { ); }; -export const IdentityPanel = () => ( -
-
-

Identities

+export const IdentityPanel = () => { + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "removeServerAdmin" + ] as const); + + const { mutate: deleteIdentitySuperAdminAccess } = useAdminRemoveIdentitySuperAdminAccess(); + + const handleRemoveServerAdmin = async () => { + const { id } = popUp?.removeServerAdmin?.data as { id: string; name: string }; + + try { + await deleteIdentitySuperAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin permissions" + }); + } catch { + createNotification({ + type: "error", + text: "Error removing server admin permissions" + }); + } + + handlePopUpClose("removeServerAdmin"); + }; + + return ( +
+
+

Identities

+
+ + handlePopUpToggle("removeServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleRemoveServerAdmin} + buttonText="Remove Access" + />
- -
-); + ); +}; diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 84d0ed6f0..9395a9a8a 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -33,22 +33,26 @@ import { THead, Tr } from "@app/components/v2"; -import { useSubscription, useUser } from "@app/context"; +import { useSubscription } from "@app/context"; import { useDebounce, usePopUp } from "@app/hooks"; import { useAdminDeleteUser, useAdminGetUsers, - useAdminGrantServerAdminAccess + useAdminGrantServerAdminAccess, + useRemoveUserServerAdminAccess } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; +const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user"; const UserPanelTable = ({ handlePopUpOpen }: { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>, + popUpName: keyof UsePopUpState< + ["removeUser", "upgradePlan", "upgradeToServerAdmin", "removeServerAdmin"] + >, data?: { username: string; id: string; @@ -58,8 +62,6 @@ const UserPanelTable = ({ }) => { const [searchUserFilter, setSearchUserFilter] = useState(""); const [adminsOnly, setAdminsOnly] = useState(false); - const { user } = useUser(); - const userId = user?.id || ""; const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { subscription } = useSubscription(); @@ -143,45 +145,61 @@ const UserPanelTable = ({ {email} - {userId !== id && ( -
- - -
- -
-
- +
+ + +
+ +
+
+ + { + e.stopPropagation(); + handlePopUpOpen("removeUser", { username, id }); + }} + > + Remove User + + {!superAdmin && ( { e.stopPropagation(); - handlePopUpOpen("removeUser", { username, id }); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: addServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("upgradeToServerAdmin", { username, id }); }} > - Remove User + Make User Server Admin - {!superAdmin && ( - { - e.stopPropagation(); - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan", { - username, - id, - message: addServerAdminUpgradePlanMessage - }); - return; - } - handlePopUpOpen("upgradeToServerAdmin", { username, id }); - }} - > - Make User Server Admin - - )} - -
-
- )} + )} + {superAdmin && ( + { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: removeServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("removeServerAdmin", { username, id }); + }} + > + Remove Server Admin + + )} +
+
+
); @@ -212,11 +230,13 @@ export const UserPanel = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "removeUser", "upgradePlan", - "upgradeToServerAdmin" + "upgradeToServerAdmin", + "removeServerAdmin" ] as const); const { mutateAsync: deleteUser } = useAdminDeleteUser(); const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess(); + const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess(); const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -256,6 +276,25 @@ export const UserPanel = () => { handlePopUpClose("upgradeToServerAdmin"); }; + const handleRemoveServerAdminAccess = async () => { + const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string }; + + try { + await removeAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin access from user" + }); + } catch { + createNotification({ + type: "error", + text: "Error removing server admin access from user" + }); + } + + handlePopUpClose("removeServerAdmin"); + }; + return (
@@ -282,6 +321,17 @@ export const UserPanel = () => { onDeleteApproved={handleGrantServerAdminAccess} buttonText="Grant Access" /> + handlePopUpToggle("removeServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleRemoveServerAdminAccess} + buttonText="Remove Access" + /> handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx index 4e048b941..08fa75b79 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -110,6 +110,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri const secretsToDelete = Object.values(selectedEntries.secret).reduce( (accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => { const entry = secretRecord[env.slug]; + if (!entry) return accum; const canDeleteSecret = permission.can( ProjectPermissionSecretActions.Delete, subject(ProjectPermissionSub.Secrets, { diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 84d926623..087bcd4b6 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.8.14 +version: v0.8.15 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.8.14" +appVersion: "v0.8.15" diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml index 1dff23eea..8c78261a0 100644 --- a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml @@ -417,7 +417,6 @@ spec: - secretNamespace type: object required: - - managedKubeConfigMapReferences - resyncInterval type: object status: diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index b38199000..325c0de58 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.8.14 + tag: v0.8.15 resources: limits: cpu: 500m