From 838c1af44883b2f9efbf3d532b4cf42d8bec4561 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 17 Mar 2025 09:51:21 -0300 Subject: [PATCH 01/55] Add file option to secret set CLI command to retrieve secrets from .env or .yaml files --- cli/packages/cmd/secrets.go | 17 +++++-- cli/packages/util/secrets.go | 95 ++++++++++++++++++++++++++++++++++- docs/cli/commands/secrets.mdx | 15 ++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index c846db10d..75bd459ca 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -143,7 +143,12 @@ var secretsSetCmd = &cobra.Command{ Short: "Used set secrets", Use: "set [secrets]", DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), + Args: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("file") { + return cobra.MinimumNArgs(0)(cmd, args) + } + return cobra.MinimumNArgs(1)(cmd, args) + }, Run: func(cmd *cobra.Command, args []string) { token, err := util.GetInfisicalToken(cmd) if err != nil { @@ -177,13 +182,18 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } + file, err := cmd.Flags().GetString("file") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + var secretOperations []models.SecretSetOperation if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { if projectId == "" { util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag") } - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token) + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file) } else { if projectId == "" { workspaceFile, err := util.GetWorkSpaceFromFile() @@ -206,7 +216,7 @@ var secretsSetCmd = &cobra.Command{ secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", Token: loggedInUserDetails.UserCredentials.JTWToken, - }) + }, file) } if err != nil { @@ -691,6 +701,7 @@ func init() { secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") + secretsSetCmd.Flags().String("file", "", "Path to a file with key=value secret pairs (comments with # or // are ignored)") secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 0e26e03b0..07f55ce33 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,6 +17,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" + "gopkg.in/yaml.v3" ) func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) { @@ -563,7 +564,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil } -func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) { +func parseSecrets(fileName string, content string) (map[string]string, error) { + secrets := make(map[string]string) + + if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { + // Handle YAML secrets + var yamlData map[string]interface{} + if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil { + return nil, fmt.Errorf("failed to parse YAML file: %v", err) + } + + for key, value := range yamlData { + if strValue, ok := value.(string); ok { + secrets[key] = strValue + } else { + return nil, fmt.Errorf("YAML secret '%s' must be a string", key) + } + } + } else { + // Handle .env files + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + + // Ignore empty lines and comments + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue + } + + // Ensure it's a valid key=value pair + splitKeyValue := strings.SplitN(line, "=", 2) + if len(splitKeyValue) != 2 { + return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line) + } + + key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1]) + + // Handle quoted values + if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + value = value[1 : len(value)-1] // Remove surrounding quotes + } + + secrets[key] = value + } + } + + return secrets, nil +} + +func validateSecretKey(key string) error { + if key == "" { + return errors.New("secret keys cannot be empty") + } + if unicode.IsNumber(rune(key[0])) { + return fmt.Errorf("secret key '%s' cannot start with a number", key) + } + if strings.Contains(key, " ") { + return fmt.Errorf("secret key '%s' cannot contain spaces", key) + } + return nil +} + +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) { + if file != "" { + content, err := os.ReadFile(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + PrintErrorMessageAndExit("File does not exist") + } + return nil, fmt.Errorf("unable to process file [err=%v]", err) + } + + parsedSecrets, err := parseSecrets(file, string(content)) + if err != nil { + PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err)) + } + + // Step 2: Validate secrets + for key, value := range parsedSecrets { + if err := validateSecretKey(key); err != nil { + PrintErrorMessageAndExit(err.Error()) + } + if strings.TrimSpace(value) == "" { + PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key)) + } + secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value)) + } + + if len(secretArgs) == 0 { + PrintErrorMessageAndExit("no valid secrets found in the file") + } + } if tokenDetails == nil { return nil, fmt.Errorf("unable to process set secret operations, token details are missing") diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 2dff5cf7b..d1f4baa9f 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb ``` + + Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory. + + The file should contain secrets in the following formats: + - `key=value` for `.env` files + - `key: value` for YAML files + + Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing. + + + ```bash + # Example + infisical secrets set --file="./.env" + ``` + From 6ef358b172c34632cae8361907026c0ec9f893d2 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 17 Mar 2025 22:35:37 +0800 Subject: [PATCH 02/55] feat: initialize base --- .../20250317101525_add-instance-admin-mi.ts | 19 ++ backend/src/db/schemas/super-admin.ts | 3 +- .../server/plugins/auth/inject-identity.ts | 6 +- backend/src/server/plugins/auth/superAdmin.ts | 14 +- backend/src/server/routes/index.ts | 3 + backend/src/server/routes/v1/admin-router.ts | 72 +++++++- .../src/services/identity/identity-types.ts | 1 + .../services/super-admin/super-admin-fns.ts | 15 ++ .../super-admin/super-admin-service.ts | 171 +++++++++++++++++- .../services/super-admin/super-admin-types.ts | 6 + frontend/src/hooks/api/admin/queries.ts | 2 +- frontend/src/hooks/api/identities/types.ts | 1 + .../pages/admin/OverviewPage/OverviewPage.tsx | 10 +- .../OverviewPage/components/IdentityPanel.tsx | 12 +- 14 files changed, 302 insertions(+), 33 deletions(-) create mode 100644 backend/src/db/migrations/20250317101525_add-instance-admin-mi.ts create mode 100644 backend/src/services/super-admin/super-admin-fns.ts 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/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 9d239a405..806c5a21e 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,6 +8,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 = | { @@ -43,6 +44,7 @@ export type TAuthMode = identityName: string; orgId: string; authMethod: null; + isInstanceAdmin?: boolean; } | { authMode: AuthMode.SCIM_TOKEN; @@ -129,13 +131,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) }; break; } 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..f46c2f2dd 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,54 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/initialize", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + email: z.string().email().trim(), + password: z.string().trim(), + organizationName: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string(), + user: UsersSchema, + organization: OrganizationsSchema, + machineIdentity: IdentitiesSchema.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.initializeInstance({ + ...req.body + }); + + 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 initialized instance", + user: user.user, + organization, + machineIdentity + }; + } + }); }; diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index ceaf3ecfc..3f9e5c523 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -12,6 +12,7 @@ export type TUpdateIdentityDTO = { role?: string; name?: string; metadata?: { key: string; value: string }[]; + isActorSuperAdmin?: boolean; } & Omit; export type TDeleteIdentityDTO = { 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..b7c00dd44 --- /dev/null +++ b/backend/src/services/super-admin/super-admin-fns.ts @@ -0,0 +1,15 @@ +import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; + +import { ActorType } from "../auth/auth-type"; + +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; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 1f343b75f..2164ca146 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, + TAdminGetIdentitiesDTO, + TAdminGetUsersDTO, + TAdminInitializeInstanceDTO, + 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 initializeInstance = async ({ email, password, organizationName }: TAdminInitializeInstanceDTO) => { + 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: "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,19 @@ export const superAdminServiceFactory = ({ return user; }; - const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { - return identityDAL.getIdentitiesByFilter({ + 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,6 +547,7 @@ export const superAdminServiceFactory = ({ initServerCfg, updateServerCfg, adminSignUp, + initializeInstance, getUsers, deleteUser, getIdentities, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 54a42c2ca..ad989fcf4 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 TAdminInitializeInstanceDTO = { + email: string; + password: string; + organizationName: string; +}; + export type TAdminGetUsersDTO = { offset: number; limit: number; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 496990abe..b24841dbd 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -1,6 +1,7 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { Identity } from "@app/hooks/api/identities/types"; import { User } from "../types"; import { @@ -10,7 +11,6 @@ import { TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; -import { Identity } from "@app/hooks/api/identities/types"; export const adminStandaloneKeys = { getUsers: "get-users", diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 8d344e7f7..da685392c 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 776c5e20f..f5aaf8c9f 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -28,13 +28,13 @@ import { useGetServerRootKmsEncryptionDetails, useUpdateServerConfig } from "@app/hooks/api"; +import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; import { AuthPanel } from "./components/AuthPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; -import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; enum TabSections { Settings = "settings", @@ -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 ?? "" } }); diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx index ee2166a4e..81ae19d4f 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -3,6 +3,7 @@ import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { + Badge, Button, EmptyState, Input, @@ -54,9 +55,16 @@ const IdentityPanelTable = () => { {isPending && } {!isPending && data?.pages?.map((identities) => - identities.map(({ name, id }) => ( + identities.map(({ name, id, isInstanceAdmin }) => ( - {name} + + {name} + {isInstanceAdmin && ( + + Server Admin + + )} + )) )} From 2d1d6f5ce86934b8ef005da119955c300a512b81 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 18 Mar 2025 01:20:01 +0800 Subject: [PATCH 03/55] misc: added privilege escalation checks --- .../routes/v1/identity-aws-iam-auth-router.ts | 4 +- .../routes/v1/identity-azure-auth-router.ts | 6 +-- .../routes/v1/identity-gcp-auth-router.ts | 4 +- .../routes/v1/identity-jwt-auth-router.ts | 4 +- .../v1/identity-kubernetes-auth-router.ts | 4 +- .../routes/v1/identity-oidc-auth-router.ts | 4 +- .../src/server/routes/v1/identity-router.ts | 5 ++- .../routes/v1/identity-token-auth-router.ts | 16 ++++++-- .../v1/identity-universal-auth-router.ts | 5 ++- .../identity-aws-auth-service.ts | 6 ++- .../identity-aws-auth-types.ts | 1 + .../identity-azure-auth-service.ts | 6 ++- .../identity-azure-auth-types.ts | 1 + .../identity-gcp-auth-service.ts | 6 ++- .../identity-gcp-auth-types.ts | 1 + .../identity-jwt-auth-service.ts | 6 ++- .../identity-jwt-auth-types.ts | 1 + .../identity-kubernetes-auth-service.ts | 6 ++- .../identity-kubernetes-auth-types.ts | 1 + .../identity-oidc-auth-service.ts | 5 ++- .../identity-oidc-auth-types.ts | 1 + .../identity-token-auth-service.ts | 37 +++++++++++++++---- .../identity-token-auth-types.ts | 7 ++++ .../identity-ua/identity-ua-service.ts | 6 ++- .../services/identity-ua/identity-ua-types.ts | 1 + .../src/services/identity/identity-service.ts | 17 ++++++++- .../src/services/identity/identity-types.ts | 1 + .../services/super-admin/super-admin-fns.ts | 15 ++++++++ 28 files changed, 147 insertions(+), 30 deletions(-) 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 7ce0b05b7..ab3a4b805 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, @@ -146,7 +147,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 ddd9e0278..5be3dfe4c 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 @@ -16,6 +16,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 { @@ -149,8 +150,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 01878dbb3..87e076e93 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 @@ -14,6 +14,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 { @@ -122,8 +123,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 7b0dd4390..321f633ff 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 @@ -14,6 +14,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 { @@ -162,8 +163,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 555dc00a6..9bfc97956 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 @@ -18,6 +18,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 { @@ -248,8 +249,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 c7b2c5c8d..40c7a0af6 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 @@ -18,6 +18,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 { @@ -227,8 +228,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 3c947f504..d3e713b30 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 @@ -19,6 +19,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 { @@ -196,8 +197,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 761f68aa7..78a10479c 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 @@ -12,6 +12,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 d9e2d66fa..1d327fff3 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 @@ -14,6 +14,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, @@ -59,8 +60,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}` }); @@ -126,8 +130,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}` }); @@ -218,8 +225,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}` }); @@ -271,8 +281,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}` }); @@ -350,8 +363,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}` }); @@ -386,7 +402,8 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + isActorSuperAdmin }: TUpdateTokenAuthTokenDTO) => { const foundToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, @@ -398,6 +415,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" @@ -446,18 +465,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 078b50c08..848b536ba 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-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 { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal"; import { TIdentityUaDALFactory } from "./identity-ua-dal"; import { @@ -150,8 +151,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 8ada2a5d1..bd6c8d30c 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -9,6 +9,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { ActorType } from "../auth/auth-type"; +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"; @@ -112,8 +113,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}` }); @@ -209,7 +213,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 3f9e5c523..0eca6b7ee 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -17,6 +17,7 @@ export type TUpdateIdentityDTO = { 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 index b7c00dd44..12ac0e7d7 100644 --- a/backend/src/services/super-admin/super-admin-fns.ts +++ b/backend/src/services/super-admin/super-admin-fns.ts @@ -1,6 +1,8 @@ +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) { @@ -13,3 +15,16 @@ export const isSuperAdmin = (auth: TAuthMode) => { 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" + }); + } +}; From 64569ab44b19d9ff6cd79882c45e3055c7aee22e Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 18 Mar 2025 02:27:42 +0800 Subject: [PATCH 04/55] feat: added bootstrap to CLI --- backend/src/server/routes/v1/admin-router.ts | 19 ++-- .../super-admin/super-admin-service.ts | 8 +- .../services/super-admin/super-admin-types.ts | 2 +- cli/packages/api/api.go | 20 +++++ cli/packages/api/model.go | 24 +++++ cli/packages/cmd/bootstrap.go | 90 +++++++++++++++++++ 6 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 cli/packages/cmd/bootstrap.go diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index f46c2f2dd..b0d474be9 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -401,22 +401,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/initialize", + url: "/bootstrap", config: { rateLimit: writeLimit }, schema: { body: z.object({ - email: z.string().email().trim(), - password: z.string().trim(), - organizationName: z.string().trim() + 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, organization: OrganizationsSchema, - machineIdentity: IdentitiesSchema.extend({ + identity: IdentitiesSchema.extend({ credentials: z.object({ token: z.string() }) // would just be Token AUTH for now @@ -425,8 +425,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { user, organization, machineIdentity } = await server.services.superAdmin.initializeInstance({ - ...req.body + const { user, organization, machineIdentity } = await server.services.superAdmin.bootstrapInstance({ + ...req.body, + organizationName: req.body.organization }); await server.services.telemetry.sendPostHogEvents({ @@ -441,10 +442,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); return { - message: "Successfully initialized instance", + message: "Successfully boostrapped instance", user: user.user, organization, - machineIdentity + identity: machineIdentity }; } }); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 2164ca146..1f1ed592d 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -27,9 +27,9 @@ import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { LoginMethod, + TAdminBoostrapInstanceDTO, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, - TAdminInitializeInstanceDTO, TAdminSignUpDTO } from "./super-admin-types"; @@ -291,7 +291,7 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; - const initializeInstance = async ({ email, password, organizationName }: TAdminInitializeInstanceDTO) => { + const bootstrapInstance = async ({ email, password, organizationName }: TAdminBoostrapInstanceDTO) => { const appCfg = getConfig(); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg?.initialized) { @@ -352,7 +352,7 @@ export const superAdminServiceFactory = ({ }); const { identity, credentials } = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name: "Admin Identity" }, tx); + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); await identityOrgMembershipDAL.create( { identityId: newIdentity.id, @@ -547,7 +547,7 @@ export const superAdminServiceFactory = ({ initServerCfg, updateServerCfg, adminSignUp, - initializeInstance, + bootstrapInstance, getUsers, deleteUser, getIdentities, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index ad989fcf4..49d48e464 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -16,7 +16,7 @@ export type TAdminSignUpDTO = { userAgent: string; }; -export type TAdminInitializeInstanceDTO = { +export type TAdminBoostrapInstanceDTO = { email: string; password: string; organizationName: string; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 454257405..04fd8819e 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) (*BootstrapInstanceResponse, error) { + var resBody BootstrapInstanceResponse + 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 1eb936f24..dba6bc3c7 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -654,3 +654,27 @@ 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"` +} + +type BootstrapInstanceResponseOrganization struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type BootstrapInstanceResponseIdentity struct { + ID string `json:"id"` + Name string `json:"name"` + Credentials interface{} `json:"credentials"` +} + +type BootstrapInstanceResponse struct { + Message string `json:"message"` + Organization BootstrapInstanceResponseOrganization `json:"organization"` + Identity BootstrapInstanceResponseIdentity `json:"identity"` +} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go new file mode 100644 index 000000000..c91ee75d9 --- /dev/null +++ b/cli/packages/cmd/bootstrap.go @@ -0,0 +1,90 @@ +/* +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 == "" { + log.Error().Msg("organization is required") + return + } + + domain, _ := cmd.Flags().GetString("domain") + 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) + } + + responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") + if err != nil { + log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + } + 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) +} From ed6306747ab43b22fbdb094012c71be57e93021a Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 18 Mar 2025 03:00:54 +0800 Subject: [PATCH 05/55] feat: add support for removing instance admin permission from identity --- backend/src/server/routes/v1/admin-router.ts | 36 +++++++ .../super-admin/super-admin-service.ts | 19 +++- frontend/src/hooks/api/admin/index.ts | 1 + frontend/src/hooks/api/admin/mutation.ts | 18 ++++ .../OverviewPage/components/IdentityPanel.tsx | 102 ++++++++++++++++-- 5 files changed, 165 insertions(+), 11 deletions(-) diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index b0d474be9..2935e2655 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -399,6 +399,42 @@ 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: "POST", url: "/bootstrap", diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 1f1ed592d..b8fb5a3e3 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -437,6 +437,22 @@ export const superAdminServiceFactory = ({ return user; }; + 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 getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { const identities = await identityDAL.getIdentitiesByFilter({ limit, @@ -554,6 +570,7 @@ export const superAdminServiceFactory = ({ getAdminSlackConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, - grantServerAdminAccessToUser + grantServerAdminAccessToUser, + deleteIdentitySuperAdminAccess }; }; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 5eb6c6732..f4982dc14 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,6 +1,7 @@ export { useAdminDeleteUser, useAdminGrantServerAdminAccess, + useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, useUpdateAdminSlackConfig, useUpdateServerConfig, diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 901c079a0..754aca5e9 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -70,6 +70,24 @@ 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 useAdminGrantServerAdminAccess = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx index 81ae19d4f..1df9a7cd3 100644 --- a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -1,10 +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, @@ -16,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); @@ -49,6 +67,7 @@ const IdentityPanelTable = () => { Name + @@ -65,6 +84,31 @@ const IdentityPanelTable = () => { )} + + {isInstanceAdmin && ( +
+ + +
+ +
+
+ + {isInstanceAdmin && ( + { + e.stopPropagation(); + handlePopUpOpen("removeServerAdmin", { name, id }); + }} + > + Remove Server Admin + + )} + +
+
+ )} + )) )} @@ -89,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" + />
- -
-); + ); +}; From f9695741f146aefdbbee37fb70739572706241d9 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 17 Mar 2025 19:36:40 -0400 Subject: [PATCH 06/55] Minor changes to oidc claims and mappings - Made the claims expanded by default (it looked off when they were closed) - Moved claims from advanced to geneal tab and kept the mapping in the advanced tab - Added better description for the tooltip question: i feel like it would be better to access metadata like: `{{identity.auth.oidc.claim.<...>}}` instead of like how it is now: `{{identity.metadata.auth.oidc.claim.<...>}}`? What do you think --- .env.migration.example | 2 - .../IdentitySection/IdentityOidcAuthForm.tsx | 178 +++++++++--------- 2 files changed, 93 insertions(+), 87 deletions(-) delete mode 100644 .env.migration.example diff --git a/.env.migration.example b/.env.migration.example deleted file mode 100644 index 2c5f5b957..000000000 --- a/.env.migration.example +++ /dev/null @@ -1,2 +0,0 @@ -DB_CONNECTION_URI= -AUDIT_LOGS_DB_CONNECTION_URI= diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 21440457a..b3347faa5 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -51,7 +51,7 @@ const schema = z.object({ key: z.string(), value: z.string() }) - ), + ).default([{ key: "", value: "" }]), claimMetadataMapping: z .array( z.object({ @@ -59,8 +59,7 @@ const schema = z.object({ value: z.string() }) ) - .optional() - .nullable(), + .default([{ key: "", value: "" }]), boundSubject: z.string().optional().default("") }); @@ -105,7 +104,9 @@ export const IdentityOidcAuthForm = ({ accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", accessTokenNumUsesLimit: "0", - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + boundClaims: [{ key: "", value: "" }], + claimMetadataMapping: [{ key: "", value: "" }] } }); const { @@ -167,7 +168,7 @@ export const IdentityOidcAuthForm = ({ caCert: "", boundIssuer: "", boundAudiences: "", - boundClaims: [], + boundClaims: [{ key: "", value: "" }], boundSubject: "", accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", @@ -253,7 +254,7 @@ export const IdentityOidcAuthForm = ({
{ setTabValue( - ["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0]) + ["accessTokenTrustedIps", "caCert", "claimMetadataMapping"].includes(Object.keys(fields)[0]) ? IdentityFormTab.Advanced : IdentityFormTab.Configuration ); @@ -343,63 +344,6 @@ export const IdentityOidcAuthForm = ({ )} /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - - - ( - -