diff --git a/backend/package-lock.json b/backend/package-lock.json index a871c38b1..808cf3204 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -128,6 +128,7 @@ "sjcl": "^1.0.8", "smee-client": "^2.0.0", "snowflake-sdk": "^1.14.0", + "ssh2": "^1.17.0", "tedious": "^18.2.1", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", @@ -164,6 +165,7 @@ "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", "@types/sjcl": "^1.0.34", + "@types/ssh2": "^1.15.5", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", @@ -15634,6 +15636,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sshpk": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.10.3.tgz", @@ -18061,6 +18090,15 @@ "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bullmq": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz", @@ -18901,6 +18939,20 @@ "node": ">= 0.10" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -24996,9 +25048,9 @@ } }, "node_modules/nan": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", - "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", + "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", "license": "MIT" }, "node_modules/nanoid": { @@ -31492,6 +31544,23 @@ "node": ">= 0.6" } }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", diff --git a/backend/package.json b/backend/package.json index aa97de2ed..9a0d9772d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -110,6 +110,7 @@ "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", "@types/sjcl": "^1.0.34", + "@types/ssh2": "^1.15.5", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", @@ -257,6 +258,7 @@ "sjcl": "^1.0.8", "smee-client": "^2.0.0", "snowflake-sdk": "^1.14.0", + "ssh2": "^1.17.0", "tedious": "^18.2.1", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", diff --git a/backend/src/ee/routes/v1/pam-account-routers/index.ts b/backend/src/ee/routes/v1/pam-account-routers/index.ts index 60d621467..d3aadd5a4 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/index.ts @@ -9,6 +9,11 @@ import { SanitizedPostgresAccountWithResourceSchema, UpdatePostgresAccountSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { + CreateSSHAccountSchema, + SanitizedSSHAccountWithResourceSchema, + UpdateSSHAccountSchema +} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; import { registerPamResourceEndpoints } from "./pam-account-endpoints"; @@ -30,5 +35,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.SSH, + accountResponseSchema: SanitizedSSHAccountWithResourceSchema, + createAccountSchema: CreateSSHAccountSchema, + updateAccountSchema: UpdateSSHAccountSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 286e0896f..0b7f89b6c 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -2,16 +2,21 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-enums"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { SanitizedSSHAccountWithResourceSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; +import { OrderByDirection } from "@app/lib/types"; 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"; const SanitizedAccountSchema = z.union([ + SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS SanitizedPostgresAccountWithResourceSchema, SanitizedMySQLAccountWithResourceSchema ]); @@ -26,33 +31,69 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { schema: { description: "List PAM accounts", querystring: z.object({ - projectId: z.string().uuid() + projectId: z.string().uuid(), + accountPath: z.string().trim().default("/").transform(removeTrailingSlash), + accountView: z.nativeEnum(PamAccountView).default(PamAccountView.Flat), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(100), + orderBy: z.nativeEnum(PamAccountOrderBy).default(PamAccountOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), + search: z.string().trim().optional(), + filterResourceIds: z + .string() + .transform((val) => + val + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + ) + .optional() }), response: { 200: z.object({ accounts: SanitizedAccountSchema.array(), - folders: PamFoldersSchema.array() + folders: PamFoldersSchema.array(), + totalCount: z.number().default(0), + folderId: z.string().optional(), + folderPaths: z.record(z.string(), z.string()) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const response = await server.services.pamAccount.list(req.query.projectId, req.permission); + const { projectId, accountPath, accountView, limit, offset, search, orderBy, orderDirection, filterResourceIds } = + req.query; + + const { accounts, folders, totalCount, folderId, folderPaths } = await server.services.pamAccount.list({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + accountPath, + accountView, + limit, + offset, + search, + orderBy, + orderDirection, + filterResourceIds + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: req.query.projectId, + projectId, event: { type: EventType.PAM_ACCOUNT_LIST, metadata: { - accountCount: response.accounts.length, - folderCount: response.folders.length + accountCount: accounts.length, + folderCount: folders.length } } }); - return response; + return { accounts, folders, totalCount, folderId, folderPaths }; } }); @@ -93,7 +134,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { gatewayClientPrivateKey: z.string(), gatewayServerCertificateChain: z.string(), relayHost: z.string(), - metadata: z.record(z.string(), z.string()).optional() + metadata: z.record(z.string(), z.string().optional()).optional() }) } }, diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index 821532598..5dae317da 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -9,6 +9,11 @@ import { SanitizedPostgresResourceSchema, UpdatePostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { + CreateSSHResourceSchema, + SanitizedSSHResourceSchema, + UpdateSSHResourceSchema +} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; @@ -30,5 +35,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.SSH, + resourceResponseSchema: SanitizedSSHResourceSchema, + createResourceSchema: CreateSSHResourceSchema, + updateResourceSchema: UpdateSSHResourceSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 6563c86c7..3536e7a99 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -5,19 +5,30 @@ import { MySQLResourceListItemSchema, SanitizedMySQLResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; +import { PamResourceOrderBy } from "@app/ee/services/pam-resource/pam-resource-enums"; import { PostgresResourceListItemSchema, SanitizedPostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { + SanitizedSSHResourceSchema, + SSHResourceListItemSchema +} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]); +const SanitizedResourceSchema = z.union([ + SanitizedPostgresResourceSchema, + SanitizedMySQLResourceSchema, + SanitizedSSHResourceSchema +]); const ResourceOptionsSchema = z.discriminatedUnion("resource", [ PostgresResourceListItemSchema, - MySQLResourceListItemSchema + MySQLResourceListItemSchema, + SSHResourceListItemSchema ]); export const registerPamResourceRouter = async (server: FastifyZodProvider) => { @@ -52,17 +63,46 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { schema: { description: "List PAM resources", querystring: z.object({ - projectId: z.string().uuid() + projectId: z.string().uuid(), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(100), + orderBy: z.nativeEnum(PamResourceOrderBy).default(PamResourceOrderBy.Name), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.ASC), + search: z.string().trim().optional(), + filterResourceTypes: z + .string() + .transform((val) => + val + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + ) + .optional() }), response: { 200: z.object({ - resources: SanitizedResourceSchema.array() + resources: SanitizedResourceSchema.array(), + totalCount: z.number().default(0) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const response = await server.services.pamResource.list(req.query.projectId, req.permission); + const { projectId, limit, offset, search, orderBy, orderDirection, filterResourceTypes } = req.query; + + const { resources, totalCount } = await server.services.pamResource.list({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + limit, + offset, + search, + orderBy, + orderDirection, + filterResourceTypes + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -71,12 +111,12 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => { event: { type: EventType.PAM_RESOURCE_LIST, metadata: { - count: response.resources.length + count: resources.length } } }); - return response; + return { resources, totalCount }; } }); }; diff --git a/backend/src/ee/routes/v1/pam-session-router.ts b/backend/src/ee/routes/v1/pam-session-router.ts index 5fe10e434..cc8969c92 100644 --- a/backend/src/ee/routes/v1/pam-session-router.ts +++ b/backend/src/ee/routes/v1/pam-session-router.ts @@ -4,12 +4,21 @@ import { PamSessionsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; -import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas"; +import { SSHSessionCredentialsSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; +import { + PamSessionCommandLogSchema, + SanitizedSessionSchema, + TerminalEventSchema +} from "@app/ee/services/pam-session/pam-session-schemas"; 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"; -const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]); +const SessionCredentialsSchema = z.union([ + SSHSessionCredentialsSchema, + PostgresSessionCredentialsSchema, + MySQLSessionCredentialsSchema +]); export const registerPamSessionRouter = async (server: FastifyZodProvider) => { // Meant to be hit solely by gateway identities @@ -50,7 +59,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => { } }); - return { credentials }; + return { credentials: credentials as z.infer }; } }); @@ -67,7 +76,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => { sessionId: z.string().uuid() }), body: z.object({ - logs: PamSessionCommandLogSchema.array() + logs: z.array(z.union([PamSessionCommandLogSchema, TerminalEventSchema])) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/pam-account/pam-account-dal.ts b/backend/src/ee/services/pam-account/pam-account-dal.ts index 6ef7df76e..5fa242627 100644 --- a/backend/src/ee/services/pam-account/pam-account-dal.ts +++ b/backend/src/ee/services/pam-account/pam-account-dal.ts @@ -1,46 +1,109 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TPamAccounts } from "@app/db/schemas"; -import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; export type TPamAccountDALFactory = ReturnType; -type PamAccountFindFilter = Parameters>[0]; - export const pamAccountDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamAccount); - const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => { - const query = (tx || db.replicaNode())(TableName.PamAccount) - .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) - .select(selectAllTableCols(TableName.PamAccount)) - .select( + const findByProjectIdWithResourceDetails = async ( + { + projectId, + folderId, + accountView = PamAccountView.Nested, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC, + filterResourceIds + }: { + projectId: string; + folderId?: string | null; + accountView?: PamAccountView; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + filterResourceIds?: string[]; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamAccount) + .leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`) + .where(`${TableName.PamAccount}.projectId`, projectId); + + if (accountView === PamAccountView.Nested) { + if (folderId) { + void query.where(`${TableName.PamAccount}.folderId`, folderId); + } else { + void query.whereNull(`${TableName.PamAccount}.folderId`); + } + } + + if (search) { + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + const pattern = `%${escapedSearch}%`; + void query.where((q) => { + void q + .whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamAccount, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamAccount, "description", pattern]); + }); + } + + if (filterResourceIds && filterResourceIds.length) { + void query.whereIn(`${TableName.PamAccount}.resourceId`, filterResourceIds); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamAccount)).select( // resource db.ref("name").withSchema(TableName.PamResource).as("resourceName"), db.ref("resourceType").withSchema(TableName.PamResource), db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource) ); - if (filter) { - /* eslint-disable @typescript-eslint/no-misused-promises */ - void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter))); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamAccount}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [results, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + const accounts = results.map( + // @ts-expect-error resourceName, resourceType, encryptedRotationAccountCredentials are from joined table + ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ + ...account, + resourceId, + resource: { + id: resourceId, + name: resourceName as string, + resourceType, + encryptedRotationAccountCredentials + } + }) + ); + return { accounts, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM accounts with resource details" }); } - - const accounts = await query; - - return accounts.map( - ({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({ - ...account, - resourceId, - resource: { - id: resourceId, - name: resourceName, - resourceType, - encryptedRotationAccountCredentials - } - }) - ); }; const findAccountsDueForRotation = async (tx?: Knex) => { @@ -59,5 +122,9 @@ export const pamAccountDALFactory = (db: TDbClient) => { return accounts; }; - return { ...orm, findWithResourceDetails, findAccountsDueForRotation }; + return { + ...orm, + findByProjectIdWithResourceDetails, + findAccountsDueForRotation + }; }; diff --git a/backend/src/ee/services/pam-account/pam-account-enums.ts b/backend/src/ee/services/pam-account/pam-account-enums.ts new file mode 100644 index 000000000..92b95df94 --- /dev/null +++ b/backend/src/ee/services/pam-account/pam-account-enums.ts @@ -0,0 +1,8 @@ +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 2f66d28d7..8c93fbfaf 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -27,12 +27,14 @@ import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns"; import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal"; import { PamResource } from "../pam-resource/pam-resource-enums"; import { TPamAccountCredentials } from "../pam-resource/pam-resource-types"; +import { TSqlResourceConnectionDetails } from "../pam-resource/shared/sql/sql-resource-types"; import { TPamSessionDALFactory } from "../pam-session/pam-session-dal"; import { PamSessionStatus } from "../pam-session/pam-session-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPamAccountDALFactory } from "./pam-account-dal"; +import { PamAccountView } from "./pam-account-enums"; import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns"; -import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types"; +import { TAccessAccountDTO, TCreateAccountDTO, TListAccountsDTO, TUpdateAccountDTO } from "./pam-account-types"; type TPamAccountServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; @@ -251,17 +253,17 @@ export const pamAccountServiceFactory = ({ gatewayV2Service ); - // Logic to prevent overwriting unedited censored values - const finalCredentials = { ...credentials }; - if (credentials.password === "__INFISICAL_UNCHANGED__") { - const decryptedCredentials = await decryptAccountCredentials({ - encryptedCredentials: account.encryptedCredentials, - projectId: account.projectId, - kmsService - }); + const decryptedCredentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + projectId: account.projectId, + kmsService + }); - finalCredentials.password = decryptedCredentials.password; - } + // Logic to prevent overwriting unedited censored values + const finalCredentials = await factory.handleOverwritePreventionForCensoredValues( + credentials, + decryptedCredentials + ); const validatedCredentials = await factory.validateAccountCredentials(finalCredentials); const encryptedCredentials = await encryptAccountCredentials({ @@ -334,21 +336,96 @@ export const pamAccountServiceFactory = ({ }; }; - const list = async (projectId: string, actor: OrgServiceActor) => { + const list = async ({ + projectId, + accountPath, + accountView, + actor, + actorId, + actorAuthMethod, + actorOrgId, + ...params + }: TListAccountsDTO) => { const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, + actor, + actorId, projectId, + actorAuthMethod, + actorOrgId, actionProjectType: ActionProjectType.PAM }); - const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId }); + const limit = params.limit || 20; + const offset = params.offset || 0; const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders); - const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : []; + const folder = accountPath === "/" ? null : await pamFolderDAL.findByPath(projectId, accountPath); + if (accountPath !== "/" && !folder) { + return { accounts: [], folders: [], totalCount: 0, folderPaths: {} }; + } + const folderId = folder?.id; + + let totalFolderCount = 0; + if (canReadFolders && accountView === PamAccountView.Nested) { + const { totalCount } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId, + search: params.search + }); + totalFolderCount = totalCount; + } + + let folders: TPamFolders[] = []; + if (canReadFolders && accountView === PamAccountView.Nested && offset < totalFolderCount) { + const folderLimit = Math.min(limit, totalFolderCount - offset); + const { folders: foldersResp } = await pamFolderDAL.findByProjectId({ + projectId, + parentId: folderId, + limit: folderLimit, + offset, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection + }); + + folders = foldersResp; + } + + let accountsWithResourceDetails: Awaited< + ReturnType + >["accounts"] = []; + let totalAccountCount = 0; + + const accountsToFetch = limit - folders.length; + if (accountsToFetch > 0) { + const accountOffset = Math.max(0, offset - totalFolderCount); + const { accounts, totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView, + offset: accountOffset, + limit: accountsToFetch, + search: params.search, + orderBy: params.orderBy, + orderDirection: params.orderDirection, + filterResourceIds: params.filterResourceIds + }); + accountsWithResourceDetails = accounts; + totalAccountCount = totalCount; + } else { + // if no accounts are to be fetched for the current page, we still need the total count for pagination + const { totalCount } = await pamAccountDAL.findByProjectIdWithResourceDetails({ + projectId, + folderId, + accountView, + search: params.search, + filterResourceIds: params.filterResourceIds + }); + totalAccountCount = totalCount; + } + + const totalCount = totalFolderCount + totalAccountCount; const decryptedAndPermittedAccounts: Array< TPamAccounts & { @@ -359,12 +436,6 @@ export const pamAccountServiceFactory = ({ > = []; for await (const account of accountsWithResourceDetails) { - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: account.projectId - }); - // Check permission for each individual account if ( permission.can( @@ -391,9 +462,27 @@ export const pamAccountServiceFactory = ({ } } + const folderPaths: Record = {}; + const accountFolderIds = [ + ...new Set(decryptedAndPermittedAccounts.flatMap((a) => (a.folderId ? [a.folderId] : []))) + ]; + + await Promise.all( + accountFolderIds.map(async (fId) => { + folderPaths[fId] = await getFullPamFolderPath({ + pamFolderDAL, + folderId: fId, + projectId + }); + }) + ); + return { accounts: decryptedAndPermittedAccounts, - folders + folders, + totalCount, + folderId, + folderPaths }; }; @@ -486,11 +575,11 @@ export const pamAccountServiceFactory = ({ case PamResource.Postgres: case PamResource.MySQL: { - const connectionCredentials = await decryptResourceConnectionDetails({ + const connectionCredentials = (await decryptResourceConnectionDetails({ encryptedConnectionDetails: resource.encryptedConnectionDetails, kmsService, projectId: account.projectId - }); + })) as TSqlResourceConnectionDetails; const credentials = await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, @@ -506,6 +595,19 @@ export const pamAccountServiceFactory = ({ }; } break; + case PamResource.SSH: + { + const credentials = await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + kmsService, + projectId: account.projectId + }); + + metadata = { + username: credentials.username + }; + } + break; default: break; } diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index 4bbccc6fa..b8498036e 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -1,4 +1,7 @@ +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; + import { TPamAccount } from "../pam-resource/pam-resource-types"; +import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; // DTOs export type TCreateAccountDTO = Pick< @@ -18,3 +21,14 @@ export type TAccessAccountDTO = { actorUserAgent: string; duration: number; }; + +export type TListAccountsDTO = { + accountPath: string; + accountView: PamAccountView; + search?: string; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + limit?: number; + offset?: number; + filterResourceIds?: string[]; +} & TProjectPermission; diff --git a/backend/src/ee/services/pam-folder/pam-folder-dal.ts b/backend/src/ee/services/pam-folder/pam-folder-dal.ts index aa334618d..0b8aa8f60 100644 --- a/backend/src/ee/services/pam-folder/pam-folder-dal.ts +++ b/backend/src/ee/services/pam-folder/pam-folder-dal.ts @@ -1,9 +1,106 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamAccountOrderBy } from "../pam-account/pam-account-enums"; export type TPamFolderDALFactory = ReturnType; export const pamFolderDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamFolder); - return { ...orm }; + + const findByProjectId = async ( + { + projectId, + parentId, + search, + limit, + offset = 0, + orderBy = PamAccountOrderBy.Name, + orderDirection = OrderByDirection.ASC + }: { + projectId: string; + parentId?: string | null; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamFolder).where(`${TableName.PamFolder}.projectId`, projectId); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + if (search) { + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + void query.whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamFolder, "name", `%${escapedSearch}%`]); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamFolder)); + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamFolder}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [folders, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + return { folders, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folders" }); + } + }; + + const findByPath = async (projectId: string, path: string, tx?: Knex) => { + try { + const dbInstance = tx || db.replicaNode(); + const pathSegments = path.split("/").filter(Boolean); + + let parentId: string | null = null; + let currentFolder: Awaited> | undefined; + + for await (const segment of pathSegments) { + const query = dbInstance(TableName.PamFolder) + .where(`${TableName.PamFolder}.projectId`, projectId) + .where(`${TableName.PamFolder}.name`, segment); + + if (parentId) { + void query.where(`${TableName.PamFolder}.parentId`, parentId); + } else { + void query.whereNull(`${TableName.PamFolder}.parentId`); + } + + currentFolder = await query.first(); + + if (!currentFolder) { + return undefined; + } + + parentId = currentFolder.id; + } + + return currentFolder; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM folder by path" }); + } + }; + + return { ...orm, findByProjectId, findByPath }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 1a408ca27..9e5cbc985 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -2,7 +2,11 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { PamResourceOrderBy } from "./pam-resource-enums"; export type TPamResourceDALFactory = ReturnType; export const pamResourceDALFactory = (db: TDbClient) => { @@ -20,5 +24,65 @@ export const pamResourceDALFactory = (db: TDbClient) => { return doc; }; - return { ...orm, findById }; + const findByProjectId = async ( + { + projectId, + search, + limit, + offset = 0, + orderBy = PamResourceOrderBy.Name, + orderDirection = OrderByDirection.ASC, + filterResourceTypes + }: { + projectId: string; + search?: string; + limit?: number; + offset?: number; + orderBy?: PamResourceOrderBy; + orderDirection?: OrderByDirection; + filterResourceTypes?: string[]; + }, + tx?: Knex + ) => { + try { + const dbInstance = tx || db.replicaNode(); + const query = dbInstance(TableName.PamResource).where(`${TableName.PamResource}.projectId`, projectId); + + if (search) { + // escape special characters (`%`, `_`) and the escape character itself (`\`) + const escapedSearch = search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); + const pattern = `%${escapedSearch}%`; + void query.where((q) => { + void q + .whereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "name", pattern]) + .orWhereRaw(`??.?? ILIKE ? ESCAPE '\\'`, [TableName.PamResource, "resourceType", pattern]); + }); + } + + if (filterResourceTypes && filterResourceTypes.length) { + void query.whereIn(`${TableName.PamResource}.resourceType`, filterResourceTypes); + } + + const countQuery = query.clone().count("*", { as: "count" }).first(); + + void query.select(selectAllTableCols(TableName.PamResource)); + + const direction = orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"; + + void query.orderByRaw(`${TableName.PamResource}.?? COLLATE "en-x-icu" ${direction}`, [orderBy]); + + if (typeof limit === "number") { + void query.limit(limit).offset(offset); + } + + const [resources, countResult] = await Promise.all([query, countQuery]); + const totalCount = Number(countResult?.count || 0); + + return { resources, totalCount }; + } catch (error) { + throw new DatabaseError({ error, name: "Find PAM resources" }); + } + }; + + return { ...orm, findById, findByProjectId }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts index dff1cc650..e4ec043e1 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-enums.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -1,4 +1,9 @@ export enum PamResource { Postgres = "postgres", - MySQL = "mysql" + MySQL = "mysql", + SSH = "ssh" +} + +export enum PamResourceOrderBy { + Name = "name" } diff --git a/backend/src/ee/services/pam-resource/pam-resource-factory.ts b/backend/src/ee/services/pam-resource/pam-resource-factory.ts index 151fa7ea1..e2d0a50f8 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-factory.ts @@ -1,10 +1,12 @@ import { PamResource } from "./pam-resource-enums"; import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types"; import { sqlResourceFactory } from "./shared/sql/sql-resource-factory"; +import { sshResourceFactory } from "./ssh/ssh-resource-factory"; type TPamResourceFactoryImplementation = TPamResourceFactory; export const PAM_RESOURCE_FACTORY_MAP: Record = { [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation, - [PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation + [PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation, + [PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index d97905dbe..0ebca02b5 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -20,7 +20,7 @@ import { encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns"; -import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types"; +import { TCreateResourceDTO, TListResourcesDTO, TUpdateResourceDTO } from "./pam-resource-types"; type TPamResourceServiceFactoryDep = { pamResourceDAL: TPamResourceDALFactory; @@ -192,19 +192,18 @@ export const pamResourceServiceFactory = ({ gatewayV2Service ); - // Logic to prevent overwriting unedited censored values - const finalCredentials = { ...rotationAccountCredentials }; - if ( - resource.encryptedRotationAccountCredentials && - rotationAccountCredentials.password === "__INFISICAL_UNCHANGED__" - ) { + let finalCredentials = { ...rotationAccountCredentials }; + if (resource.encryptedRotationAccountCredentials) { const decryptedCredentials = await decryptAccountCredentials({ encryptedCredentials: resource.encryptedRotationAccountCredentials, projectId: resource.projectId, kmsService }); - finalCredentials.password = decryptedCredentials.password; + finalCredentials = await factory.handleOverwritePreventionForCensoredValues( + rotationAccountCredentials, + decryptedCredentials + ); } try { @@ -268,22 +267,23 @@ export const pamResourceServiceFactory = ({ } }; - const list = async (projectId: string, actor: OrgServiceActor) => { + const list = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId, ...params }: TListResourcesDTO) => { const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId, projectId, actionProjectType: ActionProjectType.PAM }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources); - const resources = await pamResourceDAL.find({ projectId }); + const { resources, totalCount } = await pamResourceDAL.findByProjectId({ projectId, ...params }); return { - resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService))) + resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService))), + totalCount }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 1ca9db3e2..9da094801 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -1,3 +1,5 @@ +import { OrderByDirection, TProjectPermission } from "@app/lib/types"; + import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TMySQLAccount, @@ -5,22 +7,31 @@ import { TMySQLResource, TMySQLResourceConnectionDetails } from "./mysql/mysql-resource-types"; -import { PamResource } from "./pam-resource-enums"; +import { PamResource, PamResourceOrderBy } from "./pam-resource-enums"; import { TPostgresAccount, TPostgresAccountCredentials, TPostgresResource, TPostgresResourceConnectionDetails } from "./postgres/postgres-resource-types"; +import { + TSSHAccount, + TSSHAccountCredentials, + TSSHResource, + TSSHResourceConnectionDetails +} from "./ssh/ssh-resource-types"; // Resource types -export type TPamResource = TPostgresResource | TMySQLResource; -export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource; +export type TPamResourceConnectionDetails = + | TPostgresResourceConnectionDetails + | TMySQLResourceConnectionDetails + | TSSHResourceConnectionDetails; // Account types -export type TPamAccount = TPostgresAccount | TMySQLAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount; // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials; +export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials | TSSHAccountCredentials; // Resource DTOs export type TCreateResourceDTO = Pick< @@ -32,6 +43,15 @@ export type TUpdateResourceDTO = Partial = () => Promise; export type TPamResourceFactoryValidateAccountCredentials = ( @@ -51,4 +71,5 @@ export type TPamResourceFactory; validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials; + handleOverwritePreventionForCensoredValues: (updatedAccountCredentials: C, currentCredentials: C) => Promise; }; diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index 7dd7948ef..b3128c422 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -337,9 +337,24 @@ export const sqlResourceFactory: TPamResourceFactory { + if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") { + return { + ...updatedAccountCredentials, + password: currentCredentials.password + }; + } + + return updatedAccountCredentials; + }; + return { validateConnection, validateAccountCredentials, - rotateAccountCredentials + rotateAccountCredentials, + handleOverwritePreventionForCensoredValues }; }; diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-enums.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-enums.ts new file mode 100644 index 000000000..9b6ed1f15 --- /dev/null +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-enums.ts @@ -0,0 +1,5 @@ +export enum SSHAuthMethod { + Password = "password", + PublicKey = "public-key", + Certificate = "certificate" +} diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts new file mode 100644 index 000000000..b90aa00c6 --- /dev/null +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts @@ -0,0 +1,265 @@ +import { Client } from "ssh2"; + +import { BadRequestError } from "@app/lib/errors"; +import { GatewayProxyProtocol } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; +import { logger } from "@app/lib/logger"; + +import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; +import { PamResource } from "../pam-resource-enums"; +import { + TPamResourceFactory, + TPamResourceFactoryRotateAccountCredentials, + TPamResourceFactoryValidateAccountCredentials +} from "../pam-resource-types"; +import { SSHAuthMethod } from "./ssh-resource-enums"; +import { TSSHAccountCredentials, TSSHResourceConnectionDetails } from "./ssh-resource-types"; + +const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; + +export const executeWithGateway = async ( + config: { + connectionDetails: TSSHResourceConnectionDetails; + resourceType: PamResource; + gatewayId: string; + }, + gatewayV2Service: Pick, + operation: (proxyPort: number) => Promise +): Promise => { + const { connectionDetails, gatewayId } = config; + const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true); + const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost, + targetPort: connectionDetails.port + }); + + if (!platformConnectionDetails) { + throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" }); + } + + return withGatewayV2Proxy( + async (proxyPort) => { + return operation(proxyPort); + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: platformConnectionDetails.relayHost, + gateway: platformConnectionDetails.gateway, + relay: platformConnectionDetails.relay + } + ); +}; + +export const sshResourceFactory: TPamResourceFactory = ( + resourceType, + connectionDetails, + gatewayId, + gatewayV2Service +) => { + const validateConnection = async () => { + try { + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { + return new Promise((resolve, reject) => { + const client = new Client(); + let handshakeComplete = false; + + client.on("error", (err) => { + logger.info( + { error: err.message, handshakeComplete }, + "[SSH Resource Factory] SSH client error event received" + ); + // If we got an authentication error, it means we successfully reached the SSH server + // and completed the SSH handshake - that's good enough for connection validation + if (handshakeComplete || err.message.includes("authentication") || err.message.includes("publickey")) { + logger.info( + { handshakeComplete, errorMessage: err.message }, + "[SSH Resource Factory] SSH connection validation succeeded (auth error after handshake)" + ); + client.end(); + resolve(); + } else { + logger.error( + { error: err.message, handshakeComplete }, + "[SSH Resource Factory] SSH connection validation failed" + ); + reject(err); + } + }); + + client.on("handshake", () => { + // SSH handshake completed - the server is reachable and responding + logger.info("[SSH Resource Factory] SSH handshake event received - setting handshakeComplete to true"); + handshakeComplete = true; + client.end(); + resolve(); + }); + + client.on("timeout", () => { + logger.error("[SSH Resource Factory] SSH connection timeout"); + reject(new Error("Connection timeout")); + }); + + // Attempt connection with a dummy username (we don't care about auth success) + // The goal is just to verify SSH server is reachable and responding + client.connect({ + host: "localhost", + port: proxyPort, + username: "infisical-connection-test", + password: "infisical-connection-test-password", + readyTimeout: EXTERNAL_REQUEST_TIMEOUT, + tryKeyboard: false, + // We want to fail fast on auth, we're just testing reachability + authHandler: () => { + // If authHandler is called, SSH handshake succeeded + handshakeComplete = true; + return false; // Don't continue with auth + } + }); + }); + }); + return connectionDetails; + } catch (error) { + throw new BadRequestError({ + message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials = async ( + credentials + ) => { + try { + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { + return new Promise((resolve, reject) => { + const client = new Client(); + + client.on("ready", () => { + logger.info( + { username: credentials.username, authMethod: credentials.authMethod }, + "[SSH Resource Factory] SSH authentication successful" + ); + client.end(); + resolve(); + }); + + client.on("error", (err) => { + logger.error( + { error: err.message, username: credentials.username, authMethod: credentials.authMethod }, + "[SSH Resource Factory] SSH authentication failed" + ); + reject(err); + }); + + client.on("timeout", () => { + logger.error( + { username: credentials.username, authMethod: credentials.authMethod }, + "[SSH Resource Factory] SSH authentication timeout" + ); + reject(new Error("Connection timeout")); + }); + + // Build connection config based on auth method + const baseConfig = { + host: "localhost", + port: proxyPort, + username: credentials.username, + readyTimeout: EXTERNAL_REQUEST_TIMEOUT + }; + + switch (credentials.authMethod) { + case SSHAuthMethod.Password: + client.connect({ + ...baseConfig, + password: credentials.password, + tryKeyboard: false + }); + break; + case SSHAuthMethod.PublicKey: + client.connect({ + ...baseConfig, + privateKey: credentials.privateKey, + tryKeyboard: false + }); + break; + default: + reject(new Error(`Unsupported SSH auth method: ${(credentials as TSSHAccountCredentials).authMethod}`)); + } + }); + }); + return credentials; + } catch (error) { + if (error instanceof Error) { + // Check for common authentication failure messages + if ( + error.message.includes("authentication") || + error.message.includes("All configured authentication methods failed") || + error.message.includes("publickey") + ) { + throw new BadRequestError({ + message: "Account credentials invalid." + }); + } + + if (error.message === "Connection timeout") { + throw new BadRequestError({ + message: "Connection timeout. Verify that the SSH server is reachable" + }); + } + } + + throw new BadRequestError({ + message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials = async ( + rotationAccountCredentials + ) => { + return rotationAccountCredentials; + }; + + const handleOverwritePreventionForCensoredValues = async ( + updatedAccountCredentials: TSSHAccountCredentials, + currentCredentials: TSSHAccountCredentials + ) => { + if (updatedAccountCredentials.authMethod !== currentCredentials.authMethod) { + return updatedAccountCredentials; + } + + if ( + updatedAccountCredentials.authMethod === SSHAuthMethod.Password && + currentCredentials.authMethod === SSHAuthMethod.Password + ) { + if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") { + return { + ...updatedAccountCredentials, + password: currentCredentials.password + }; + } + } + + if ( + updatedAccountCredentials.authMethod === SSHAuthMethod.PublicKey && + currentCredentials.authMethod === SSHAuthMethod.PublicKey + ) { + if (updatedAccountCredentials.privateKey === "__INFISICAL_UNCHANGED__") { + return { + ...updatedAccountCredentials, + privateKey: currentCredentials.privateKey + }; + } + } + + return updatedAccountCredentials; + }; + + return { + validateConnection, + validateAccountCredentials, + rotateAccountCredentials, + handleOverwritePreventionForCensoredValues + }; +}; diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts new file mode 100644 index 000000000..97d462369 --- /dev/null +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; + +import { PamResource } from "../pam-resource-enums"; +import { + BaseCreatePamAccountSchema, + BaseCreatePamResourceSchema, + BasePamAccountSchema, + BasePamAccountSchemaWithResource, + BasePamResourceSchema, + BaseUpdatePamAccountSchema, + BaseUpdatePamResourceSchema +} from "../pam-resource-schemas"; +import { SSHAuthMethod } from "./ssh-resource-enums"; + +export const BaseSSHResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.SSH) }); + +export const SSHResourceListItemSchema = z.object({ + name: z.literal("SSH"), + resource: z.literal(PamResource.SSH) +}); + +export const SSHResourceConnectionDetailsSchema = z.object({ + host: z.string().trim().max(255), + port: z.number() +}); + +export const SSHPasswordCredentialsSchema = z.object({ + authMethod: z.literal(SSHAuthMethod.Password), + username: z.string().trim().max(255), + password: z.string().trim().max(255) +}); + +export const SSHPublicKeyCredentialsSchema = z.object({ + authMethod: z.literal(SSHAuthMethod.PublicKey), + username: z.string().trim().max(255), + privateKey: z.string().trim().max(5000) +}); + +export const SSHCertificateCredentialsSchema = z.object({ + authMethod: z.literal(SSHAuthMethod.Certificate), + username: z.string().trim().max(255) +}); + +export const SSHAccountCredentialsSchema = z.discriminatedUnion("authMethod", [ + SSHPasswordCredentialsSchema, + SSHPublicKeyCredentialsSchema, + SSHCertificateCredentialsSchema +]); + +export const SSHResourceSchema = BaseSSHResourceSchema.extend({ + connectionDetails: SSHResourceConnectionDetailsSchema, + rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() +}); + +export const SanitizedSSHResourceSchema = BaseSSHResourceSchema.extend({ + connectionDetails: SSHResourceConnectionDetailsSchema, + rotationAccountCredentials: z + .discriminatedUnion("authMethod", [ + z.object({ + authMethod: z.literal(SSHAuthMethod.Password), + username: z.string() + }), + z.object({ + authMethod: z.literal(SSHAuthMethod.PublicKey), + username: z.string() + }), + z.object({ + authMethod: z.literal(SSHAuthMethod.Certificate), + username: z.string() + }) + ]) + .nullable() + .optional() +}); + +export const CreateSSHResourceSchema = BaseCreatePamResourceSchema.extend({ + connectionDetails: SSHResourceConnectionDetailsSchema, + rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() +}); + +export const UpdateSSHResourceSchema = BaseUpdatePamResourceSchema.extend({ + connectionDetails: SSHResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() +}); + +// Accounts +export const SSHAccountSchema = BasePamAccountSchema.extend({ + credentials: SSHAccountCredentialsSchema +}); + +export const CreateSSHAccountSchema = BaseCreatePamAccountSchema.extend({ + credentials: SSHAccountCredentialsSchema +}); + +export const UpdateSSHAccountSchema = BaseUpdatePamAccountSchema.extend({ + credentials: SSHAccountCredentialsSchema.optional() +}); + +export const SanitizedSSHAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({ + credentials: z.discriminatedUnion("authMethod", [ + z.object({ + authMethod: z.literal(SSHAuthMethod.Password), + username: z.string() + }), + z.object({ + authMethod: z.literal(SSHAuthMethod.PublicKey), + username: z.string() + }), + z.object({ + authMethod: z.literal(SSHAuthMethod.Certificate), + username: z.string() + }) + ]) +}); + +// Sessions +export const SSHSessionCredentialsSchema = SSHResourceConnectionDetailsSchema.and(SSHAccountCredentialsSchema); diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-types.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-types.ts new file mode 100644 index 000000000..920dc4274 --- /dev/null +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { + SSHAccountCredentialsSchema, + SSHAccountSchema, + SSHResourceConnectionDetailsSchema, + SSHResourceSchema +} from "./ssh-resource-schemas"; + +// Resources +export type TSSHResource = z.infer; +export type TSSHResourceConnectionDetails = z.infer; + +// Accounts +export type TSSHAccount = z.infer; +export type TSSHAccountCredentials = z.infer; diff --git a/backend/src/ee/services/pam-session/pam-session-fns.ts b/backend/src/ee/services/pam-session/pam-session-fns.ts index 4afe205b5..38e9f6239 100644 --- a/backend/src/ee/services/pam-session/pam-session-fns.ts +++ b/backend/src/ee/services/pam-session/pam-session-fns.ts @@ -2,7 +2,7 @@ import { TPamSessions } from "@app/db/schemas"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; -import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types"; +import { TPamSanitizedSession, TPamSessionCommandLog, TTerminalEvent } from "./pam-session-types"; export const decryptSessionCommandLogs = async ({ projectId, @@ -22,7 +22,7 @@ export const decryptSessionCommandLogs = async ({ cipherTextBlob: encryptedLogs }); - return JSON.parse(decryptedPlainTextBlob.toString()) as TPamSessionCommandLog; + return JSON.parse(decryptedPlainTextBlob.toString()) as (TPamSessionCommandLog | TTerminalEvent)[]; }; export const decryptSession = async ( @@ -32,7 +32,7 @@ export const decryptSession = async ( ) => { return { ...session, - commandLogs: session.encryptedLogsBlob + logs: session.encryptedLogsBlob ? await decryptSessionCommandLogs({ projectId, encryptedLogs: session.encryptedLogsBlob, diff --git a/backend/src/ee/services/pam-session/pam-session-schemas.ts b/backend/src/ee/services/pam-session/pam-session-schemas.ts index 2bc1d5345..db2493196 100644 --- a/backend/src/ee/services/pam-session/pam-session-schemas.ts +++ b/backend/src/ee/services/pam-session/pam-session-schemas.ts @@ -8,8 +8,18 @@ export const PamSessionCommandLogSchema = z.object({ timestamp: z.coerce.date() }); +// SSH Terminal Event schemas +export const TerminalEventTypeSchema = z.enum(["input", "output", "resize", "error"]); + +export const TerminalEventSchema = z.object({ + timestamp: z.coerce.date(), + eventType: TerminalEventTypeSchema, + data: z.string(), // Base64 encoded binary data + elapsedTime: z.number() // Seconds since session start (for replay) +}); + export const SanitizedSessionSchema = PamSessionsSchema.omit({ encryptedLogsBlob: true }).extend({ - commandLogs: PamSessionCommandLogSchema.array() + logs: z.array(z.union([PamSessionCommandLogSchema, TerminalEventSchema])) }); diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 26ff7daa6..18c185cac 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -12,10 +12,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { ProjectPermissionPamSessionActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TUpdateSessionLogsDTO } from "./pam-session.types"; import { TPamSessionDALFactory } from "./pam-session-dal"; import { PamSessionStatus } from "./pam-session-enums"; import { decryptSession } from "./pam-session-fns"; +import { TUpdateSessionLogsDTO } from "./pam-session-types"; type TPamSessionServiceFactoryDep = { pamSessionDAL: TPamSessionDALFactory; diff --git a/backend/src/ee/services/pam-session/pam-session.types.ts b/backend/src/ee/services/pam-session/pam-session-types.ts similarity index 52% rename from backend/src/ee/services/pam-session/pam-session.types.ts rename to backend/src/ee/services/pam-session/pam-session-types.ts index 0c87a9fa4..893f930e5 100644 --- a/backend/src/ee/services/pam-session/pam-session.types.ts +++ b/backend/src/ee/services/pam-session/pam-session-types.ts @@ -1,12 +1,13 @@ import { z } from "zod"; -import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "./pam-session-schemas"; +import { PamSessionCommandLogSchema, SanitizedSessionSchema, TerminalEventSchema } from "./pam-session-schemas"; export type TPamSessionCommandLog = z.infer; +export type TTerminalEvent = z.infer; export type TPamSanitizedSession = z.infer; // DTOs export type TUpdateSessionLogsDTO = { sessionId: string; - logs: TPamSessionCommandLog[]; + logs: (TPamSessionCommandLog | TTerminalEvent)[]; }; diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 87063c6da..12e272ad6 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -213,7 +213,7 @@ export const certificateProfileServiceFactory = ({ throw new NotFoundError({ message: "Project not found" }); } const plan = await licenseService.getPlan(project.orgId); - if (!plan.pkiAcme) { + if (!plan.pkiAcme && data.enrollmentType === EnrollmentType.ACME) { throw new BadRequestError({ message: "Failed to create certificate profile: Plan restriction. Upgrade plan to continue" }); diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 529634a6d..8829d4622 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -112,7 +112,7 @@ export const SECRET_SYNC_PLAN_MAP: Record = { export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { [SecretSync.AWSParameterStore]: [], - [SecretSync.AWSSecretsManager]: ["mappingBehavior", "secretName"], + [SecretSync.AWSSecretsManager]: ["mappingBehavior"], [SecretSync.GitHub]: [], [SecretSync.GCPSecretManager]: [], [SecretSync.AzureKeyVault]: [], diff --git a/docs/docs.json b/docs/docs.json index c3fa85861..8bed5694c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -890,15 +890,30 @@ ] }, { - "group": "Endpoints", + "group": "Organization", "pages": [ { - "group": "Admin", - "pages": ["api-reference/endpoints/admin/bootstrap-instance"] + "group": "Organization Users", + "pages": [ + "api-reference/endpoints/organizations/memberships", + "api-reference/endpoints/organizations/update-membership", + "api-reference/endpoints/organizations/delete-membership", + "api-reference/endpoints/organizations/bulk-delete-memberships", + "api-reference/endpoints/organizations/list-identity-memberships" + ] }, { - "group": "Events", - "pages": ["api-reference/endpoints/events/project-events"] + "group": "Organization Groups", + "pages": [ + "api-reference/endpoints/groups/create", + "api-reference/endpoints/groups/update", + "api-reference/endpoints/groups/delete", + "api-reference/endpoints/groups/get", + "api-reference/endpoints/groups/get-by-id", + "api-reference/endpoints/groups/add-group-user", + "api-reference/endpoints/groups/remove-group-user", + "api-reference/endpoints/groups/list-group-users" + ] }, { "group": "Organization Identities", @@ -912,187 +927,34 @@ ] }, { - "group": "Token Auth", + "group": "OIDC SSO", "pages": [ - "api-reference/endpoints/token-auth/attach", - "api-reference/endpoints/token-auth/retrieve", - "api-reference/endpoints/token-auth/update", - "api-reference/endpoints/token-auth/revoke", - "api-reference/endpoints/token-auth/get-tokens", - "api-reference/endpoints/token-auth/create-token", - "api-reference/endpoints/token-auth/update-token", - "api-reference/endpoints/token-auth/revoke-token" + "api-reference/endpoints/organizations/oidc-sso/get-oidc-config", + "api-reference/endpoints/organizations/oidc-sso/update-oidc-config", + "api-reference/endpoints/organizations/oidc-sso/create-oidc-config" ] }, { - "group": "Universal Auth", + "group": "LDAP SSO", "pages": [ - "api-reference/endpoints/universal-auth/login", - "api-reference/endpoints/universal-auth/attach", - "api-reference/endpoints/universal-auth/retrieve", - "api-reference/endpoints/universal-auth/update", - "api-reference/endpoints/universal-auth/revoke", - "api-reference/endpoints/universal-auth/create-client-secret", - "api-reference/endpoints/universal-auth/list-client-secrets", - "api-reference/endpoints/universal-auth/revoke-client-secret", - "api-reference/endpoints/universal-auth/get-client-secret-by-id", - "api-reference/endpoints/universal-auth/renew-access-token", - "api-reference/endpoints/universal-auth/revoke-access-token" + "api-reference/endpoints/organizations/ldap-sso/get-ldap-config", + "api-reference/endpoints/organizations/ldap-sso/update-ldap-config", + "api-reference/endpoints/organizations/ldap-sso/create-ldap-config" ] }, { - "group": "GCP Auth", + "group": "SAML SSO", "pages": [ - "api-reference/endpoints/gcp-auth/login", - "api-reference/endpoints/gcp-auth/attach", - "api-reference/endpoints/gcp-auth/retrieve", - "api-reference/endpoints/gcp-auth/update", - "api-reference/endpoints/gcp-auth/revoke" + "api-reference/endpoints/organizations/saml-sso/get-saml-config", + "api-reference/endpoints/organizations/saml-sso/update-saml-config", + "api-reference/endpoints/organizations/saml-sso/create-saml-config" ] - }, - { - "group": "Alibaba Cloud Auth", - "pages": [ - "api-reference/endpoints/alicloud-auth/login", - "api-reference/endpoints/alicloud-auth/attach", - "api-reference/endpoints/alicloud-auth/retrieve", - "api-reference/endpoints/alicloud-auth/update", - "api-reference/endpoints/alicloud-auth/revoke" - ] - }, - { - "group": "TLS Certificate Auth", - "pages": [ - "api-reference/endpoints/tls-cert-auth/login", - "api-reference/endpoints/tls-cert-auth/attach", - "api-reference/endpoints/tls-cert-auth/retrieve", - "api-reference/endpoints/tls-cert-auth/update", - "api-reference/endpoints/tls-cert-auth/revoke" - ] - }, - { - "group": "AWS Auth", - "pages": [ - "api-reference/endpoints/aws-auth/login", - "api-reference/endpoints/aws-auth/attach", - "api-reference/endpoints/aws-auth/retrieve", - "api-reference/endpoints/aws-auth/update", - "api-reference/endpoints/aws-auth/revoke" - ] - }, - { - "group": "OCI Auth", - "pages": [ - "api-reference/endpoints/oci-auth/login", - "api-reference/endpoints/oci-auth/attach", - "api-reference/endpoints/oci-auth/retrieve", - "api-reference/endpoints/oci-auth/update", - "api-reference/endpoints/oci-auth/revoke" - ] - }, - { - "group": "Azure Auth", - "pages": [ - "api-reference/endpoints/azure-auth/login", - "api-reference/endpoints/azure-auth/attach", - "api-reference/endpoints/azure-auth/retrieve", - "api-reference/endpoints/azure-auth/update", - "api-reference/endpoints/azure-auth/revoke" - ] - }, - { - "group": "Kubernetes Auth", - "pages": [ - "api-reference/endpoints/kubernetes-auth/login", - "api-reference/endpoints/kubernetes-auth/attach", - "api-reference/endpoints/kubernetes-auth/retrieve", - "api-reference/endpoints/kubernetes-auth/update", - "api-reference/endpoints/kubernetes-auth/revoke" - ] - }, - { - "group": "OIDC Auth", - "pages": [ - "api-reference/endpoints/oidc-auth/login", - "api-reference/endpoints/oidc-auth/attach", - "api-reference/endpoints/oidc-auth/retrieve", - "api-reference/endpoints/oidc-auth/update", - "api-reference/endpoints/oidc-auth/revoke" - ] - }, - { - "group": "JWT Auth", - "pages": [ - "api-reference/endpoints/jwt-auth/login", - "api-reference/endpoints/jwt-auth/attach", - "api-reference/endpoints/jwt-auth/retrieve", - "api-reference/endpoints/jwt-auth/update", - "api-reference/endpoints/jwt-auth/revoke" - ] - }, - { - "group": "LDAP Auth", - "pages": [ - "api-reference/endpoints/ldap-auth/login", - "api-reference/endpoints/ldap-auth/attach", - "api-reference/endpoints/ldap-auth/retrieve", - "api-reference/endpoints/ldap-auth/update", - "api-reference/endpoints/ldap-auth/revoke" - ] - }, - { - "group": "Groups", - "pages": [ - "api-reference/endpoints/groups/create", - "api-reference/endpoints/groups/update", - "api-reference/endpoints/groups/delete", - "api-reference/endpoints/groups/get", - "api-reference/endpoints/groups/get-by-id", - "api-reference/endpoints/groups/add-group-user", - "api-reference/endpoints/groups/remove-group-user", - "api-reference/endpoints/groups/list-group-users" - ] - }, - { - "group": "Organizations", - "pages": [ - { - "group": "OIDC SSO", - "pages": [ - "api-reference/endpoints/organizations/oidc-sso/get-oidc-config", - "api-reference/endpoints/organizations/oidc-sso/update-oidc-config", - "api-reference/endpoints/organizations/oidc-sso/create-oidc-config" - ] - }, - { - "group": "LDAP SSO", - "pages": [ - "api-reference/endpoints/organizations/ldap-sso/get-ldap-config", - "api-reference/endpoints/organizations/ldap-sso/update-ldap-config", - "api-reference/endpoints/organizations/ldap-sso/create-ldap-config" - ] - }, - { - "group": "SAML SSO", - "pages": [ - "api-reference/endpoints/organizations/saml-sso/get-saml-config", - "api-reference/endpoints/organizations/saml-sso/update-saml-config", - "api-reference/endpoints/organizations/saml-sso/create-saml-config" - ] - }, - "api-reference/endpoints/organizations/memberships", - "api-reference/endpoints/organizations/update-membership", - "api-reference/endpoints/organizations/delete-membership", - "api-reference/endpoints/organizations/bulk-delete-memberships", - "api-reference/endpoints/organizations/list-identity-memberships", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/organizations/projects" - ] - } - ] - }, + } + ] + }, + { + "group": "Project", + "pages": [ { "group": "Projects", "pages": [ @@ -1110,6 +972,7 @@ "api-reference/endpoints/deprecated/projects/create-project", "api-reference/endpoints/deprecated/projects/delete-project", "api-reference/endpoints/deprecated/projects/get-project", + "api-reference/endpoints/deprecated/organizations/projects", "api-reference/endpoints/deprecated/projects/update-project", "api-reference/endpoints/deprecated/projects/secret-snapshots" ] @@ -1186,6 +1049,16 @@ } ] }, + { + "group": "Project Templates", + "pages": [ + "api-reference/endpoints/project-templates/create", + "api-reference/endpoints/project-templates/update", + "api-reference/endpoints/project-templates/delete", + "api-reference/endpoints/project-templates/get-by-id", + "api-reference/endpoints/project-templates/list" + ] + }, { "group": "Project Roles", "pages": [ @@ -1206,349 +1079,6 @@ } ] }, - { - "group": "Project Templates", - "pages": [ - "api-reference/endpoints/project-templates/create", - "api-reference/endpoints/project-templates/update", - "api-reference/endpoints/project-templates/delete", - "api-reference/endpoints/project-templates/get-by-id", - "api-reference/endpoints/project-templates/list" - ] - }, - { - "group": "Environments", - "pages": [ - "api-reference/endpoints/environments/create", - "api-reference/endpoints/environments/update", - "api-reference/endpoints/environments/delete", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/environments/create", - "api-reference/endpoints/deprecated/environments/update", - "api-reference/endpoints/deprecated/environments/delete" - ] - } - ] - }, - { - "group": "Folders", - "pages": [ - "api-reference/endpoints/folders/list", - "api-reference/endpoints/folders/get-by-id", - "api-reference/endpoints/folders/create", - "api-reference/endpoints/folders/update", - "api-reference/endpoints/folders/delete", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/folders/list", - "api-reference/endpoints/deprecated/folders/get-by-id", - "api-reference/endpoints/deprecated/folders/create", - "api-reference/endpoints/deprecated/folders/update", - "api-reference/endpoints/deprecated/folders/delete" - ] - } - ] - }, - { - "group": "Secret Tags", - "pages": [ - "api-reference/endpoints/secret-tags/list", - "api-reference/endpoints/secret-tags/get-by-id", - "api-reference/endpoints/secret-tags/get-by-slug", - "api-reference/endpoints/secret-tags/create", - "api-reference/endpoints/secret-tags/update", - "api-reference/endpoints/secret-tags/delete", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/secret-tags/list", - "api-reference/endpoints/deprecated/secret-tags/get-by-id", - "api-reference/endpoints/deprecated/secret-tags/get-by-slug", - "api-reference/endpoints/deprecated/secret-tags/create", - "api-reference/endpoints/deprecated/secret-tags/update", - "api-reference/endpoints/deprecated/secret-tags/delete" - ] - } - ] - }, - { - "group": "Secrets", - "pages": [ - "api-reference/endpoints/secrets/list", - "api-reference/endpoints/secrets/create", - "api-reference/endpoints/secrets/read", - "api-reference/endpoints/secrets/update", - "api-reference/endpoints/secrets/delete", - "api-reference/endpoints/secrets/create-many", - "api-reference/endpoints/secrets/update-many", - "api-reference/endpoints/secrets/delete-many", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/secrets/list", - "api-reference/endpoints/deprecated/secrets/create", - "api-reference/endpoints/deprecated/secrets/read", - "api-reference/endpoints/deprecated/secrets/update", - "api-reference/endpoints/deprecated/secrets/delete", - "api-reference/endpoints/deprecated/secrets/create-many", - "api-reference/endpoints/deprecated/secrets/update-many", - "api-reference/endpoints/deprecated/secrets/delete-many", - "api-reference/endpoints/deprecated/secrets/attach-tags", - "api-reference/endpoints/deprecated/secrets/detach-tags" - ] - } - ] - }, - { - "group": "Dynamic Secrets", - "pages": [ - { - "group": "Kubernetes", - "pages": [ - "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" - ] - }, - "api-reference/endpoints/dynamic-secrets/create", - "api-reference/endpoints/dynamic-secrets/update", - "api-reference/endpoints/dynamic-secrets/delete", - "api-reference/endpoints/dynamic-secrets/get", - "api-reference/endpoints/dynamic-secrets/list", - "api-reference/endpoints/dynamic-secrets/list-leases", - "api-reference/endpoints/dynamic-secrets/create-lease", - "api-reference/endpoints/dynamic-secrets/delete-lease", - "api-reference/endpoints/dynamic-secrets/renew-lease", - "api-reference/endpoints/dynamic-secrets/get-lease" - ] - }, - { - "group": "Secret Imports", - "pages": [ - "api-reference/endpoints/secret-imports/list", - "api-reference/endpoints/secret-imports/create", - "api-reference/endpoints/secret-imports/update", - "api-reference/endpoints/secret-imports/delete", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/deprecated/secret-imports/list", - "api-reference/endpoints/deprecated/secret-imports/create", - "api-reference/endpoints/deprecated/secret-imports/update", - "api-reference/endpoints/deprecated/secret-imports/delete" - ] - } - ] - }, - { - "group": "Secret Rotations", - "pages": [ - "api-reference/endpoints/secret-rotations/list", - "api-reference/endpoints/secret-rotations/options", - { - "group": "Auth0 Client Secret", - "pages": [ - "api-reference/endpoints/secret-rotations/auth0-client-secret/create", - "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", - "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", - "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", - "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/auth0-client-secret/list", - "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", - "api-reference/endpoints/secret-rotations/auth0-client-secret/update" - ] - }, - { - "group": "AWS IAM User Secret", - "pages": [ - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/create", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/list", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets", - "api-reference/endpoints/secret-rotations/aws-iam-user-secret/update" - ] - }, - { - "group": "Azure Client Secret", - "pages": [ - "api-reference/endpoints/secret-rotations/azure-client-secret/create", - "api-reference/endpoints/secret-rotations/azure-client-secret/delete", - "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-id", - "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-name", - "api-reference/endpoints/secret-rotations/azure-client-secret/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/azure-client-secret/list", - "api-reference/endpoints/secret-rotations/azure-client-secret/rotate-secrets", - "api-reference/endpoints/secret-rotations/azure-client-secret/update" - ] - }, - { - "group": "LDAP Password", - "pages": [ - "api-reference/endpoints/secret-rotations/ldap-password/create", - "api-reference/endpoints/secret-rotations/ldap-password/delete", - "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", - "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", - "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/ldap-password/list", - "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", - "api-reference/endpoints/secret-rotations/ldap-password/update" - ] - }, - { - "group": "Microsoft SQL Server Credentials", - "pages": [ - "api-reference/endpoints/secret-rotations/mssql-credentials/create", - "api-reference/endpoints/secret-rotations/mssql-credentials/delete", - "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id", - "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name", - "api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/mssql-credentials/list", - "api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets", - "api-reference/endpoints/secret-rotations/mssql-credentials/update" - ] - }, - { - "group": "MySQL Credentials", - "pages": [ - "api-reference/endpoints/secret-rotations/mysql-credentials/create", - "api-reference/endpoints/secret-rotations/mysql-credentials/delete", - "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-id", - "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-name", - "api-reference/endpoints/secret-rotations/mysql-credentials/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/mysql-credentials/list", - "api-reference/endpoints/secret-rotations/mysql-credentials/rotate-secrets", - "api-reference/endpoints/secret-rotations/mysql-credentials/update" - ] - }, - { - "group": "Okta Client Secret", - "pages": [ - "api-reference/endpoints/secret-rotations/okta-client-secret/create", - "api-reference/endpoints/secret-rotations/okta-client-secret/delete", - "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id", - "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name", - "api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/okta-client-secret/list", - "api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets", - "api-reference/endpoints/secret-rotations/okta-client-secret/update" - ] - }, - { - "group": "OracleDB Credentials", - "pages": [ - "api-reference/endpoints/secret-rotations/oracledb-credentials/create", - "api-reference/endpoints/secret-rotations/oracledb-credentials/delete", - "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-id", - "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-name", - "api-reference/endpoints/secret-rotations/oracledb-credentials/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/oracledb-credentials/list", - "api-reference/endpoints/secret-rotations/oracledb-credentials/rotate-secrets", - "api-reference/endpoints/secret-rotations/oracledb-credentials/update" - ] - }, - { - "group": "PostgreSQL Credentials", - "pages": [ - "api-reference/endpoints/secret-rotations/postgres-credentials/create", - "api-reference/endpoints/secret-rotations/postgres-credentials/delete", - "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id", - "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name", - "api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/postgres-credentials/list", - "api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets", - "api-reference/endpoints/secret-rotations/postgres-credentials/update" - ] - }, - { - "group": "Redis Credentials", - "pages": [ - "api-reference/endpoints/secret-rotations/redis-credentials/create", - "api-reference/endpoints/secret-rotations/redis-credentials/delete", - "api-reference/endpoints/secret-rotations/redis-credentials/get-by-id", - "api-reference/endpoints/secret-rotations/redis-credentials/get-by-name", - "api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id", - "api-reference/endpoints/secret-rotations/redis-credentials/list", - "api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets", - "api-reference/endpoints/secret-rotations/redis-credentials/update" - ] - } - ] - }, - { - "group": "Secret Scanning", - "pages": [ - { - "group": "Data Sources", - "pages": [ - "api-reference/endpoints/secret-scanning/data-sources/list", - "api-reference/endpoints/secret-scanning/data-sources/options", - { - "group": "Bitbucket", - "pages": [ - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/create", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/update", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan", - "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource" - ] - }, - { - "group": "GitHub", - "pages": [ - "api-reference/endpoints/secret-scanning/data-sources/github/list", - "api-reference/endpoints/secret-scanning/data-sources/github/get-by-id", - "api-reference/endpoints/secret-scanning/data-sources/github/get-by-name", - "api-reference/endpoints/secret-scanning/data-sources/github/list-resources", - "api-reference/endpoints/secret-scanning/data-sources/github/list-scans", - "api-reference/endpoints/secret-scanning/data-sources/github/create", - "api-reference/endpoints/secret-scanning/data-sources/github/update", - "api-reference/endpoints/secret-scanning/data-sources/github/delete", - "api-reference/endpoints/secret-scanning/data-sources/github/scan", - "api-reference/endpoints/secret-scanning/data-sources/github/scan-resource" - ] - }, - { - "group": "GitLab", - "pages": [ - "api-reference/endpoints/secret-scanning/data-sources/gitlab/list", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-id", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-name", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/list-resources", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/list-scans", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/create", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/update", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/delete", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/scan", - "api-reference/endpoints/secret-scanning/data-sources/gitlab/scan-resource" - ] - } - ] - }, - { - "group": "Findings", - "pages": [ - "api-reference/endpoints/secret-scanning/findings/list", - "api-reference/endpoints/secret-scanning/findings/update" - ] - }, - { - "group": "Configuration", - "pages": [ - "api-reference/endpoints/secret-scanning/config/get-by-project-id", - "api-reference/endpoints/secret-scanning/config/update" - ] - } - ] - }, { "group": "Identity Specific Privilege", "pages": [ @@ -1576,6 +1106,15 @@ } ] }, + { + "group": "Service Tokens", + "pages": ["api-reference/endpoints/service-tokens/get"] + } + ] + }, + { + "group": "Shared", + "pages": [ { "group": "App Connections", "pages": [ @@ -2086,6 +1625,415 @@ } ] }, + { + "group": "Audit Logs", + "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] + }, + { + "group": "Events", + "pages": ["api-reference/endpoints/events/project-events"] + } + ] + }, + { + "group": "Identity Auth", + "pages": [ + { + "group": "Token Auth", + "pages": [ + "api-reference/endpoints/token-auth/attach", + "api-reference/endpoints/token-auth/retrieve", + "api-reference/endpoints/token-auth/update", + "api-reference/endpoints/token-auth/revoke", + "api-reference/endpoints/token-auth/get-tokens", + "api-reference/endpoints/token-auth/create-token", + "api-reference/endpoints/token-auth/update-token", + "api-reference/endpoints/token-auth/revoke-token" + ] + }, + { + "group": "Universal Auth", + "pages": [ + "api-reference/endpoints/universal-auth/login", + "api-reference/endpoints/universal-auth/attach", + "api-reference/endpoints/universal-auth/retrieve", + "api-reference/endpoints/universal-auth/update", + "api-reference/endpoints/universal-auth/revoke", + "api-reference/endpoints/universal-auth/create-client-secret", + "api-reference/endpoints/universal-auth/list-client-secrets", + "api-reference/endpoints/universal-auth/revoke-client-secret", + "api-reference/endpoints/universal-auth/get-client-secret-by-id", + "api-reference/endpoints/universal-auth/renew-access-token", + "api-reference/endpoints/universal-auth/revoke-access-token" + ] + }, + { + "group": "GCP Auth", + "pages": [ + "api-reference/endpoints/gcp-auth/login", + "api-reference/endpoints/gcp-auth/attach", + "api-reference/endpoints/gcp-auth/retrieve", + "api-reference/endpoints/gcp-auth/update", + "api-reference/endpoints/gcp-auth/revoke" + ] + }, + { + "group": "Alibaba Cloud Auth", + "pages": [ + "api-reference/endpoints/alicloud-auth/login", + "api-reference/endpoints/alicloud-auth/attach", + "api-reference/endpoints/alicloud-auth/retrieve", + "api-reference/endpoints/alicloud-auth/update", + "api-reference/endpoints/alicloud-auth/revoke" + ] + }, + { + "group": "TLS Certificate Auth", + "pages": [ + "api-reference/endpoints/tls-cert-auth/login", + "api-reference/endpoints/tls-cert-auth/attach", + "api-reference/endpoints/tls-cert-auth/retrieve", + "api-reference/endpoints/tls-cert-auth/update", + "api-reference/endpoints/tls-cert-auth/revoke" + ] + }, + { + "group": "AWS Auth", + "pages": [ + "api-reference/endpoints/aws-auth/login", + "api-reference/endpoints/aws-auth/attach", + "api-reference/endpoints/aws-auth/retrieve", + "api-reference/endpoints/aws-auth/update", + "api-reference/endpoints/aws-auth/revoke" + ] + }, + { + "group": "OCI Auth", + "pages": [ + "api-reference/endpoints/oci-auth/login", + "api-reference/endpoints/oci-auth/attach", + "api-reference/endpoints/oci-auth/retrieve", + "api-reference/endpoints/oci-auth/update", + "api-reference/endpoints/oci-auth/revoke" + ] + }, + { + "group": "Azure Auth", + "pages": [ + "api-reference/endpoints/azure-auth/login", + "api-reference/endpoints/azure-auth/attach", + "api-reference/endpoints/azure-auth/retrieve", + "api-reference/endpoints/azure-auth/update", + "api-reference/endpoints/azure-auth/revoke" + ] + }, + { + "group": "Kubernetes Auth", + "pages": [ + "api-reference/endpoints/kubernetes-auth/login", + "api-reference/endpoints/kubernetes-auth/attach", + "api-reference/endpoints/kubernetes-auth/retrieve", + "api-reference/endpoints/kubernetes-auth/update", + "api-reference/endpoints/kubernetes-auth/revoke" + ] + }, + { + "group": "OIDC Auth", + "pages": [ + "api-reference/endpoints/oidc-auth/login", + "api-reference/endpoints/oidc-auth/attach", + "api-reference/endpoints/oidc-auth/retrieve", + "api-reference/endpoints/oidc-auth/update", + "api-reference/endpoints/oidc-auth/revoke" + ] + }, + { + "group": "JWT Auth", + "pages": [ + "api-reference/endpoints/jwt-auth/login", + "api-reference/endpoints/jwt-auth/attach", + "api-reference/endpoints/jwt-auth/retrieve", + "api-reference/endpoints/jwt-auth/update", + "api-reference/endpoints/jwt-auth/revoke" + ] + }, + { + "group": "LDAP Auth", + "pages": [ + "api-reference/endpoints/ldap-auth/login", + "api-reference/endpoints/ldap-auth/attach", + "api-reference/endpoints/ldap-auth/retrieve", + "api-reference/endpoints/ldap-auth/update", + "api-reference/endpoints/ldap-auth/revoke" + ] + } + ] + }, + { + "group": "Secrets Management", + "pages": [ + { + "group": "Environments", + "pages": [ + "api-reference/endpoints/environments/create", + "api-reference/endpoints/environments/update", + "api-reference/endpoints/environments/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/environments/create", + "api-reference/endpoints/deprecated/environments/update", + "api-reference/endpoints/deprecated/environments/delete" + ] + } + ] + }, + { + "group": "Folders", + "pages": [ + "api-reference/endpoints/folders/list", + "api-reference/endpoints/folders/get-by-id", + "api-reference/endpoints/folders/create", + "api-reference/endpoints/folders/update", + "api-reference/endpoints/folders/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/folders/list", + "api-reference/endpoints/deprecated/folders/get-by-id", + "api-reference/endpoints/deprecated/folders/create", + "api-reference/endpoints/deprecated/folders/update", + "api-reference/endpoints/deprecated/folders/delete" + ] + } + ] + }, + { + "group": "Secrets", + "pages": [ + "api-reference/endpoints/secrets/list", + "api-reference/endpoints/secrets/create", + "api-reference/endpoints/secrets/read", + "api-reference/endpoints/secrets/update", + "api-reference/endpoints/secrets/delete", + "api-reference/endpoints/secrets/create-many", + "api-reference/endpoints/secrets/update-many", + "api-reference/endpoints/secrets/delete-many", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secrets/list", + "api-reference/endpoints/deprecated/secrets/create", + "api-reference/endpoints/deprecated/secrets/read", + "api-reference/endpoints/deprecated/secrets/update", + "api-reference/endpoints/deprecated/secrets/delete", + "api-reference/endpoints/deprecated/secrets/create-many", + "api-reference/endpoints/deprecated/secrets/update-many", + "api-reference/endpoints/deprecated/secrets/delete-many", + "api-reference/endpoints/deprecated/secrets/attach-tags", + "api-reference/endpoints/deprecated/secrets/detach-tags" + ] + } + ] + }, + { + "group": "Secret Tags", + "pages": [ + "api-reference/endpoints/secret-tags/list", + "api-reference/endpoints/secret-tags/get-by-id", + "api-reference/endpoints/secret-tags/get-by-slug", + "api-reference/endpoints/secret-tags/create", + "api-reference/endpoints/secret-tags/update", + "api-reference/endpoints/secret-tags/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secret-tags/list", + "api-reference/endpoints/deprecated/secret-tags/get-by-id", + "api-reference/endpoints/deprecated/secret-tags/get-by-slug", + "api-reference/endpoints/deprecated/secret-tags/create", + "api-reference/endpoints/deprecated/secret-tags/update", + "api-reference/endpoints/deprecated/secret-tags/delete" + ] + } + ] + }, + { + "group": "Secret Imports", + "pages": [ + "api-reference/endpoints/secret-imports/list", + "api-reference/endpoints/secret-imports/create", + "api-reference/endpoints/secret-imports/update", + "api-reference/endpoints/secret-imports/delete", + { + "group": "Legacy", + "pages": [ + "api-reference/endpoints/deprecated/secret-imports/list", + "api-reference/endpoints/deprecated/secret-imports/create", + "api-reference/endpoints/deprecated/secret-imports/update", + "api-reference/endpoints/deprecated/secret-imports/delete" + ] + } + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + { + "group": "Kubernetes", + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] + }, + "api-reference/endpoints/dynamic-secrets/create", + "api-reference/endpoints/dynamic-secrets/update", + "api-reference/endpoints/dynamic-secrets/delete", + "api-reference/endpoints/dynamic-secrets/get", + "api-reference/endpoints/dynamic-secrets/list", + "api-reference/endpoints/dynamic-secrets/list-leases", + "api-reference/endpoints/dynamic-secrets/create-lease", + "api-reference/endpoints/dynamic-secrets/delete-lease", + "api-reference/endpoints/dynamic-secrets/renew-lease", + "api-reference/endpoints/dynamic-secrets/get-lease" + ] + }, + { + "group": "Secret Rotations", + "pages": [ + "api-reference/endpoints/secret-rotations/list", + "api-reference/endpoints/secret-rotations/options", + { + "group": "Auth0 Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/auth0-client-secret/create", + "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/list", + "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/auth0-client-secret/update" + ] + }, + { + "group": "AWS IAM User Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/create", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/list", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/update" + ] + }, + { + "group": "Azure Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/azure-client-secret/create", + "api-reference/endpoints/secret-rotations/azure-client-secret/delete", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/list", + "api-reference/endpoints/secret-rotations/azure-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/azure-client-secret/update" + ] + }, + { + "group": "LDAP Password", + "pages": [ + "api-reference/endpoints/secret-rotations/ldap-password/create", + "api-reference/endpoints/secret-rotations/ldap-password/delete", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", + "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/list", + "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", + "api-reference/endpoints/secret-rotations/ldap-password/update" + ] + }, + { + "group": "Microsoft SQL Server Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mssql-credentials/create", + "api-reference/endpoints/secret-rotations/mssql-credentials/delete", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/list", + "api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mssql-credentials/update" + ] + }, + { + "group": "MySQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mysql-credentials/create", + "api-reference/endpoints/secret-rotations/mysql-credentials/delete", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/list", + "api-reference/endpoints/secret-rotations/mysql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mysql-credentials/update" + ] + }, + { + "group": "Okta Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/okta-client-secret/create", + "api-reference/endpoints/secret-rotations/okta-client-secret/delete", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/okta-client-secret/list", + "api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/okta-client-secret/update" + ] + }, + { + "group": "OracleDB Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/oracledb-credentials/create", + "api-reference/endpoints/secret-rotations/oracledb-credentials/delete", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/list", + "api-reference/endpoints/secret-rotations/oracledb-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/oracledb-credentials/update" + ] + }, + { + "group": "PostgreSQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/postgres-credentials/create", + "api-reference/endpoints/secret-rotations/postgres-credentials/delete", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/list", + "api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/postgres-credentials/update" + ] + }, + { + "group": "Redis Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/redis-credentials/create", + "api-reference/endpoints/secret-rotations/redis-credentials/delete", + "api-reference/endpoints/secret-rotations/redis-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/redis-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/redis-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/redis-credentials/list", + "api-reference/endpoints/secret-rotations/redis-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/redis-credentials/update" + ] + } + ] + }, { "group": "Secret Syncs", "pages": [ @@ -2554,14 +2502,6 @@ "api-reference/endpoints/integrations/delete", "api-reference/endpoints/integrations/list-project-integrations" ] - }, - { - "group": "Service Tokens", - "pages": ["api-reference/endpoints/service-tokens/get"] - }, - { - "group": "Audit Logs", - "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] } ] }, @@ -2693,6 +2633,77 @@ } ] }, + { + "group": "Secret Scanning", + "pages": [ + { + "group": "Data Sources", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/list", + "api-reference/endpoints/secret-scanning/data-sources/options", + { + "group": "Bitbucket", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/create", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/update", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/delete", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan", + "api-reference/endpoints/secret-scanning/data-sources/bitbucket/scan-resource" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/github/list", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/github/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/github/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/github/create", + "api-reference/endpoints/secret-scanning/data-sources/github/update", + "api-reference/endpoints/secret-scanning/data-sources/github/delete", + "api-reference/endpoints/secret-scanning/data-sources/github/scan", + "api-reference/endpoints/secret-scanning/data-sources/github/scan-resource" + ] + }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/gitlab/list", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/create", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/update", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/delete", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/scan", + "api-reference/endpoints/secret-scanning/data-sources/gitlab/scan-resource" + ] + } + ] + }, + { + "group": "Findings", + "pages": [ + "api-reference/endpoints/secret-scanning/findings/list", + "api-reference/endpoints/secret-scanning/findings/update" + ] + }, + { + "group": "Configuration", + "pages": [ + "api-reference/endpoints/secret-scanning/config/get-by-project-id", + "api-reference/endpoints/secret-scanning/config/update" + ] + } + ] + }, { "group": "Infisical SSH", "pages": [ @@ -2786,6 +2797,15 @@ ] } ] + }, + { + "group": "Other", + "pages": [ + { + "group": "Admin", + "pages": ["api-reference/endpoints/admin/bootstrap-instance"] + } + ] } ] }, diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index a6daf0491..f65d6adc6 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -27,9 +27,18 @@ export const useDeleteCert = () => { ); return certificate; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: projectKeys.forProjectCertificates(projectSlug) + queryKey: ["certificate-profiles", "list"] + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.allProjectCertificates() + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.forProjectCertificates(projectId) }); } }); @@ -49,16 +58,18 @@ export const useRevokeCert = () => { ); return certificate; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: (_, { projectId }) => { queryClient.invalidateQueries({ - queryKey: projectKeys.forProjectCertificates(projectSlug) + queryKey: ["certificate-profiles", "list"] }); queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() }); - queryClient.invalidateQueries({ - queryKey: ["certificate-profiles", "list"] + queryKey: projectKeys.allProjectCertificates() + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.forProjectCertificates(projectId) }); } }); diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 5e950b9c9..ee543aac7 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -24,12 +24,12 @@ export type TCertificate = { }; export type TDeleteCertDTO = { - projectSlug: string; + projectId: string; serialNumber: string; }; export type TRevokeCertDTO = { - projectSlug: string; + projectId: string; serialNumber: string; revocationReason: string; }; diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index 78a4f4c13..2c86d9921 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -1,3 +1,4 @@ +// Resources export enum PamResourceType { Postgres = "postgres", MySQL = "mysql", @@ -18,9 +19,24 @@ export enum PamResourceType { DynamoDB = "dynamodb" } +export enum PamResourceOrderBy { + Name = "name" +} + +// Sessions export enum PamSessionStatus { Starting = "starting", Active = "active", Ended = "ended", Terminated = "terminated" } + +// Accounts +export enum PamAccountOrderBy { + Name = "name" +} + +export enum PamAccountView { + Flat = "flat", + Nested = "nested" +} diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index 99a89b425..c5d6ff05b 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -31,7 +31,7 @@ export const useCreatePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; @@ -48,7 +48,7 @@ export const useUpdatePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; @@ -64,7 +64,7 @@ export const useDeletePamResource = () => { return data.resource; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listResources({ projectId }) }); } }); }; @@ -82,7 +82,7 @@ export const useCreatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -99,7 +99,7 @@ export const useUpdatePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -115,7 +115,7 @@ export const useDeletePamAccount = () => { return data.account; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -130,7 +130,7 @@ export const useCreatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -147,7 +147,7 @@ export const useUpdatePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; @@ -163,7 +163,7 @@ export const useDeletePamFolder = () => { return data.folder; }, onSuccess: ({ projectId }) => { - queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) }); + queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/pam/queries.tsx b/frontend/src/hooks/api/pam/queries.tsx index 6339b4761..8f8b681e6 100644 --- a/frontend/src/hooks/api/pam/queries.tsx +++ b/frontend/src/hooks/api/pam/queries.tsx @@ -4,7 +4,14 @@ import { apiRequest } from "@app/config/request"; import { TPamResourceOption } from "./types/resource-options"; import { PamResourceType } from "./enums"; -import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types"; +import { + TListPamAccountsDTO, + TListPamResourcesDTO, + TPamAccount, + TPamFolder, + TPamResource, + TPamSession +} from "./types"; export const pamKeys = { all: ["pam"] as const, @@ -12,14 +19,24 @@ export const pamKeys = { account: () => [...pamKeys.all, "account"] as const, session: () => [...pamKeys.all, "session"] as const, listResourceOptions: () => [...pamKeys.resource(), "options"] as const, - listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId], + listResources: ({ projectId, ...params }: TListPamResourcesDTO) => [ + ...pamKeys.resource(), + "list", + projectId, + params + ], getResource: (resourceType: string, resourceId: string) => [ ...pamKeys.resource(), "get", resourceType, resourceId ], - listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId], + listAccounts: ({ projectId, ...params }: TListPamAccountsDTO) => [ + ...pamKeys.account(), + "list", + projectId, + params + ], getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId], listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId] }; @@ -49,28 +66,33 @@ export const useListPamResourceOptions = ( }); }; +type TListPamResourcesResponse = { + resources: TPamResource[]; + totalCount: number; +}; + export const useListPamResources = ( - projectId: string, + params: TListPamResourcesDTO, options?: Omit< UseQueryOptions< - TPamResource[], + TListPamResourcesResponse, unknown, - TPamResource[], + TListPamResourcesResponse, ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: pamKeys.listResources(projectId), + queryKey: pamKeys.listResources(params), queryFn: async () => { - const { data } = await apiRequest.get<{ resources: TPamResource[] }>( - "/api/v1/pam/resources", - { params: { projectId } } - ); + const { data } = await apiRequest.get("/api/v1/pam/resources", { + params + }); - return data.resources; + return data; }, + placeholderData: (prev) => prev, ...options }); }; @@ -98,28 +120,36 @@ export const useGetPamResourceById = ( }; // Accounts +type TListPamAccountsResponse = { + accounts: TPamAccount[]; + folders: TPamFolder[]; + totalCount: number; + folderId?: string; + folderPaths: Record; +}; + export const useListPamAccounts = ( - projectId: string, + params: TListPamAccountsDTO, options?: Omit< UseQueryOptions< - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, unknown, - { accounts: TPamAccount[]; folders: TPamFolder[] }, + TListPamAccountsResponse, ReturnType >, "queryKey" | "queryFn" > ) => { return useQuery({ - queryKey: pamKeys.listAccounts(projectId), + queryKey: pamKeys.listAccounts(params), queryFn: async () => { - const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>( - "/api/v1/pam/accounts", - { params: { projectId } } - ); + const { data } = await apiRequest.get("/api/v1/pam/accounts", { + params + }); return data; }, + placeholderData: (prev) => prev, ...options }); }; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 1b1890cbd..01b87c282 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,13 +1,22 @@ -import { PamResourceType, PamSessionStatus } from "../enums"; +import { OrderByDirection } from "../../generic/types"; +import { + PamAccountOrderBy, + PamAccountView, + PamResourceOrderBy, + PamResourceType, + PamSessionStatus +} from "../enums"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; +import { TSSHAccount, TSSHResource } from "./ssh-resource"; export * from "./mysql-resource"; export * from "./postgres-resource"; +export * from "./ssh-resource"; -export type TPamResource = TPostgresResource | TMySQLResource; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource; -export type TPamAccount = TPostgresAccount | TMySQLAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount; export type TPamFolder = { id: string; @@ -19,6 +28,22 @@ export type TPamFolder = { updatedAt: string; }; +// Session log types +export type TPamCommandLog = { + input: string; + output: string; + timestamp: string; +}; + +export type TTerminalEvent = { + timestamp: string; + eventType: "input" | "output" | "resize" | "error"; + data: string; // Base64 encoded binary data + elapsedTime: number; // Seconds since session start (for replay) +}; + +export type TPamSessionLog = TPamCommandLog | TTerminalEvent; + export type TPamSession = { id: string; projectId: string; @@ -37,14 +62,20 @@ export type TPamSession = { endedAt?: string | null; createdAt: string; updatedAt: string; - commandLogs: { - input: string; - output: string; - timestamp: string; - }[]; + logs: TPamSessionLog[]; }; // Resource DTOs +export type TListPamResourcesDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: PamResourceOrderBy; + orderDirection?: OrderByDirection; + search?: string; + filterResourceTypes?: string; +}; + export type TCreatePamResourceDTO = Pick< TPamResource, "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" @@ -63,6 +94,18 @@ export type TDeletePamResourceDTO = { }; // Account DTOs +export type TListPamAccountsDTO = { + projectId: string; + accountPath?: string | null; + accountView?: PamAccountView; + offset?: number; + limit?: number; + orderBy?: PamAccountOrderBy; + orderDirection?: OrderByDirection; + search?: string; + filterResourceIds?: string; +}; + export type TCreatePamAccountDTO = Pick< TPamAccount, "name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId" diff --git a/frontend/src/hooks/api/pam/types/ssh-resource.ts b/frontend/src/hooks/api/pam/types/ssh-resource.ts new file mode 100644 index 000000000..0ac3b5a77 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/ssh-resource.ts @@ -0,0 +1,47 @@ +import { PamResourceType } from "../enums"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +export enum SSHAuthMethod { + Password = "password", + PublicKey = "public-key", + Certificate = "certificate" +} + +export type TSSHConnectionDetails = { + host: string; + port: number; +}; + +export type TSSHPasswordCredentials = { + authMethod: SSHAuthMethod.Password; + username: string; + password: string; +}; + +export type TSSHPublicKeyCredentials = { + authMethod: SSHAuthMethod.PublicKey; + username: string; + privateKey: string; +}; + +export type TSSHCertificateCredentials = { + authMethod: SSHAuthMethod.Certificate; + username: string; +}; + +export type TSSHCredentials = + | TSSHPasswordCredentials + | TSSHPublicKeyCredentials + | TSSHCertificateCredentials; + +// Resources +export type TSSHResource = TBasePamResource & { resourceType: PamResourceType.SSH } & { + connectionDetails: TSSHConnectionDetails; + rotationAccountCredentials?: TSSHCredentials | null; +}; + +// Accounts +export type TSSHAccount = TBasePamAccount & { + credentials: TSSHCredentials; +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 4025ecc17..6be963e2b 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -16,15 +16,19 @@ import { faSignOut, faToolbox, faUser, + faUserCog, + faUserPlus, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; +import { UserPlusIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { Mfa } from "@app/components/auth/Mfa"; import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { BreadcrumbContainer, @@ -44,7 +48,13 @@ import { } from "@app/components/v2"; import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3"; import { envConfig } from "@app/config/env"; -import { useOrganization, useSubscription, useUser } from "@app/context"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription, + useUser +} from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; import { @@ -567,15 +577,36 @@ export const Navbar = () => { )} - {user.superAdmin && !location.pathname.startsWith("/admin") && ( - - - Server Console - - )} + {/* eslint-disable-next-line no-nested-ternary */} + {!location.pathname.startsWith("/admin") ? ( + user.superAdmin ? ( + + + Server Console + + ) : ( + + {(isAllowed) => + isAllowed ? ( + + + Invite Members + + ) : null + } + + ) + ) : null}
@@ -657,8 +688,27 @@ export const Navbar = () => {
- Personal Settings + }> + Personal Settings + + + {(isAllowed) => + isAllowed ? ( + + }> + Invite Members + + + ) : null + } + { const onRemoveCertificateSubmit = async (serialNumber: string) => { if (!currentProject?.slug) return; - await deleteCert({ serialNumber, projectSlug: currentProject.slug }); + await deleteCert({ + serialNumber, + projectId: currentProject.id + }); createNotification({ text: "Successfully deleted certificate", diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index c9a0a23f4..a5e2e113c 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -49,7 +49,7 @@ export const OrgGroupsSection = () => { return (
-
+

Groups

diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 037429ad3..87a3860bf 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -97,7 +97,7 @@ export const IdentitySection = withPermission( return (
-
+

Identities

@@ -139,7 +139,7 @@ export const IdentitySection = withPermission(
{/* Identity Auth Templates Section */}
-
+

Identity Auth Templates

diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 0b71dea25..10b0cd07d 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; -import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useEffect, useState } from "react"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { BanIcon } from "lucide-react"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import { BanIcon, UserPlusIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -16,6 +17,7 @@ import { Tooltip } from "@app/components/v2"; import { Badge, DocumentationLinkBadge } from "@app/components/v3"; +import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, @@ -35,6 +37,7 @@ import { OrgMembersTable } from "./OrgMembersTable"; export const OrgMembersSection = () => { const { subscription } = useSubscription(); const { currentOrg, isSubOrganization } = useOrganization(); + const navigate = useNavigate(); const orgId = currentOrg?.id ?? ""; const { user } = useUser(); const userId = user?.id || ""; @@ -55,6 +58,22 @@ export const OrgMembersSection = () => { const [selectedMemberIds, setSelectedMemberIds] = useState([]); + const urlAction = useSearch({ + from: ROUTE_PATHS.Organization.AccessControlPage.id, + select: (el) => el.action, + structuralSharing: true + }); + + useEffect(() => { + if (urlAction === "invite-members") { + handlePopUpOpen("addMember"); + navigate({ + to: ".", + search: ({ action, ...search }) => search + }); + } + }, [urlAction]); + const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); const { mutateAsync: deleteBatchMutateAsync } = useDeleteOrgMembershipBatch(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); @@ -184,7 +203,7 @@ export const OrgMembersSection = () => {
-
+

Users

@@ -194,13 +213,13 @@ export const OrgMembersSection = () => { )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 66e1a3fa7..ea0465bb8 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -392,7 +392,7 @@ export const OrgMembersTable = ({ }} /> - +
Name -
+

{name ?? Not Set}

diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index aa14ac130..9579ce6d6 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -189,7 +189,7 @@ export const OrgRoleTable = () => { return (
-
+

{isSubOrganization ? "Sub-" : ""}Organization Roles diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx index e6d3736fb..e7c517d6c 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -150,8 +150,8 @@ const Page = () => { -

-
+
+
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 62f21ffa4..4fdc55f07 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -116,8 +116,8 @@ const Page = () => { )}
-
-
+
+
{

-
+

diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx index e802e250b..aa7765db8 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx @@ -71,7 +71,7 @@ export const VaultConnectionSection = () => { return (

-
+
{ project setup

-
-

Project Templates

- +
+
+

Project Templates

+ +
{ handlePopUpOpen("addTemplate"); }} isDisabled={!isAllowed} - className="ml-auto" + // className="ml-auto" > Add Template diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index d607bc100..2277e767d 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -242,8 +242,8 @@ const Page = withPermission( )}
-
-
+
+
diff --git a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx index 92b2f7859..51e568551 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/AccountViewToggle.tsx @@ -1,13 +1,9 @@ import { Button } from "@app/components/v2"; - -export enum AccountView { - Flat = "flat", - Nested = "nested" -} +import { PamAccountView } from "@app/hooks/api/pam"; type Props = { - value: AccountView; - onChange: (value: AccountView) => void; + value: PamAccountView; + onChange: (value: PamAccountView) => void; }; export const AccountViewToggle = ({ value, onChange }: Props) => { @@ -16,11 +12,11 @@ export const AccountViewToggle = ({ value, onChange }: Props) => {