mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical into feat/adds-GETtokenAuthTokenById-api-endpoint
This commit is contained in:
75
backend/package-lock.json
generated
75
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<PamResource, (server: Fasti
|
||||
createAccountSchema: CreateMySQLAccountSchema,
|
||||
updateAccountSchema: UpdateMySQLAccountSchema
|
||||
});
|
||||
},
|
||||
[PamResource.SSH]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.SSH,
|
||||
accountResponseSchema: SanitizedSSHAccountWithResourceSchema,
|
||||
createAccountSchema: CreateSSHAccountSchema,
|
||||
updateAccountSchema: UpdateSSHAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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<PamResource, (server: Fast
|
||||
createResourceSchema: CreateMySQLResourceSchema,
|
||||
updateResourceSchema: UpdateMySQLResourceSchema
|
||||
});
|
||||
},
|
||||
[PamResource.SSH]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.SSH,
|
||||
resourceResponseSchema: SanitizedSSHResourceSchema,
|
||||
createResourceSchema: CreateSSHResourceSchema,
|
||||
updateResourceSchema: UpdateSSHResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<typeof SessionCredentialsSchema> };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<typeof pamAccountDALFactory>;
|
||||
|
||||
type PamAccountFindFilter = Parameters<typeof buildFindFilter<TPamAccounts>>[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
|
||||
};
|
||||
};
|
||||
|
||||
8
backend/src/ee/services/pam-account/pam-account-enums.ts
Normal file
8
backend/src/ee/services/pam-account/pam-account-enums.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum PamAccountOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export enum PamAccountView {
|
||||
Flat = "flat",
|
||||
Nested = "nested"
|
||||
}
|
||||
@@ -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<typeof pamAccountDAL.findByProjectIdWithResourceDetails>
|
||||
>["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<string, string> = {};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<typeof pamFolderDALFactory>;
|
||||
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<ReturnType<typeof orm.findOne>> | 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 };
|
||||
};
|
||||
|
||||
@@ -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<typeof pamResourceDALFactory>;
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql"
|
||||
MySQL = "mysql",
|
||||
SSH = "ssh"
|
||||
}
|
||||
|
||||
export enum PamResourceOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
@@ -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<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Omit<TCreateResourceDTO, "resourceType"
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export type TListResourcesDTO = {
|
||||
search?: string;
|
||||
orderBy?: PamResourceOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
filterResourceTypes?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
// Resource factory
|
||||
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
|
||||
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
||||
@@ -51,4 +71,5 @@ export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C exten
|
||||
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
||||
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
||||
rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<C>;
|
||||
handleOverwritePreventionForCensoredValues: (updatedAccountCredentials: C, currentCredentials: C) => Promise<C>;
|
||||
};
|
||||
|
||||
@@ -337,9 +337,24 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
}
|
||||
};
|
||||
|
||||
const handleOverwritePreventionForCensoredValues = async (
|
||||
updatedAccountCredentials: TSqlAccountCredentials,
|
||||
currentCredentials: TSqlAccountCredentials
|
||||
) => {
|
||||
if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") {
|
||||
return {
|
||||
...updatedAccountCredentials,
|
||||
password: currentCredentials.password
|
||||
};
|
||||
}
|
||||
|
||||
return updatedAccountCredentials;
|
||||
};
|
||||
|
||||
return {
|
||||
validateConnection,
|
||||
validateAccountCredentials,
|
||||
rotateAccountCredentials
|
||||
rotateAccountCredentials,
|
||||
handleOverwritePreventionForCensoredValues
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum SSHAuthMethod {
|
||||
Password = "password",
|
||||
PublicKey = "public-key",
|
||||
Certificate = "certificate"
|
||||
}
|
||||
265
backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts
Normal file
265
backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts
Normal file
@@ -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 <T>(
|
||||
config: {
|
||||
connectionDetails: TSSHResourceConnectionDetails;
|
||||
resourceType: PamResource;
|
||||
gatewayId: string;
|
||||
},
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
operation: (proxyPort: number) => Promise<T>
|
||||
): Promise<T> => {
|
||||
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<TSSHResourceConnectionDetails, TSSHAccountCredentials> = (
|
||||
resourceType,
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
gatewayV2Service
|
||||
) => {
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => {
|
||||
return new Promise<void>((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<TSSHAccountCredentials> = async (
|
||||
credentials
|
||||
) => {
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => {
|
||||
return new Promise<void>((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<TSSHAccountCredentials> = 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
|
||||
};
|
||||
};
|
||||
117
backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts
Normal file
117
backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts
Normal file
@@ -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);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
SSHAccountCredentialsSchema,
|
||||
SSHAccountSchema,
|
||||
SSHResourceConnectionDetailsSchema,
|
||||
SSHResourceSchema
|
||||
} from "./ssh-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TSSHResource = z.infer<typeof SSHResourceSchema>;
|
||||
export type TSSHResourceConnectionDetails = z.infer<typeof SSHResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TSSHAccount = z.infer<typeof SSHAccountSchema>;
|
||||
export type TSSHAccountCredentials = z.infer<typeof SSHAccountCredentialsSchema>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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]))
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<typeof PamSessionCommandLogSchema>;
|
||||
export type TTerminalEvent = z.infer<typeof TerminalEventSchema>;
|
||||
export type TPamSanitizedSession = z.infer<typeof SanitizedSessionSchema>;
|
||||
|
||||
// DTOs
|
||||
export type TUpdateSessionLogsDTO = {
|
||||
sessionId: string;
|
||||
logs: TPamSessionCommandLog[];
|
||||
logs: (TPamSessionCommandLog | TTerminalEvent)[];
|
||||
};
|
||||
@@ -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"
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
|
||||
export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
||||
[SecretSync.AWSParameterStore]: [],
|
||||
[SecretSync.AWSSecretsManager]: ["mappingBehavior", "secretName"],
|
||||
[SecretSync.AWSSecretsManager]: ["mappingBehavior"],
|
||||
[SecretSync.GitHub]: [],
|
||||
[SecretSync.GCPSecretManager]: [],
|
||||
[SecretSync.AzureKeyVault]: [],
|
||||
|
||||
Reference in New Issue
Block a user