Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dc8760a
feat(pam-oracle): save-time TLS validation + frontend session logs
saifsmailbox98 Apr 22, 2026
e139cfe
feat(pam): OracleDB resource and account support
saifsmailbox98 Apr 22, 2026
c8647ea
fix(pam-oracle): defer credential rotation to a later release
saifsmailbox98 Apr 22, 2026
5c3ffd3
chore(pam-oracle): update probeOracleTls comment after rotation stub
saifsmailbox98 Apr 22, 2026
9462c3b
docs(pam-oracle): add Oracle PAM resource documentation
saifsmailbox98 Apr 22, 2026
099b174
Merge origin/main into oracle-db
saifsmailbox98 Apr 22, 2026
2b96c6f
chore(pam-oracle): sort imports in sql-resource-factory
saifsmailbox98 Apr 23, 2026
ec17e24
fix(pam-oracle): honor TLS settings in resource probe + defer SSL acc…
saifsmailbox98 Apr 23, 2026
2cde79a
Merge branch 'main' into saif/eng-4890-add-support-for-oracle-db-acce…
saifsmailbox98 Apr 24, 2026
c6392cf
Add comment for AWS IAM in pam-account-router
saifsmailbox98 Apr 24, 2026
a50ff24
fix(pam-oracle): probe TLS on account save, not just resource save
saifsmailbox98 Apr 24, 2026
a54edbd
fix(pam-oracle): include Oracle in the PAM session credentials respon…
saifsmailbox98 Apr 24, 2026
995b858
chore(pam-oracle): move probeOracleTls into oracle-resource-fns for n…
saifsmailbox98 Apr 24, 2026
9b37c98
chore(pam-oracle): split probe args type and timeout constant into st…
saifsmailbox98 Apr 24, 2026
d61fbd0
fix(pam-oracle): remove unnecessary checkServerIdentity skip in TLS p…
saifsmailbox98 May 6, 2026
0e75f38
chore(pam-oracle): strip verbose comments in types and factory
saifsmailbox98 May 6, 2026
04e4eb7
chore(pam-oracle): trim TLS validation comment
saifsmailbox98 May 6, 2026
f40f701
chore(pam-oracle): remove obvious servername comment
saifsmailbox98 May 6, 2026
02cead1
merge: resolve conflict with main (Oracle + Windows gateway response …
saifsmailbox98 May 6, 2026
7a40b0f
refactor(pam-oracle): extract Oracle into its own resource factory
saifsmailbox98 May 7, 2026
e63ee0a
chore(pam-oracle): rename PamResource.Oracle to PamResource.OracleDB
saifsmailbox98 May 7, 2026
91c764e
fix(pam-oracle): validate Oracle service name characters in schema
saifsmailbox98 May 7, 2026
c220dc5
fix(pam-oracle): fix prettier formatting for regex line
saifsmailbox98 May 7, 2026
090f621
fix(pam-oracle): map ORA-01017 to friendly error message on account save
saifsmailbox98 May 7, 2026
835a039
fix(pam-oracle): use tcps:// connect string for TLS credential valida…
saifsmailbox98 May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/src/ee/routes/v1/pam-account-routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import {
SanitizedMySQLAccountWithResourceSchema,
UpdateMySQLAccountSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import {
CreateOracleAccountSchema,
SanitizedOracleAccountWithResourceSchema,
UpdateOracleAccountSchema
} from "@app/ee/services/pam-resource/oracle/oracle-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresAccountSchema,
Expand Down Expand Up @@ -128,5 +133,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
createAccountSchema: CreateWindowsAccountSchema,
updateAccountSchema: UpdateWindowsAccountSchema
});
},
[PamResource.OracleDB]: async (server: FastifyZodProvider) => {
registerPamAccountEndpoints({
server,
parentType: PamResource.OracleDB,
accountResponseSchema: SanitizedOracleAccountWithResourceSchema,
createAccountSchema: CreateOracleAccountSchema,
updateAccountSchema: UpdateOracleAccountSchema
});
}
};
10 changes: 10 additions & 0 deletions backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
MySQLAccountCredentialsSchema,
SanitizedMySQLAccountWithResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import {
OracleAccountCredentialsSchema,
SanitizedOracleAccountWithResourceSchema
} from "@app/ee/services/pam-resource/oracle/oracle-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { GatewayAccessResponseSchema } from "@app/ee/services/pam-resource/pam-resource-schemas";
import {
Expand Down Expand Up @@ -69,6 +73,7 @@ const SanitizedAccountSchema = z
SanitizedRedisAccountWithResourceSchema,
SanitizedAwsIamAccountWithResourceSchema,
SanitizedWindowsAccountWithResourceSchema,
SanitizedOracleAccountWithResourceSchema,
SanitizedActiveDirectoryAccountWithDomainSchema
])
.and(
Expand Down Expand Up @@ -128,6 +133,10 @@ const AccountCredentialsResponseSchema = z.discriminatedUnion("parentType", [
parentType: z.literal(PamResource.Windows),
credentials: WindowsAccountCredentialsSchema
}),
AccountCredentialsBaseSchema.extend({
parentType: z.literal(PamResource.OracleDB),
credentials: OracleAccountCredentialsSchema
}),
AccountCredentialsBaseSchema.extend({
parentType: z.literal(PamDomainType.ActiveDirectory),
credentials: ActiveDirectoryAccountCredentialsSchema
Expand Down Expand Up @@ -557,6 +566,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Redis) }),
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.SSH) }),
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Kubernetes) }),
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.OracleDB) }),
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Windows) }),
// AWS IAM (no gateway, returns short-lived STS credentials usable by both CLI and console)
z.object({
Expand Down
14 changes: 14 additions & 0 deletions backend/src/ee/routes/v1/pam-resource-routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import {
MySQLResourceSchema,
UpdateMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import {
CreateOracleResourceSchema,
OracleResourceSchema,
UpdateOracleResourceSchema
} from "@app/ee/services/pam-resource/oracle/oracle-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresResourceSchema,
Expand Down Expand Up @@ -134,5 +139,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
createResourceSchema: CreateWindowsResourceSchema,
updateResourceSchema: UpdateWindowsResourceSchema
});
},
[PamResource.OracleDB]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.OracleDB,
resourceResponseSchema: OracleResourceSchema,
createResourceSchema: CreateOracleResourceSchema,
updateResourceSchema: UpdateOracleResourceSchema
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import {
MySQLResourceListItemSchema,
SanitizedMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import {
OracleResourceListItemSchema,
SanitizedOracleResourceSchema
} from "@app/ee/services/pam-resource/oracle/oracle-resource-schemas";
import { PamResource, PamResourceOrderBy } from "@app/ee/services/pam-resource/pam-resource-enums";
import { PAM_AI_INSIGHT_MODELS } from "@app/ee/services/pam-resource/pam-resource-schemas";
import {
Expand Down Expand Up @@ -54,7 +58,8 @@ const SanitizedResourceSchema = z.discriminatedUnion("resourceType", [
SanitizedAwsIamResourceSchema,
SanitizedMongoDBResourceSchema,
SanitizedRedisResourceSchema,
SanitizedWindowsResourceSchema
SanitizedWindowsResourceSchema,
SanitizedOracleResourceSchema
]);

const SanitizedResourceWithFavoriteSchema = z.intersection(
Expand All @@ -71,7 +76,8 @@ const ResourceOptionsSchema = z.discriminatedUnion("resource", [
AwsIamResourceListItemSchema,
MongoDBResourceListItemSchema,
RedisResourceListItemSchema,
WindowsResourceListItemSchema
WindowsResourceListItemSchema,
OracleResourceListItemSchema
]);

export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/ee/routes/v1/pam-session-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PolicyRulesResponseSchema } from "@app/ee/services/pam-account-policy";
import { KubernetesSessionCredentialsSchema } from "@app/ee/services/pam-resource/kubernetes/kubernetes-resource-schemas";
import { MongoDBSessionCredentialsSchema } from "@app/ee/services/pam-resource/mongodb/mongodb-resource-schemas";
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { OracleSessionCredentialsSchema } from "@app/ee/services/pam-resource/oracle/oracle-resource-schemas";
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { RedisSessionCredentialsSchema } from "@app/ee/services/pam-resource/redis/redis-resource-schemas";
import { SSHSessionCredentialsSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
Expand All @@ -29,6 +30,7 @@ const SessionCredentialsSchema = z.union([
SSHSessionCredentialsSchema,
PostgresSessionCredentialsSchema,
MySQLSessionCredentialsSchema,
OracleSessionCredentialsSchema,
MongoDBSessionCredentialsSchema,
KubernetesSessionCredentialsSchema,
RedisSessionCredentialsSchema,
Expand Down
1 change: 1 addition & 0 deletions backend/src/ee/services/pam-account/pam-account-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ export const pamAccountServiceFactory = ({
case PamResource.Postgres:
case PamResource.MySQL:
case PamResource.MsSQL:
case PamResource.OracleDB:
case PamResource.MongoDB:
{
const connectionCredentials = (await decryptResourceConnectionDetails({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ORACLE_TLS_PROBE_TIMEOUT_MS = 10 * 1000;
223 changes: 223 additions & 0 deletions backend/src/ee/services/pam-resource/oracle/oracle-resource-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import oracledb from "oracledb";

import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { BadRequestError } from "@app/lib/errors";
import { GatewayProxyProtocol } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";

import {
TPamResourceFactory,
TPamResourceFactoryRotateAccountCredentials,
TPamResourceFactoryValidateAccountCredentials,
TPamResourceInternalMetadata
} from "../pam-resource-types";
import { probeOracleTls } from "./oracle-resource-fns";
import { TOracleAccountCredentials, TOracleResourceConnectionDetails } from "./oracle-resource-types";

const ORACLE_CONNECT_TIMEOUT_SECONDS = 30;
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";

interface OracleResourceConnection {
validate: (connectOnly: boolean) => Promise<void>;
rotateCredentials: () => Promise<never>;
close: () => Promise<void>;
}

const makeOracleConnection = (
proxyPort: number,
config: {
connectionDetails: TOracleResourceConnectionDetails;
username?: string;
password?: string;
}
): OracleResourceConnection => {
const { connectionDetails } = config;
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
const actualUsername = config.username ?? TEST_CONNECTION_USERNAME;
const actualPassword = config.password ?? TEST_CONNECTION_PASSWORD;

const connectString = sslEnabled
? `tcps://localhost:${proxyPort}/${connectionDetails.database}?ssl_server_dn_match=false`
: `localhost:${proxyPort}/${connectionDetails.database}`;

const openConnection = () =>
oracledb.getConnection({
user: actualUsername,
password: actualPassword,
connectString,
connectTimeout: ORACLE_CONNECT_TIMEOUT_SECONDS,
transportConnectTimeout: ORACLE_CONNECT_TIMEOUT_SECONDS
});

return {
validate: async (connectOnly) => {
// When a custom CA is provided, probe the cert chain first — node-oracledb
// thin mode can't accept an inline CA, so tcps:// only works if the cert
// is already trusted by the system store.
if (sslEnabled && sslCertificate) {
try {
await probeOracleTls({
tcpHost: "localhost",
port: proxyPort,
servername: host,
caPem: sslCertificate,
rejectUnauthorized: sslRejectUnauthorized
});
} catch (error) {
throw new BadRequestError({
message: `Unable to validate connection to Oracle: ${(error as Error).message || String(error)}`
});
}
}

let conn: oracledb.Connection | null = null;
try {
conn = await openConnection();
await conn.execute("SELECT 1 FROM DUAL");
} catch (error) {
if (error instanceof Error) {
const msg = error.message || "";
if (connectOnly && msg.includes("ORA-")) {
// Any Oracle response means the host is reachable.
return;
}
}
// If CA was provided and probe passed, the cert is valid — the tcps://
// failure is likely because the CA isn't in the system trust store.
// Save anyway; creds will be checked on first PAM session.
if (sslEnabled && sslCertificate) {
return;
}
throw new BadRequestError({
message: `Unable to validate connection to Oracle: ${(error as Error).message || String(error)}`
});
} finally {
if (conn) {
await conn.close().catch(() => undefined);
}
}
},
rotateCredentials: async () => {
throw new BadRequestError({ message: "Credential rotation is not yet supported for Oracle resources" });
},
close: async () => {}
};
};

const executeWithGateway = async <T>(
config: {
connectionDetails: TOracleResourceConnectionDetails;
gatewayId: string;
username?: string;
password?: string;
},
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (connection: OracleResourceConnection) => Promise<T>
): Promise<T> => {
const { connectionDetails, gatewayId } = config;
const [targetHost] = await verifyHostInputValidity({
host: connectionDetails.host,
isGateway: true,
isDynamicSecret: false
});
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) => {
const connection = makeOracleConnection(proxyPort, config);
try {
return await operation(connection);
} finally {
await connection.close();
}
},
{
protocol: GatewayProxyProtocol.Tcp,
relayHost: platformConnectionDetails.relayHost,
gateway: platformConnectionDetails.gateway,
relay: platformConnectionDetails.relay
}
);
};

export const oracleResourceFactory: TPamResourceFactory<
TOracleResourceConnectionDetails,
TOracleAccountCredentials,
TPamResourceInternalMetadata
> = (_resourceType, connectionDetails, gatewayId, gatewayV2Service) => {
const validateConnection = async () => {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}

await executeWithGateway({ connectionDetails, gatewayId }, gatewayV2Service, async (client) => {
await client.validate(true);
});
return connectionDetails;
};

const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<TOracleAccountCredentials> = async (
credentials
) => {
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required" });
}

try {
await executeWithGateway(
{
connectionDetails,
gatewayId,
username: credentials.username,
password: credentials.password
},
gatewayV2Service,
async (client) => {
await client.validate(false);
}
);
return credentials;
} catch (error) {
if (error instanceof BadRequestError && error.message.includes("ORA-01017")) {
throw new BadRequestError({
message: "Account credentials invalid: Username or password incorrect"
});
}
throw error;
}
};

const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<TOracleAccountCredentials> = async () => {
throw new BadRequestError({ message: "Credential rotation is not yet supported for Oracle resources" });
};

const handleOverwritePreventionForCensoredValues = async (
updatedAccountCredentials: TOracleAccountCredentials,
currentCredentials: TOracleAccountCredentials
) => {
if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") {
return {
...updatedAccountCredentials,
password: currentCredentials.password
};
}
return updatedAccountCredentials;
};

return {
validateConnection,
validateAccountCredentials,
rotateAccountCredentials,
handleOverwritePreventionForCensoredValues
};
};
Loading
Loading