Merge branch 'main' of https://github.com/Infisical/infisical into fix/suborgs-routing

This commit is contained in:
Piyush Gupta
2025-11-21 01:06:17 +05:30
47 changed files with 813 additions and 222 deletions
@@ -0,0 +1,32 @@
import { Knex } from "knex";
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
import { TableName } from "@app/db/schemas";
const CONSTRAINT_NAME = "unique_pki_acme_account_public_key_and_profile_id";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiAcmeAccount)) {
const hasProfileId = await knex.schema.hasColumn(TableName.PkiAcmeAccount, "profileId");
const hasPublicKeyThumbprint = await knex.schema.hasColumn(TableName.PkiAcmeAccount, "publicKeyThumbprint");
if (hasProfileId && hasPublicKeyThumbprint) {
await knex.schema.alterTable(TableName.PkiAcmeAccount, (table) => {
table.unique(["profileId", "publicKeyThumbprint"], { indexName: CONSTRAINT_NAME });
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiAcmeAccount)) {
const hasProfileId = await knex.schema.hasColumn(TableName.PkiAcmeAccount, "profileId");
const hasPublicKeyThumbprint = await knex.schema.hasColumn(TableName.PkiAcmeAccount, "publicKeyThumbprint");
await knex.schema.alterTable(TableName.PkiAcmeAccount, async () => {
if (hasProfileId && hasPublicKeyThumbprint) {
await dropConstraintIfExists(TableName.PkiAcmeAccount, CONSTRAINT_NAME, knex);
}
});
}
}
+18 -5
View File
@@ -41,17 +41,15 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { credentials, projectId, account } = await server.services.pamAccount.getSessionCredentials(
req.params.sessionId,
req.permission
);
const { credentials, projectId, account, sessionStarted } =
await server.services.pamAccount.getSessionCredentials(req.params.sessionId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId,
event: {
type: EventType.PAM_SESSION_START,
type: EventType.PAM_SESSION_CREDENTIALS_GET,
metadata: {
sessionId: req.params.sessionId,
accountName: account.name
@@ -59,6 +57,21 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
}
});
if (sessionStarted) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId,
event: {
type: EventType.PAM_SESSION_START,
metadata: {
sessionId: req.params.sessionId,
accountName: account.name
}
}
});
}
return { credentials: credentials as z.infer<typeof SessionCredentialsSchema> };
}
});
@@ -186,6 +186,7 @@ export enum EventType {
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
GET_TOKEN_IDENTITY_TOKEN_AUTH = "get-token-identity-token-auth",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
@@ -535,6 +536,7 @@ export enum EventType {
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value",
PAM_SESSION_CREDENTIALS_GET = "pam-session-credentials-get",
PAM_SESSION_START = "pam-session-start",
PAM_SESSION_LOGS_UPDATE = "pam-session-logs-update",
PAM_SESSION_END = "pam-session-end",
@@ -1029,6 +1031,15 @@ interface GetTokensIdentityTokenAuthEvent {
};
}
interface GetTokenIdentityTokenAuthEvent {
type: EventType.GET_TOKEN_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
identityName: string;
tokenId: string;
};
}
interface AddIdentityTokenAuthEvent {
type: EventType.ADD_IDENTITY_TOKEN_AUTH;
metadata: {
@@ -3978,6 +3989,14 @@ interface OrgRoleDeleteEvent {
};
}
interface PamSessionCredentialsGetEvent {
type: EventType.PAM_SESSION_CREDENTIALS_GET;
metadata: {
sessionId: string;
accountName: string;
};
}
interface PamSessionStartEvent {
type: EventType.PAM_SESSION_START;
metadata: {
@@ -4214,6 +4233,7 @@ export type Event =
| CreateTokenIdentityTokenAuthEvent
| UpdateTokenIdentityTokenAuthEvent
| GetTokensIdentityTokenAuthEvent
| GetTokenIdentityTokenAuthEvent
| AddIdentityTokenAuthEvent
| UpdateIdentityTokenAuthEvent
| GetIdentityTokenAuthEvent
@@ -4531,6 +4551,7 @@ export type Event =
| OrgRoleCreateEvent
| OrgRoleUpdateEvent
| OrgRoleDeleteEvent
| PamSessionCredentialsGetEvent
| PamSessionStartEvent
| PamSessionLogsUpdateEvent
| PamSessionEndEvent
@@ -39,3 +39,9 @@ export const getDefaultOnPremFeatures = () => {
};
export const setupLicenseRequestWithStore = () => {};
export const getLicenseKeyConfig = () => {
return {
isValid: false
};
};
+45 -2
View File
@@ -1,13 +1,56 @@
import axios, { AxiosError } from "axios";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env";
import { getConfig, TEnvConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TFeatureSet } from "./license-types";
import { LicenseType, TFeatureSet, TLicenseKeyConfig, TOfflineLicenseContents } from "./license-types";
export const isOfflineLicenseKey = (licenseKey: string): boolean => {
try {
const contents = JSON.parse(Buffer.from(licenseKey, "base64").toString("utf8")) as TOfflineLicenseContents;
return "signature" in contents && "license" in contents;
} catch (error) {
return false;
}
};
export const getLicenseKeyConfig = (
config?: Pick<TEnvConfig, "LICENSE_KEY" | "LICENSE_KEY_OFFLINE">
): TLicenseKeyConfig => {
const cfg = config || getConfig();
if (!cfg) {
return { isValid: false };
}
const licenseKey = cfg.LICENSE_KEY;
if (licenseKey) {
if (isOfflineLicenseKey(licenseKey)) {
return { isValid: true, licenseKey, type: LicenseType.Offline };
}
return { isValid: true, licenseKey, type: LicenseType.Online };
}
const offlineLicenseKey = cfg.LICENSE_KEY_OFFLINE;
// backwards compatibility
if (offlineLicenseKey) {
if (isOfflineLicenseKey(offlineLicenseKey)) {
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
}
return { isValid: false };
}
return { isValid: false };
};
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
_id: null,
@@ -22,9 +22,10 @@ import { OrgPermissionBillingActions, OrgPermissionSubjects } from "../permissio
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums";
import { TLicenseDALFactory } from "./license-dal";
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
import { getDefaultOnPremFeatures, getLicenseKeyConfig, setupLicenseRequestWithStore } from "./license-fns";
import {
InstanceType,
LicenseType,
TAddOrgPmtMethodDTO,
TAddOrgTaxIdDTO,
TCreateOrgPortalSession,
@@ -77,6 +78,7 @@ export const licenseServiceFactory = ({
let instanceType = InstanceType.OnPrem;
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
let selfHostedLicense: TOfflineLicense | null = null;
const licenseKeyConfig = getLicenseKeyConfig(envConfig);
const licenseServerCloudApi = setupLicenseRequestWithStore(
envConfig.LICENSE_SERVER_URL || "",
@@ -85,10 +87,13 @@ export const licenseServiceFactory = ({
envConfig.INTERNAL_REGION
);
const onlineLicenseKey =
licenseKeyConfig.isValid && licenseKeyConfig.type === LicenseType.Online ? licenseKeyConfig.licenseKey : "";
const licenseServerOnPremApi = setupLicenseRequestWithStore(
envConfig.LICENSE_SERVER_URL || "",
LICENSE_SERVER_ON_PREM_LOGIN,
envConfig.LICENSE_KEY || "",
onlineLicenseKey,
envConfig.INTERNAL_REGION
);
@@ -131,7 +136,7 @@ export const licenseServiceFactory = ({
return;
}
if (envConfig.LICENSE_KEY) {
if (licenseKeyConfig.isValid && licenseKeyConfig.type === LicenseType.Online) {
const token = await licenseServerOnPremApi.refreshLicense();
if (token) {
await syncLicenseKeyOnPremFeatures(true);
@@ -142,10 +147,10 @@ export const licenseServiceFactory = ({
return;
}
if (envConfig.LICENSE_KEY_OFFLINE) {
if (licenseKeyConfig.isValid && licenseKeyConfig.type === LicenseType.Offline) {
let isValidOfflineLicense = true;
const contents: TOfflineLicenseContents = JSON.parse(
Buffer.from(envConfig.LICENSE_KEY_OFFLINE, "base64").toString("utf8")
Buffer.from(licenseKeyConfig.licenseKey, "base64").toString("utf8")
);
const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature);
@@ -184,7 +189,7 @@ export const licenseServiceFactory = ({
};
const initializeBackgroundSync = async () => {
if (envConfig.LICENSE_KEY) {
if (licenseKeyConfig?.isValid && licenseKeyConfig?.type === LicenseType.Online) {
logger.info("Setting up background sync process for refresh onPremFeatures");
const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures);
job.start();
@@ -136,3 +136,18 @@ export type TDelOrgTaxIdDTO = TOrgPermission & { taxId: string };
export type TOrgInvoiceDTO = TOrgPermission;
export type TOrgLicensesDTO = TOrgPermission;
export enum LicenseType {
Offline = "offline",
Online = "online"
}
export type TLicenseKeyConfig =
| {
isValid: false;
}
| {
isValid: true;
licenseKey: string;
type: LicenseType;
};
@@ -668,11 +668,6 @@ export const pamAccountServiceFactory = ({
throw new BadRequestError({ message: "Session has ended or expired" });
}
// Verify that the session has not already had credentials fetched
if (session.status !== PamSessionStatus.Starting) {
throw new BadRequestError({ message: "Session has already been started" });
}
const account = await pamAccountDAL.findById(session.accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` });
@@ -689,11 +684,16 @@ export const pamAccountServiceFactory = ({
const decryptedResource = await decryptResource(resource, session.projectId, kmsService);
let sessionStarted = false;
// Mark session as started
await pamSessionDAL.updateById(sessionId, {
status: PamSessionStatus.Active,
startedAt: new Date()
});
if (session.status === PamSessionStatus.Starting) {
await pamSessionDAL.updateById(sessionId, {
status: PamSessionStatus.Active,
startedAt: new Date()
});
sessionStarted = true;
}
return {
credentials: {
@@ -701,7 +701,8 @@ export const pamAccountServiceFactory = ({
...decryptedAccount.credentials
},
projectId: project.id,
account
account,
sessionStarted
};
};
@@ -74,7 +74,12 @@ export const pkiAcmeChallengeServiceFactory = ({
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
// a long running operation for long time. But assuming we are not performing a tons of
// challenge validation at the same time, it should be fine.
const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) });
const challengeResponse = await fetch(challengeUrl, {
// In case if we override the host in the development mode, still provide the original host in the header
// to help the upstream server to validate the request
headers: { Host: host },
signal: AbortSignal.timeout(timeoutMs)
});
if (challengeResponse.status !== 200) {
throw new AcmeIncorrectResponseError({
message: `ACME challenge response is not 200: ${challengeResponse.status}`
@@ -58,7 +58,15 @@ export const GetAcmeDirectoryResponseSchema = z.object({
newNonce: z.string(),
newAccount: z.string(),
newOrder: z.string(),
revokeCert: z.string().optional()
revokeCert: z.string().optional(),
meta: z
.object({
termsOfService: z.string().optional(),
website: z.string().optional(),
caaIdentities: z.array(z.string()).optional(),
externalAccountRequired: z.boolean().optional()
})
.optional()
});
// New Account payload schema
@@ -206,6 +206,9 @@ export const pkiAcmeServiceFactory = ({
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
if (protectedHeader.jwk && protectedHeader.kid) {
throw new AcmeMalformedError({ message: "Both JWK and KID are provided in the protected header" });
}
const parsedUrl = (() => {
try {
return new URL(protectedHeader.url);
@@ -288,6 +291,7 @@ export const pkiAcmeServiceFactory = ({
url,
rawJwsPayload,
getJWK: async (protectedHeader) => {
// get jwk instead of kid
if (!protectedHeader.kid) {
throw new AcmeMalformedError({ message: "KID is required in the protected header" });
}
@@ -353,7 +357,10 @@ export const pkiAcmeServiceFactory = ({
return {
newNonce: buildUrl(profile.id, "/new-nonce"),
newAccount: buildUrl(profile.id, "/new-account"),
newOrder: buildUrl(profile.id, "/new-order")
newOrder: buildUrl(profile.id, "/new-order"),
meta: {
externalAccountRequired: true
}
};
};
@@ -386,11 +393,61 @@ export const pkiAcmeServiceFactory = ({
payload: TCreateAcmeAccountPayload;
}): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
const profile = await validateAcmeProfile(profileId);
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg(
profileId,
alg,
publicKeyThumbprint
);
if (onlyReturnExisting) {
if (!existingAccount) {
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
}
return {
status: 200,
body: {
status: "valid",
contact: existingAccount.emails,
orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`)
},
headers: {
Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`),
Link: `<${buildUrl(profile.id, "/directory")}>;rel="index"`
}
};
}
// Note: We only check EAB for the new account request. This is a very special case for cert-manager.
// There's a bug in their ACME client implementation, they don't take the account KID value they have
// and relying on a '{"onlyReturnExisting": true}' new-account request to find out their KID value.
// But the problem is, that new-account request doesn't come with EAB. And while the get existing account operation
// fails, they just discard the error and proceed to request a new order. Since no KID provided, their ACME
// client will send JWK instead. As a result, we are seeing KID not provide in header error for the new-order
// endpoint.
//
// To solve the problem, we lose the check for EAB a bit for the onlyReturnExisting new account request.
// It should be fine as we've already checked EAB when they created the account.
// And the private key ownership indicating they are the same user.
// ref: https://github.com/cert-manager/cert-manager/issues/7388#issuecomment-3535630925
if (!externalAccountBinding) {
throw new AcmeExternalAccountRequiredError({ message: "External account binding is required" });
}
if (existingAccount) {
return {
status: 200,
body: {
status: "valid",
contact: existingAccount.emails,
orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`)
},
headers: {
Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`),
Link: `<${buildUrl(profile.id, "/directory")}>;rel="index"`
}
};
}
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: profile.projectId,
projectDAL,
@@ -441,30 +498,7 @@ export const pkiAcmeServiceFactory = ({
});
}
const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg(
profileId,
alg,
publicKeyThumbprint
);
if (onlyReturnExisting && !existingAccount) {
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
}
if (existingAccount) {
// With the same public key, we found an existing account, just return it
return {
status: 200,
body: {
status: "valid",
contact: existingAccount.emails,
orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`)
},
headers: {
Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`),
Link: `<${buildUrl(profile.id, "/directory")}>;rel="index"`
}
};
}
// TODO: handle unique constraint violation error, should be very very rare
const newAccount = await acmeAccountDAL.create({
profileId: profile.id,
alg,
+4
View File
@@ -584,6 +584,10 @@ export const TOKEN_AUTH = {
offset: "The offset to start from. If you enter 10, it will start from the 10th token.",
limit: "The number of tokens to return."
},
GET_TOKEN: {
identityId: "The ID of the machine identity to get the token for.",
tokenId: "The ID of the token to get metadata for."
},
CREATE_TOKEN: {
identityId: "The ID of the machine identity to create the token for.",
name: "The name of the token to create."
+1 -1
View File
@@ -400,7 +400,7 @@ const envSchema = z
isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
isBddNockApiEnabled: data.NODE_ENV === "development" && data.BDD_NOCK_API_ENABLED,
isBddNockApiEnabled: data.NODE_ENV !== "production" && data.BDD_NOCK_API_ENABLED,
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",")
.map((el) => {
@@ -0,0 +1,104 @@
import type { Definition } from "nock";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// When running in production, we don't want to even import nock, because it's not needed and it increases memory usage a lots.
// It once caused an outage in the production environment.
// This is why we would rather to crash the app if it's not in development mode (in that case, Kubernetes should stop it from rolling out).
if (process.env.NODE_ENV === "production") {
throw new Error("BDD Nock API can only be enabled in development or test mode");
}
export const registerBddNockRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const importNock = async () => {
// eslint-disable-next-line import/no-extraneous-dependencies
const { default: nock } = await import("nock");
return nock;
};
const checkIfBddNockApiEnabled = () => {
// Note: Please note that this API is only available in development mode and only for BDD tests.
// This endpoint should NEVER BE ENABLED IN PRODUCTION!
if (appCfg.NODE_ENV === "production" || !appCfg.isBddNockApiEnabled) {
throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
}
};
server.route({
method: "POST",
url: "/define",
schema: {
body: z.object({ definitions: z.unknown().array() }),
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
checkIfBddNockApiEnabled();
const { body } = req;
const { definitions } = body;
logger.info(definitions, "Defining nock");
const processedDefinitions = definitions.map((definition: unknown) => {
const { path, ...rest } = definition as Definition;
return {
...rest,
path:
path !== undefined && typeof path === "string"
? path
: new RegExp((path as unknown as { regex: string }).regex ?? "")
} as Definition;
});
const nock = await importNock();
nock.define(processedDefinitions);
// Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
if (!nock.isActive()) {
nock.activate();
}
return { status: "ok" };
}
});
server.route({
method: "POST",
url: "/clean-all",
schema: {
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => {
checkIfBddNockApiEnabled();
logger.info("Cleaning all nocks");
const nock = await importNock();
nock.cleanAll();
return { status: "ok" };
}
});
server.route({
method: "POST",
url: "/restore",
schema: {
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => {
checkIfBddNockApiEnabled();
logger.info("Restore network requests from nock");
const nock = await importNock();
nock.restore();
return { status: "ok" };
}
});
};
@@ -0,0 +1,6 @@
export const registerBddNockRouter = async () => {
// This route is only available in development or test mode.
// The actual implementation is in the dev.ts file and will be aliased to that file in development or test mode.
// And if somehow we try to enable it in production, we will throw an error.
throw new Error("BDD Nock should not be enabled in production");
};
+8 -1
View File
@@ -1,3 +1,4 @@
import { registerBddNockRouter } from "@bdd_routes/bdd-nock-router";
import { CronJob } from "cron";
import { Knex } from "knex";
import { monitorEventLoopDelay } from "perf_hooks";
@@ -2431,6 +2432,7 @@ export const registerRoutes = async (
}
}
await kmsService.startService(hsmStatus);
await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.init();
@@ -2443,7 +2445,6 @@ export const registerRoutes = async (
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await pkiAlertV2Queue.init();
await certificateV3Queue.init();
await kmsService.startService(hsmStatus);
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
await eventBusService.init();
@@ -2698,6 +2699,12 @@ export const registerRoutes = async (
await server.register(registerV3Routes, { prefix: "/api/v3" });
await server.register(registerV4Routes, { prefix: "/api/v4" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
if (getConfig().isBddNockApiEnabled) {
await server.register(registerBddNockRouter, { prefix: "/api/__bdd_nock__" });
}
server.addHook("onClose", async () => {
cronJobs.forEach((job) => job.stop());
await telemetryService.flushAll();
+6 -1
View File
@@ -9,6 +9,8 @@ import {
SuperAdminSchema,
UsersSchema
} from "@app/db/schemas";
import { getLicenseKeyConfig } from "@app/ee/services/license/license-fns";
import { LicenseType } from "@app/ee/services/license/license-types";
import { getConfig, overridableKeys } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
@@ -65,6 +67,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
const config = await getServerCfg();
const serverEnvs = getConfig();
const licenseKeyConfig = getLicenseKeyConfig();
const hasOfflineLicense = licenseKeyConfig.isValid && licenseKeyConfig.type === LicenseType.Offline;
return {
config: {
...config,
@@ -73,7 +78,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING,
kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN,
paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED,
isOfflineUsageReportsEnabled: !!serverEnvs.LICENSE_KEY_OFFLINE
isOfflineUsageReportsEnabled: hasOfflineLicense
}
};
}
@@ -1,87 +0,0 @@
// import { z } from "zod";
// import { getConfig } from "@app/lib/config/env";
// import { ForbiddenRequestError } from "@app/lib/errors";
// import { logger } from "@app/lib/logger";
// import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
// import { AuthMode } from "@app/services/auth/auth-type";
// export const registerBddNockRouter = async (server: FastifyZodProvider) => {
// const checkIfBddNockApiEnabled = () => {
// const appCfg = getConfig();
// // Note: Please note that this API is only available in development mode and only for BDD tests.
// // This endpoint should NEVER BE ENABLED IN PRODUCTION!
// if (appCfg.NODE_ENV !== "development" || !appCfg.isBddNockApiEnabled) {
// throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
// }
// };
// server.route({
// method: "POST",
// url: "/define",
// schema: {
// body: z.object({ definitions: z.unknown().array() }),
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// checkIfBddNockApiEnabled();
// const { body } = req;
// const { definitions } = body;
// logger.info(definitions, "Defining nock");
// const processedDefinitions = definitions.map((definition: unknown) => {
// const { path, ...rest } = definition as Definition;
// return {
// ...rest,
// path:
// path !== undefined && typeof path === "string"
// ? path
// : new RegExp((path as unknown as { regex: string }).regex ?? "")
// } as Definition;
// });
// nock.define(processedDefinitions);
// // Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
// if (!nock.isActive()) {
// nock.activate();
// }
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/clean-all",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Cleaning all nocks");
// nock.cleanAll();
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/restore",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Restore network requests from nock");
// nock.restore();
// return { status: "ok" };
// }
// });
// };
@@ -314,7 +314,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
tokenType: z.literal("Bearer"),
tokenData: IdentityAccessTokensSchema
})
}
},
@@ -346,7 +347,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
tokenData: identityAccessToken
};
}
});
@@ -406,6 +408,60 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
}
});
server.route({
method: "GET",
url: "/token-auth/identities/:identityId/tokens/:tokenId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Get token for machine identity with Token Auth",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(TOKEN_AUTH.GET_TOKEN.identityId),
tokenId: z.string().describe(TOKEN_AUTH.GET_TOKEN.tokenId)
}),
response: {
200: z.object({
token: IdentityAccessTokensSchema
})
}
},
handler: async (req) => {
const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({
identityId: req.params.identityId,
tokenId: req.params.tokenId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId,
event: {
type: EventType.GET_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: token.identityId,
identityName: identityMembershipOrg.identity.name,
tokenId: token.id
}
}
});
return { token };
}
});
server.route({
method: "PATCH",
url: "/token-auth/tokens/:tokenId",
-7
View File
@@ -8,7 +8,6 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
// import { registerBddNockRouter } from "./bdd-nock-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
@@ -238,10 +237,4 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
// if (getConfig().isBddNockApiEnabled) {
// await server.register(registerBddNockRouter, { prefix: "/bdd-nock" });
// }
};
@@ -428,7 +428,13 @@ describe("CertificateProfileService", () => {
service.createProfile({
...mockActor,
projectId: "project-123",
data: validProfileData
data: {
...validProfileData,
enrollmentType: EnrollmentType.ACME,
acmeConfig: {},
apiConfig: undefined,
estConfig: undefined
}
})
).rejects.toThrowError(
new BadRequestError({
@@ -18,7 +18,6 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.where(filter)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(db.ref("name").withSchema(TableName.Identity))
.select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId"))
.first();
@@ -38,6 +38,7 @@ import {
TAttachTokenAuthDTO,
TCreateTokenAuthTokenDTO,
TGetTokenAuthDTO,
TGetTokenAuthTokenByIdDTO,
TGetTokenAuthTokensDTO,
TRevokeTokenAuthDTO,
TRevokeTokenAuthTokenDTO,
@@ -618,6 +619,52 @@ export const identityTokenAuthServiceFactory = ({
return { tokens, identityMembershipOrg };
};
const getTokenAuthTokenById = async ({
tokenId,
identityId,
isActorSuperAdmin,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetTokenAuthTokenByIdDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({
scopeData: {
scope: AccessScope.Organization,
orgId: actorOrgId
},
identityId
});
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
}
const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor,
actorId,
orgId: identityMembershipOrg.scopeOrgId,
actorAuthMethod,
actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
const token = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH,
[`${TableName.IdentityAccessToken}.identityId` as "identityId"]: identityId
});
if (!token) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
return { token, identityMembershipOrg };
};
const updateTokenAuthToken = async ({
tokenId,
name,
@@ -797,6 +844,7 @@ export const identityTokenAuthServiceFactory = ({
revokeIdentityTokenAuth,
createTokenAuthToken,
getTokenAuthTokens,
getTokenAuthTokenById,
updateTokenAuthToken,
revokeTokenAuthToken
};
@@ -40,6 +40,12 @@ export type TGetTokenAuthTokensDTO = {
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthTokenByIdDTO = {
tokenId: string;
identityId: string;
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthTokenDTO = {
tokenId: string;
name?: string;
@@ -1,7 +1,8 @@
import crypto from "crypto";
import { getLicenseKeyConfig } from "@app/ee/services/license/license-fns";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env";
import { LicenseType } from "@app/ee/services/license/license-types";
import { BadRequestError } from "@app/lib/errors";
import { TOfflineUsageReportDALFactory } from "./offline-usage-report-dal";
@@ -30,10 +31,13 @@ export const offlineUsageReportServiceFactory = ({
};
const generateUsageReportCSV = async () => {
const cfg = getConfig();
if (!cfg.LICENSE_KEY_OFFLINE) {
const licenseKeyConfig = getLicenseKeyConfig();
const hasOfflineLicense = licenseKeyConfig.isValid && licenseKeyConfig.type === LicenseType.Offline;
if (!hasOfflineLicense) {
throw new BadRequestError({
message: "Offline usage reports are not enabled. LICENSE_KEY_OFFLINE must be configured."
message:
"Offline usage reports are not enabled. Usage reports are only available for self-hosted offline instances"
});
}