mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
60 Commits
feat/autom
...
misc/add-e
Author | SHA1 | Date | |
---|---|---|---|
e5947fcab9 | |||
f7cf2bb78f | |||
ff24e76a32 | |||
6ac802b6c9 | |||
ff92e00503 | |||
b20474c505 | |||
e19ffc91c6 | |||
61eb66efca | |||
15999daa24 | |||
ec31211bca | |||
0ecf6044d9 | |||
6c512f47bf | |||
33b135f02c | |||
eed7cc6408 | |||
440ada464f | |||
6b7abbbeb9 | |||
3944e20a5b | |||
9ad725fd6c | |||
9a954c8f15 | |||
81a64d081c | |||
43804f62e6 | |||
67089af17a | |||
d83240749f | |||
36144d8c42 | |||
c487b2b34a | |||
8e20531b40 | |||
8ead2aa774 | |||
1b2128e3cc | |||
ad6f285b59 | |||
d4842dd273 | |||
78f83cb478 | |||
c8a871de7c | |||
64c0951df3 | |||
c185414a3c | |||
f9695741f1 | |||
b7c4b11260 | |||
81f3613393 | |||
a7fe79c046 | |||
9eb89bb46d | |||
c4da1ce32d | |||
add97c9b38 | |||
768ba4f4dc | |||
18c32d872c | |||
1fd40ab6ab | |||
9d258f57ce | |||
45ccbaf4c9 | |||
838c1af448 | |||
e53439d586 | |||
cc7d0d752f | |||
b89212a0c9 | |||
d4c69d8e5d | |||
48943b4d78 | |||
fd1afc2cbe | |||
6905029455 | |||
2ef77c737a | |||
0f31fa3128 | |||
1da5a5f417 | |||
94d7d2b029 | |||
e39d1a0530 | |||
4c5f3859d6 |
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,5 +1,3 @@
|
||||
.direnv/
|
||||
|
||||
# backend
|
||||
node_modules
|
||||
.env
|
||||
@ -28,6 +26,8 @@ node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
.env
|
||||
|
||||
# testing
|
||||
coverage
|
||||
reports
|
||||
@ -63,12 +63,10 @@ yarn-error.log*
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
**/.idea/*
|
||||
.idea/*
|
||||
|
||||
frontend-build
|
||||
|
||||
# cli
|
||||
.go/
|
||||
*.tgz
|
||||
cli/infisical-merge
|
||||
cli/test/infisical-merge
|
||||
|
@ -1,7 +0,0 @@
|
||||
import "@fastify/request-context";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
}
|
||||
}
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -100,6 +100,12 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
identityAuthInfo?: {
|
||||
identityId: string;
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
// not setting a foreign constraint so that cascade effects are not triggered
|
||||
if (!doesGatewayColExist) {
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (!hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.jsonb("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.dropColumn("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
|
||||
boundSubject: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional()
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||
claimMetadataMapping: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
|
@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional()
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share")
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@ -978,6 +978,7 @@ interface AddIdentityOidcAuthEvent {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -1002,6 +1003,7 @@ interface UpdateIdentityOidcAuthEvent {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
@ -50,7 +50,7 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
|
||||
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
||||
|
||||
export const licenseServiceFactory = ({
|
||||
@ -142,7 +142,10 @@ export const licenseServiceFactory = ({
|
||||
try {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
if (cachedPlan) {
|
||||
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
}
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
@ -170,6 +173,8 @@ export const licenseServiceFactory = ({
|
||||
JSON.stringify(onPremFeatures)
|
||||
);
|
||||
return onPremFeatures;
|
||||
} finally {
|
||||
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
}
|
||||
return onPremFeatures;
|
||||
};
|
||||
|
@ -131,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
|
||||
}
|
||||
}
|
||||
|
||||
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
||||
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
|
||||
const handler = {
|
||||
get(target: Record<string, string>, prop: string) {
|
||||
if (!(prop in target)) {
|
||||
if (!Object.hasOwn(target, prop)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
|
||||
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
|
||||
}
|
||||
return target[prop];
|
||||
}
|
||||
@ -145,4 +145,4 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
||||
return new Proxy(obj, handler);
|
||||
};
|
||||
|
||||
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
|
||||
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
||||
|
||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||
import { TPermissionDALFactory } from "./permission-dal";
|
||||
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
|
||||
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
|
||||
import {
|
||||
TBuildOrgPermissionDTO,
|
||||
TBuildProjectPermissionDTO,
|
||||
@ -243,20 +244,22 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
),
|
||||
"identity.metadata"
|
||||
);
|
||||
const templateValue = {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
identity: templateValue
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -317,21 +320,26 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
const unescapedMetadata = objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
);
|
||||
|
||||
const identityAuthInfo =
|
||||
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
|
||||
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
|
||||
: {};
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||
const templateValue = {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair,
|
||||
auth: identityAuthInfo
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
identity: templateValue
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -424,20 +432,22 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
),
|
||||
"identity.metadata"
|
||||
);
|
||||
const templateValue = {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: userProjectPermission.userId,
|
||||
username: userProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
identity: templateValue
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
@ -469,21 +479,22 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
),
|
||||
"identity.metadata"
|
||||
);
|
||||
|
||||
const templateValue = {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
};
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
identity: templateValue
|
||||
},
|
||||
{ data: false }
|
||||
);
|
||||
|
@ -329,6 +329,7 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The list of intended recipients.",
|
||||
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
@ -342,6 +343,7 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The new list of intended recipients.",
|
||||
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The new expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
@ -1725,7 +1727,8 @@ export const SecretSyncs = {
|
||||
SYNC_OPTIONS: (destination: SecretSync) => {
|
||||
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||
return {
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
|
||||
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
|
||||
};
|
||||
},
|
||||
ADDITIONAL_SYNC_OPTIONS: {
|
||||
|
34
backend/src/lib/template/dot-access.ts
Normal file
34
backend/src/lib/template/dot-access.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Safely retrieves a value from a nested object using dot notation path
|
||||
*/
|
||||
export const getStringValueByDot = (
|
||||
obj: Record<string, unknown> | null | undefined,
|
||||
path: string,
|
||||
defaultValue?: string
|
||||
): string | undefined => {
|
||||
// Handle null or undefined input
|
||||
if (!obj) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const parts = path.split(".");
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
const isObject = typeof current === "object" && !Array.isArray(current) && current !== null;
|
||||
if (!isObject) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (!Object.hasOwn(current as object, part)) {
|
||||
// Check if the property exists as an own property
|
||||
return defaultValue;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
if (typeof current !== "string") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import fp from "fastify-plugin";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
@ -141,6 +142,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
authMethod: null,
|
||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId)
|
||||
};
|
||||
if (token?.identityAuth?.oidc) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
oidc: token?.identityAuth?.oidc
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
import { monitorEventLoopDelay } from "perf_hooks";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
|
||||
@ -96,6 +97,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig, TEnvConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||
@ -246,6 +248,9 @@ import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
|
||||
const histogram = monitorEventLoopDelay({ resolution: 20 });
|
||||
histogram.enable();
|
||||
|
||||
export const registerRoutes = async (
|
||||
server: FastifyZodProvider,
|
||||
{
|
||||
@ -1630,6 +1635,18 @@ export const registerRoutes = async (
|
||||
const cfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
const meanLagMs = histogram.mean / 1e6;
|
||||
const maxLagMs = histogram.max / 1e6;
|
||||
const p99LagMs = histogram.percentile(99) / 1e6;
|
||||
|
||||
logger.info(
|
||||
`Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed(
|
||||
2
|
||||
)}ms`
|
||||
);
|
||||
|
||||
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
|
||||
|
||||
// try {
|
||||
// await db.raw("SELECT NOW()");
|
||||
// } catch (err) {
|
||||
|
@ -24,6 +24,7 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
|
||||
boundIssuer: true,
|
||||
boundAudiences: true,
|
||||
boundClaims: true,
|
||||
claimMetadataMapping: true,
|
||||
boundSubject: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
@ -105,6 +106,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
|
||||
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
|
||||
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
|
||||
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(),
|
||||
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
@ -163,6 +165,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: identityOidcAuth.boundIssuer,
|
||||
boundAudiences: identityOidcAuth.boundAudiences,
|
||||
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
|
||||
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
|
||||
boundSubject: identityOidcAuth.boundSubject as string,
|
||||
accessTokenTTL: identityOidcAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
|
||||
@ -202,6 +205,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer),
|
||||
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences),
|
||||
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims),
|
||||
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(),
|
||||
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
@ -260,6 +264,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: identityOidcAuth.boundIssuer,
|
||||
boundAudiences: identityOidcAuth.boundAudiences,
|
||||
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
|
||||
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
|
||||
boundSubject: identityOidcAuth.boundSubject as string,
|
||||
accessTokenTTL: identityOidcAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
|
||||
|
@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
|
||||
clientSecretId: string;
|
||||
identityAccessTokenId: string;
|
||||
authTokenType: string;
|
||||
identityAuth: {
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@ -178,8 +179,9 @@ export const identityJwtAuthServiceFactory = ({
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
|
||||
if (!tokenData[claimKey]) {
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
|
@ -102,7 +102,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||
},
|
||||
|
||||
signal: AbortSignal.timeout(10000),
|
||||
timeout: 10000,
|
||||
// if ca cert, rejectUnauthorized: true
|
||||
httpsAgent: new https.Agent({
|
||||
ca: caCert,
|
||||
|
@ -12,6 +12,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@ -78,7 +79,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: requestAgent
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
const jwksUri = discoveryDoc.jwks_uri;
|
||||
@ -92,7 +93,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const client = new JwksClient({
|
||||
jwksUri,
|
||||
requestAgent
|
||||
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
});
|
||||
|
||||
const { kid } = decodedToken.header;
|
||||
@ -109,7 +110,6 @@ export const identityOidcAuthServiceFactory = ({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -136,10 +136,16 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.boundClaims) {
|
||||
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC claim not allowed."
|
||||
});
|
||||
@ -147,6 +153,20 @@ export const identityOidcAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const filteredClaims: Record<string, string> = {};
|
||||
if (identityOidcAuth.claimMetadataMapping) {
|
||||
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
||||
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
filteredClaims[permissionKey] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
@ -168,7 +188,12 @@ export const identityOidcAuthServiceFactory = ({
|
||||
{
|
||||
identityId: identityOidcAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||
identityAuth: {
|
||||
oidc: {
|
||||
claims: filteredClaims
|
||||
}
|
||||
}
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
@ -189,6 +214,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@ -257,6 +283,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
@ -277,6 +304,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@ -338,6 +366,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
|
@ -7,6 +7,7 @@ export type TAttachOidcAuthDTO = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -22,6 +23,7 @@ export type TUpdateOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
@ -382,6 +382,8 @@ export const AwsParameterStoreSyncFns = {
|
||||
}
|
||||
}
|
||||
|
||||
if (syncOptions.disableSecretDeletion) return;
|
||||
|
||||
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||
|
||||
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||
|
@ -396,6 +396,8 @@ export const AwsSecretsManagerSyncFns = {
|
||||
}
|
||||
}
|
||||
|
||||
if (syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
|
||||
try {
|
||||
|
@ -136,6 +136,8 @@ export const azureAppConfigurationSyncFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const key of Object.keys(azureAppConfigSecrets)) {
|
||||
const azureSecret = azureAppConfigSecrets[key];
|
||||
if (
|
||||
|
@ -189,6 +189,8 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
});
|
||||
}
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const deleteSecretKey of deleteSecrets.filter(
|
||||
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
|
||||
)) {
|
||||
|
@ -112,6 +112,8 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
|
||||
accessToken
|
||||
});
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const secret of databricksSecretKeys) {
|
||||
if (!(secret.key in secretMap)) {
|
||||
await deleteDatabricksSecrets({
|
||||
|
@ -71,8 +71,16 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
||||
|
||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||
} catch (error) {
|
||||
// when a secret in GCP has no versions, we treat it as if it's a blank value
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
// when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value
|
||||
if (
|
||||
error instanceof AxiosError &&
|
||||
(error.response?.status === 404 ||
|
||||
(error.response?.status === 400 &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
error.response.data.error.status === "FAILED_PRECONDITION" &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
|
||||
error.response.data.error.message.match(/(?:disabled|destroyed)/i)))
|
||||
) {
|
||||
res[key] = "";
|
||||
} else {
|
||||
throw new SecretSyncError({
|
||||
@ -147,6 +155,9 @@ export const GcpSyncFns = {
|
||||
for await (const key of Object.keys(gcpSecrets)) {
|
||||
try {
|
||||
if (!(key in secretMap) || !secretMap[key].value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (secretSync.syncOptions.disableSecretDeletion) continue;
|
||||
|
||||
// case: delete secret
|
||||
await request.delete(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||
|
@ -192,12 +192,6 @@ export const GithubSyncFns = {
|
||||
|
||||
const publicKey = await getPublicKey(client, secretSync);
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (!(encryptedSecret.name in secretMap)) {
|
||||
await deleteSecret(client, secretSync, encryptedSecret);
|
||||
}
|
||||
}
|
||||
|
||||
await sodium.ready.then(async () => {
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
// convert secret & base64 key to Uint8Array.
|
||||
@ -224,6 +218,14 @@ export const GithubSyncFns = {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (!(encryptedSecret.name in secretMap)) {
|
||||
await deleteSecret(client, secretSync, encryptedSecret);
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
|
@ -196,6 +196,8 @@ export const HumanitecSyncFns = {
|
||||
}
|
||||
}
|
||||
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const humanitecSecret of humanitecSecrets) {
|
||||
if (!secretMap[humanitecSecret.key]) {
|
||||
await deleteSecret(secretSync, humanitecSecret);
|
||||
|
@ -23,7 +23,8 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||
initialSyncBehavior: (canImportSecrets
|
||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
|
||||
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
|
||||
});
|
||||
|
||||
const schema = merge ? baseSchema.merge(merge) : baseSchema;
|
||||
|
@ -2,12 +2,6 @@ package api
|
||||
|
||||
import "time"
|
||||
|
||||
type Environment struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Stores info for login one
|
||||
type LoginOneRequest struct {
|
||||
Email string `json:"email"`
|
||||
@ -20,6 +14,7 @@ type LoginOneResponse struct {
|
||||
}
|
||||
|
||||
// Stores info for login two
|
||||
|
||||
type LoginTwoRequest struct {
|
||||
Email string `json:"email"`
|
||||
ClientProof string `json:"clientProof"`
|
||||
@ -173,10 +168,9 @@ type Secret struct {
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Environments []Environment `json:"environments"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
type RawSecret struct {
|
||||
|
@ -15,9 +15,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/fatih/color"
|
||||
@ -62,11 +59,11 @@ var runCmd = &cobra.Command{
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
environmentSlug, _ := cmd.Flags().GetString("env")
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentSlug = environmentFromWorkspace
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,20 +136,8 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Confirming selected environment is valid: %s", environmentSlug)
|
||||
|
||||
hasEnvironment, err := confirmProjectHasEnvironment(environmentSlug, projectId, token)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Could not confirm project has environment")
|
||||
}
|
||||
if !hasEnvironment {
|
||||
util.HandleError(fmt.Errorf("project does not have environment '%s'", environmentSlug))
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Project '%s' has environment '%s'", projectId, environmentSlug)
|
||||
|
||||
request := models.GetAllSecretsParameters{
|
||||
Environment: environmentSlug,
|
||||
Environment: environmentName,
|
||||
WorkspaceId: projectId,
|
||||
TagSlugs: tagSlugs,
|
||||
SecretsPath: secretsPath,
|
||||
@ -323,6 +308,7 @@ func waitForExitCommand(cmd *exec.Cmd) (int, error) {
|
||||
}
|
||||
|
||||
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) {
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var err error
|
||||
var lastSecretsFetch time.Time
|
||||
@ -453,53 +439,8 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
|
||||
}
|
||||
}
|
||||
|
||||
func confirmProjectHasEnvironment(environmentSlug, projectId string, token *models.TokenDetails) (bool, error) {
|
||||
var accessToken string
|
||||
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
accessToken = token.Token
|
||||
} else {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to authenticate")
|
||||
}
|
||||
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
accessToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get local project details")
|
||||
}
|
||||
|
||||
projectId = workspaceFile.WorkspaceId
|
||||
}
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
project, err := api.CallGetProjectById(httpClient, projectId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, env := range project.Environments {
|
||||
if env.Slug == environmentSlug {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
request.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
|
@ -143,7 +143,15 @@ var secretsSetCmd = &cobra.Command{
|
||||
Short: "Used set secrets",
|
||||
Use: "set [secrets]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("file") {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return cobra.MinimumNArgs(1)(cmd, args)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
@ -177,13 +185,18 @@ var secretsSetCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse secret type")
|
||||
}
|
||||
|
||||
file, err := cmd.Flags().GetString("file")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
var secretOperations []models.SecretSetOperation
|
||||
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||
if projectId == "" {
|
||||
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
|
||||
}
|
||||
|
||||
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token)
|
||||
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file)
|
||||
} else {
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -206,7 +219,7 @@ var secretsSetCmd = &cobra.Command{
|
||||
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
|
||||
Type: "",
|
||||
Token: loggedInUserDetails.UserCredentials.JTWToken,
|
||||
})
|
||||
}, file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -691,6 +704,7 @@ func init() {
|
||||
secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth")
|
||||
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
|
||||
secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared")
|
||||
secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.")
|
||||
|
||||
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
|
||||
secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zalando/go-keyring"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) {
|
||||
@ -232,6 +233,7 @@ func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tag
|
||||
|
||||
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
var secretsToReturn []models.SingleEnvironmentVariable
|
||||
// var serviceTokenDetails api.GetServiceTokenDetailsResponse
|
||||
var errorToReturn error
|
||||
|
||||
if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
|
||||
@ -563,7 +565,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str
|
||||
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
|
||||
}
|
||||
|
||||
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) {
|
||||
func parseSecrets(fileName string, content string) (map[string]string, error) {
|
||||
secrets := make(map[string]string)
|
||||
|
||||
if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") {
|
||||
// Handle YAML secrets
|
||||
var yamlData map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML file: %v", err)
|
||||
}
|
||||
|
||||
for key, value := range yamlData {
|
||||
if strValue, ok := value.(string); ok {
|
||||
secrets[key] = strValue
|
||||
} else {
|
||||
return nil, fmt.Errorf("YAML secret '%s' must be a string", key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle .env files
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Ignore empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure it's a valid key=value pair
|
||||
splitKeyValue := strings.SplitN(line, "=", 2)
|
||||
if len(splitKeyValue) != 2 {
|
||||
return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line)
|
||||
}
|
||||
|
||||
key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1])
|
||||
|
||||
// Handle quoted values
|
||||
if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
|
||||
(strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
|
||||
value = value[1 : len(value)-1] // Remove surrounding quotes
|
||||
}
|
||||
|
||||
secrets[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func validateSecretKey(key string) error {
|
||||
if key == "" {
|
||||
return errors.New("secret keys cannot be empty")
|
||||
}
|
||||
if unicode.IsNumber(rune(key[0])) {
|
||||
return fmt.Errorf("secret key '%s' cannot start with a number", key)
|
||||
}
|
||||
if strings.Contains(key, " ") {
|
||||
return fmt.Errorf("secret key '%s' cannot contain spaces", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) {
|
||||
if file != "" {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
PrintErrorMessageAndExit("File does not exist")
|
||||
}
|
||||
return nil, fmt.Errorf("unable to process file [err=%v]", err)
|
||||
}
|
||||
|
||||
parsedSecrets, err := parseSecrets(file, string(content))
|
||||
if err != nil {
|
||||
PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err))
|
||||
}
|
||||
|
||||
// Step 2: Validate secrets
|
||||
for key, value := range parsedSecrets {
|
||||
if err := validateSecretKey(key); err != nil {
|
||||
PrintErrorMessageAndExit(err.Error())
|
||||
}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key))
|
||||
}
|
||||
secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
if len(secretArgs) == 0 {
|
||||
PrintErrorMessageAndExit("no valid secrets found in the file")
|
||||
}
|
||||
}
|
||||
|
||||
if tokenDetails == nil {
|
||||
return nil, fmt.Errorf("unable to process set secret operations, token details are missing")
|
||||
|
@ -76,6 +76,7 @@ func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUserAuth_SecretsGetAll(t *testing.T) {
|
||||
|
@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--file">
|
||||
Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory.
|
||||
|
||||
The file should contain secrets in the following formats:
|
||||
- `key=value` for `.env` files
|
||||
- `key: value` for YAML files
|
||||
|
||||
Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing.
|
||||
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical secrets set --file="./.env"
|
||||
```
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical secrets delete">
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
After Width: | Height: | Size: 641 KiB |
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: "Machine identities"
|
||||
description: "Learn how to set metadata and leverage authentication attributes for machine identities."
|
||||
---
|
||||
|
||||
Machine identities can have metadata set manually, just like users. In addition, during the machine authentication process (e.g., via OIDC), extra attributes called claims—are provided, which can be used in your ABAC policies.
|
||||
|
||||
#### Setting Metadata on Machine Identities
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Manually Configure Metadata">
|
||||
<Steps>
|
||||
<Step title="Navigate to the Access Control page on the organization sidebar and select a machine identity.">
|
||||
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png" />
|
||||
</Step>
|
||||
<Step title="On the machine identity page, click the pencil icon to edit the selected identity.">
|
||||
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png" />
|
||||
</Step>
|
||||
<Step title="Add metadata via key-value pairs and update the machine identity.">
|
||||
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png" />
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
#### Accessing Attributes From Machine Identity Login
|
||||
|
||||
When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
|
||||
For methods like OIDC, these come as claims in the token and can be made available in your policies.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="OIDC Login Attributes">
|
||||
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
|
||||
2. In the **Advanced section**, locate the Claim Mapping configuration.
|
||||
3. Map the OIDC claims to permission attributes by specifying:
|
||||
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
|
||||
- **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
|
||||
|
||||
For example, if your OIDC provider returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "machine456",
|
||||
"name": "Service A",
|
||||
"user": {
|
||||
"department": "engineering",
|
||||
"role": "service"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You might map:
|
||||
|
||||
- **department:** to `user.department`
|
||||
- **role:** to `user.role`
|
||||
|
||||
Once configured, these attributes become available in your policies using the following format:
|
||||
|
||||
```
|
||||
{{ identity.auth.oidc.claims.<permission claim name> }}
|
||||
```
|
||||
|
||||
<img src="/images/platform/access-controls/abac-policy-oidc-format.png" />
|
||||
</Tab>
|
||||
<Tab title="Other Authentication Method Attributes">
|
||||
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
|
||||
</Tab>
|
||||
</Tabs>
|
@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "Users identities"
|
||||
description: "How to set and use metadata attributes on user identities for ABAC."
|
||||
---
|
||||
|
||||
User identities can have metadata attributes assigned directly. These attributes (such as location or department) are used to define dynamic access policies.
|
||||
|
||||
#### Setting Metadata on Users
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Manually Configure Metadata">
|
||||
<Steps>
|
||||
<Step title="Navigate to the Access Control page on the organization sidebar and select a user.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step1.png" />
|
||||
</Step>
|
||||
<Step title="On the User Page, click the pencil icon to edit the selected user.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step2.png" />
|
||||
</Step>
|
||||
<Step title="Add metadata via key-value pairs and update the user identity.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step3.png" />
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Automatically Populate Metadata">
|
||||
For organizations using SAML for **user logins**, Infisical automatically maps metadata attributes from SAML assertions to user identities on every login. This enables dynamic policies based on the user's SAML attributes.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
#### Applying ABAC Policies with User Metadata
|
||||
Attribute-based access controls are currently only available for polices defined on Secrets Manager projects.
|
||||
You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
|
||||
|
||||
<img src="/images/platform/access-controls/example-abac-1.png" />
|
||||
|
||||
In your policies, metadata values are accessed as follows:
|
||||
|
||||
- **User ID:** `{{ identity.id }}` (always available)
|
||||
- **Username:** `{{ identity.username }}` (always available)
|
||||
- **Metadata Attributes:** `{{ identity.metadata.<metadata-key-name> }}` (available if set)
|
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "Learn the basics of ABAC for both users and machine identities."
|
||||
---
|
||||
|
||||
Infisical's Attribute-based Access Controls (ABAC) enable dynamic, attribute-driven permissions for both users and machine identities. ABAC enforces fine-grained, context-aware access controls using metadata attributes—stored as key-value pairs—either attached to identities or provided during authentication.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Users" icon="square-1" href="./managing-user-metadata">
|
||||
Manage user metadata manually or automatically via SAML logins.
|
||||
</Card>
|
||||
<Card title="Machine Identities" icon="square-2" href="./managing-machine-identity-attributes">
|
||||
Set metadata manually like users and access additional attributes provided during machine authentication (for example, OIDC claims).
|
||||
</Card>
|
||||
</CardGroup>
|
@ -1,65 +0,0 @@
|
||||
---
|
||||
title: "Attribute-based Access Controls"
|
||||
description: "Learn how to use ABAC to manage permissions based on identity attributes."
|
||||
---
|
||||
|
||||
Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities.
|
||||
ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware.
|
||||
|
||||
In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities.
|
||||
During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation.
|
||||
|
||||
## Project-level Permissions
|
||||
|
||||
Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags.
|
||||
|
||||
### Setting Metadata on Identities
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Manually Configure Metadata">
|
||||
<Steps>
|
||||
<Step title="Navigate to the Access Control page on the organization sidebar and select an identity (user or machine).">
|
||||
<img src="/images/platform/access-controls/add-metadata-step1.png" />
|
||||
</Step>
|
||||
<Step title="On the Identity Page, click the pencil icon to edit the selected identity.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step2.png" />
|
||||
</Step>
|
||||
<Step title="Add metadata via key-value pairs and update the identity.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step3.png" />
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Automatically Populate Metadata">
|
||||
For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities.
|
||||
This makes it easy to create policies that dynamically adapt based on the SAML user’s attributes.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Defining ABAC Policies
|
||||
|
||||
<img src="/images/platform/access-controls/example-abac-1.png" />
|
||||
|
||||
ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ <attribute-name> }}`.
|
||||
The following attributes are available within project permissions:
|
||||
|
||||
- **User ID**: `{{ identity.id }}`
|
||||
- **Username**: `{{ identity.username }}`
|
||||
- **Metadata Attributes**: `{{ identity.metadata.<metadata-key-name> }}`
|
||||
|
||||
During policy execution, these placeholders are replaced by their actual values prior to evaluation.
|
||||
|
||||
### Example Use Case
|
||||
|
||||
#### Location-based Access Control
|
||||
|
||||
Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region.
|
||||
You could assign a `location` attribute to each user (e.g., `identity.metadata.location`).
|
||||
You could then structure your folders to align with this attribute and define permissions accordingly.
|
||||
|
||||
For example, a policy might restrict access to folders matching the user's location attribute in the following pattern:
|
||||
```
|
||||
/appA/{{ identity.metadata.location }}
|
||||
```
|
||||
Using this structure, users can only access folders that correspond to their configured `location` attribute.
|
||||
Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location.
|
@ -18,7 +18,7 @@ To make sure that users and machine identities are only accessing the resources
|
||||
|
||||
<Card
|
||||
title="Attribute-based Access Control"
|
||||
href="./attribute-based-access-controls"
|
||||
href="/documentation/platform/access-controls/abac"
|
||||
icon="address-book"
|
||||
color="#000000"
|
||||
>
|
||||
|
@ -114,6 +114,13 @@ using the Universal Auth authentication method.
|
||||
that is to exchange the **Client ID** and **Client Secret** of the identity for an access token
|
||||
by making a request to the `/api/v1/auth/universal-auth/login` endpoint.
|
||||
|
||||
<Tip>
|
||||
Choose the correct base URL based on your region:
|
||||
|
||||
- For Infisical Cloud US users: `https://app.infisical.com`
|
||||
- For Infisical Cloud EU users: `https://eu.infisical.com`
|
||||
</Tip>
|
||||
|
||||
#### Sample request
|
||||
|
||||
```bash Request
|
||||
|
@ -66,7 +66,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
<Step title="Configure HSM on Infisical">
|
||||
|
||||
<Warning>
|
||||
Are you using Docker? If you are using Docker, please follow the instructions in the [Using HSM's with Docker](#using-hsms-with-docker) section.
|
||||
Are you using Docker or Kubernetes for your deployment? If you are using Docker or Kubernetes, please follow the instructions in the [Using HSM's in your Deployment](#using-hsms-in-your-deployment) section.
|
||||
</Warning>
|
||||
|
||||
Configuring the HSM on Infisical requires setting a set of environment variables:
|
||||
@ -94,165 +94,447 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
</Steps>
|
||||
|
||||
|
||||
## Using HSMs with Docker
|
||||
When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker.
|
||||
## Using HSMs In Your Deployment
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Thales Luna Cloud HSM">
|
||||
<Steps>
|
||||
<Step title="Create HSM client folder">
|
||||
When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`.
|
||||
<Tab title="Docker">
|
||||
When using Docker, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Docker.
|
||||
|
||||
```bash
|
||||
mkdir /etc/luna-docker
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Thales Luna Cloud HSM">
|
||||
<Steps>
|
||||
<Step title="Create HSM client folder">
|
||||
When using Docker, you are able to set your HSM library path to any location on your machine. In this example, we are going to be using `/etc/luna-docker`.
|
||||
|
||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||
```bash
|
||||
mkdir /etc/luna-docker
|
||||
```
|
||||
|
||||
A folder structure of a client folder will often look like this:
|
||||
```
|
||||
partition-ca-certificate.pem
|
||||
partition-certificate.pem
|
||||
server-certificate.pem
|
||||
Chrystoki.conf
|
||||
/plugins
|
||||
libcloud.plugin
|
||||
/lock
|
||||
/libs
|
||||
/64
|
||||
libCryptoki2.so
|
||||
/jsp
|
||||
LunaProvider.jar
|
||||
/64
|
||||
libLunaAPI.so
|
||||
/etc
|
||||
openssl.cnf
|
||||
/bin
|
||||
/64
|
||||
ckdemo
|
||||
lunacm
|
||||
multitoken
|
||||
vtl
|
||||
```
|
||||
|
||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||
|
||||
```bash
|
||||
cp -r /<path-to-where-your-luna-client-is-located> /etc/luna-docker
|
||||
```
|
||||
A folder structure of a client folder will often look like this:
|
||||
```
|
||||
partition-ca-certificate.pem
|
||||
partition-certificate.pem
|
||||
server-certificate.pem
|
||||
Chrystoki.conf
|
||||
/plugins
|
||||
libcloud.plugin
|
||||
/lock
|
||||
/libs
|
||||
/64
|
||||
libCryptoki2.so
|
||||
/jsp
|
||||
LunaProvider.jar
|
||||
/64
|
||||
libLunaAPI.so
|
||||
/etc
|
||||
openssl.cnf
|
||||
/bin
|
||||
/64
|
||||
ckdemo
|
||||
lunacm
|
||||
multitoken
|
||||
vtl
|
||||
```
|
||||
|
||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||
|
||||
</Step>
|
||||
```bash
|
||||
cp -r /<path-to-where-your-luna-client-is-located> /etc/luna-docker
|
||||
```
|
||||
|
||||
<Step title="Update Chrystoki.conf">
|
||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||
</Step>
|
||||
|
||||
In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container.
|
||||
<Step title="Update Chrystoki.conf">
|
||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||
|
||||
An example config file will look like this:
|
||||
In this example, we will be mounting the `/etc/luna-docker` folder to the Docker container under a different path. The path we will use in this example is `/usr/safenet/lunaclient`. This means `/etc/luna-docker` will be mounted to `/usr/safenet/lunaclient` in the Docker container.
|
||||
|
||||
```Chrystoki.conf
|
||||
Chrystoki2 = {
|
||||
# This path points to the mounted path, /usr/safenet/lunaclient
|
||||
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
|
||||
}
|
||||
An example config file will look like this:
|
||||
|
||||
Luna = {
|
||||
DefaultTimeOut = 500000;
|
||||
PEDTimeout1 = 100000;
|
||||
PEDTimeout2 = 200000;
|
||||
PEDTimeout3 = 20000;
|
||||
KeypairGenTimeOut = 2700000;
|
||||
CloningCommandTimeOut = 300000;
|
||||
CommandTimeOutPedSet = 720000;
|
||||
}
|
||||
```Chrystoki.conf
|
||||
Chrystoki2 = {
|
||||
# This path points to the mounted path, /usr/safenet/lunaclient
|
||||
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
|
||||
}
|
||||
|
||||
CardReader = {
|
||||
LunaG5Slots = 0;
|
||||
RemoteCommand = 1;
|
||||
}
|
||||
Luna = {
|
||||
DefaultTimeOut = 500000;
|
||||
PEDTimeout1 = 100000;
|
||||
PEDTimeout2 = 200000;
|
||||
PEDTimeout3 = 20000;
|
||||
KeypairGenTimeOut = 2700000;
|
||||
CloningCommandTimeOut = 300000;
|
||||
CommandTimeOutPedSet = 720000;
|
||||
}
|
||||
|
||||
Misc = {
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
PluginModuleDir = /usr/safenet/lunaclient/plugins;
|
||||
MutexFolder = /usr/safenet/lunaclient/lock;
|
||||
PE1746Enabled = 1;
|
||||
ToolsDir = /usr/bin;
|
||||
CardReader = {
|
||||
LunaG5Slots = 0;
|
||||
RemoteCommand = 1;
|
||||
}
|
||||
|
||||
}
|
||||
Misc = {
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
PluginModuleDir = /usr/safenet/lunaclient/plugins;
|
||||
MutexFolder = /usr/safenet/lunaclient/lock;
|
||||
PE1746Enabled = 1;
|
||||
ToolsDir = /usr/bin;
|
||||
|
||||
Presentation = {
|
||||
ShowEmptySlots = no;
|
||||
}
|
||||
}
|
||||
|
||||
LunaSA Client = {
|
||||
ReceiveTimeout = 20000;
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
|
||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||
ServerCAFile = ./etc/CAFile.pem;
|
||||
NetClient = 1;
|
||||
TCPKeepAlive = 1;
|
||||
}
|
||||
Presentation = {
|
||||
ShowEmptySlots = no;
|
||||
}
|
||||
|
||||
LunaSA Client = {
|
||||
ReceiveTimeout = 20000;
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
|
||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||
ServerCAFile = ./etc/CAFile.pem;
|
||||
NetClient = 1;
|
||||
TCPKeepAlive = 1;
|
||||
}
|
||||
|
||||
|
||||
REST = {
|
||||
AppLogLevel = error
|
||||
ServerName = <REDACTED>;
|
||||
ServerPort = 443;
|
||||
AuthTokenConfigURI = <REDACTED>;
|
||||
AuthTokenClientId = <REDACTED>;
|
||||
AuthTokenClientSecret = <REDACTED>;
|
||||
RestClient = 1;
|
||||
ClientTimeoutSec = 120;
|
||||
ClientPoolSize = 32;
|
||||
ClientEofRetryCount = 15;
|
||||
ClientConnectRetryCount = 900;
|
||||
ClientConnectIntervalMs = 1000;
|
||||
}
|
||||
XTC = {
|
||||
Enabled = 1;
|
||||
TimeoutSec = 600;
|
||||
}
|
||||
```
|
||||
REST = {
|
||||
AppLogLevel = error
|
||||
ServerName = <REDACTED>;
|
||||
ServerPort = 443;
|
||||
AuthTokenConfigURI = <REDACTED>;
|
||||
AuthTokenClientId = <REDACTED>;
|
||||
AuthTokenClientSecret = <REDACTED>;
|
||||
RestClient = 1;
|
||||
ClientTimeoutSec = 120;
|
||||
ClientPoolSize = 32;
|
||||
ClientEofRetryCount = 15;
|
||||
ClientConnectRetryCount = 900;
|
||||
ClientConnectIntervalMs = 1000;
|
||||
}
|
||||
XTC = {
|
||||
Enabled = 1;
|
||||
TimeoutSec = 600;
|
||||
}
|
||||
```
|
||||
|
||||
Save the file after updating the paths.
|
||||
</Step>
|
||||
Save the file after updating the paths.
|
||||
</Step>
|
||||
|
||||
<Step title="Run Docker">
|
||||
Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command.
|
||||
<Step title="Run Docker">
|
||||
Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command.
|
||||
|
||||
We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables.
|
||||
We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables.
|
||||
|
||||
<Warning>
|
||||
If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`.
|
||||
</Warning>
|
||||
<Warning>
|
||||
If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
docker run -p 80:8080 \
|
||||
-v /etc/luna-docker:/usr/safenet/lunaclient \
|
||||
-e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \
|
||||
-e HSM_PIN="<your-hsm-device-pin>" \
|
||||
-e HSM_SLOT=<hsm-device-slot> \
|
||||
-e HSM_KEY_LABEL="<your-key-label>" \
|
||||
|
||||
# The rest are unrelated to HSM setup...
|
||||
-e ENCRYPTION_KEY="<>" \
|
||||
-e AUTH_SECRET="<>" \
|
||||
-e DB_CONNECTION_URI="<>" \
|
||||
-e REDIS_URL="<>" \
|
||||
-e SITE_URL="<>" \
|
||||
infisical/infisical-fips:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
```bash
|
||||
docker run -p 80:8080 \
|
||||
-v /etc/luna-docker:/usr/safenet/lunaclient \
|
||||
-e HSM_LIB_PATH="/usr/safenet/lunaclient/libs/64/libCryptoki2.so" \
|
||||
-e HSM_PIN="<your-hsm-device-pin>" \
|
||||
-e HSM_SLOT=<hsm-device-slot> \
|
||||
-e HSM_KEY_LABEL="<your-key-label>" \
|
||||
|
||||
# The rest are unrelated to HSM setup...
|
||||
-e ENCRYPTION_KEY="<>" \
|
||||
-e AUTH_SECRET="<>" \
|
||||
-e DB_CONNECTION_URI="<>" \
|
||||
-e REDIS_URL="<>" \
|
||||
-e SITE_URL="<>" \
|
||||
infisical/infisical-fips:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
|
||||
We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical).
|
||||
We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Docker setup will be ready to use HSM encryption.
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Docker setup will be ready to use HSM encryption.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
When you are deploying Infisical with the [Kubernetes self-hosting option](/self-hosting/deployment-options/kubernetes-helm), you can still use HSM encryption, but you need to ensure that the HSM client files are present in the container.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Thales Luna Cloud HSM">
|
||||
<Note>
|
||||
This is only supported on helm chart version `1.4.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create HSM client folder">
|
||||
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes.
|
||||
|
||||
|
||||
```bash
|
||||
mkdir /etc/hsm-client
|
||||
```
|
||||
|
||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||
|
||||
A folder structure of a client folder will often look like this:
|
||||
```
|
||||
partition-ca-certificate.pem
|
||||
partition-certificate.pem
|
||||
server-certificate.pem
|
||||
Chrystoki.conf
|
||||
/plugins
|
||||
libcloud.plugin
|
||||
/lock
|
||||
/libs
|
||||
/64
|
||||
libCryptoki2.so
|
||||
/jsp
|
||||
LunaProvider.jar
|
||||
/64
|
||||
libLunaAPI.so
|
||||
/etc
|
||||
openssl.cnf
|
||||
/bin
|
||||
/64
|
||||
ckdemo
|
||||
lunacm
|
||||
multitoken
|
||||
vtl
|
||||
```
|
||||
|
||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||
|
||||
```bash
|
||||
cp -r /<path-to-where-your-hsm-client-is-located> /etc/hsm-client
|
||||
```
|
||||
</Step>
|
||||
<Step title="Update Chrystoki.conf">
|
||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||
|
||||
In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers.
|
||||
|
||||
An example config file will look like this:
|
||||
|
||||
```Chrystoki.conf
|
||||
Chrystoki2 = {
|
||||
# This path points to the mounted path, /hsm-client
|
||||
LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so;
|
||||
}
|
||||
|
||||
Luna = {
|
||||
DefaultTimeOut = 500000;
|
||||
PEDTimeout1 = 100000;
|
||||
PEDTimeout2 = 200000;
|
||||
PEDTimeout3 = 20000;
|
||||
KeypairGenTimeOut = 2700000;
|
||||
CloningCommandTimeOut = 300000;
|
||||
CommandTimeOutPedSet = 720000;
|
||||
}
|
||||
|
||||
CardReader = {
|
||||
LunaG5Slots = 0;
|
||||
RemoteCommand = 1;
|
||||
}
|
||||
|
||||
Misc = {
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
PluginModuleDir = /hsm-client/plugins;
|
||||
MutexFolder = /hsm-client/lock;
|
||||
PE1746Enabled = 1;
|
||||
ToolsDir = /usr/bin;
|
||||
|
||||
}
|
||||
|
||||
Presentation = {
|
||||
ShowEmptySlots = no;
|
||||
}
|
||||
|
||||
LunaSA Client = {
|
||||
ReceiveTimeout = 20000;
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
SSLConfigFile = /hsm-client/etc/openssl.cnf;
|
||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||
ServerCAFile = ./etc/CAFile.pem;
|
||||
NetClient = 1;
|
||||
TCPKeepAlive = 1;
|
||||
}
|
||||
|
||||
|
||||
REST = {
|
||||
AppLogLevel = error
|
||||
ServerName = <REDACTED>;
|
||||
ServerPort = 443;
|
||||
AuthTokenConfigURI = <REDACTED>;
|
||||
AuthTokenClientId = <REDACTED>;
|
||||
AuthTokenClientSecret = <REDACTED>;
|
||||
RestClient = 1;
|
||||
ClientTimeoutSec = 120;
|
||||
ClientPoolSize = 32;
|
||||
ClientEofRetryCount = 15;
|
||||
ClientConnectRetryCount = 900;
|
||||
ClientConnectIntervalMs = 1000;
|
||||
}
|
||||
XTC = {
|
||||
Enabled = 1;
|
||||
TimeoutSec = 600;
|
||||
}
|
||||
```
|
||||
|
||||
Save the file after updating the paths.
|
||||
</Step>
|
||||
|
||||
<Step title="Creating Persistent Volume Claim (PVC)">
|
||||
You need to create a Persistent Volume Claim (PVC) to mount the HSM client files to the Infisical deployment.
|
||||
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: infisical-data-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 500Mi
|
||||
EOF
|
||||
```
|
||||
The above command will create a PVC named `infisical-data-pvc` with a storage size of `500Mi`. You can change the storage size if needed.
|
||||
|
||||
|
||||
Next we need to create a temporary pod with the PVC mounted as a volume, allowing us to copy the HSM client files into this mounted storage.
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: hsm-setup-pod
|
||||
spec:
|
||||
containers:
|
||||
- name: setup
|
||||
image: busybox
|
||||
command: ["/bin/sh", "-c", "sleep 3600"]
|
||||
volumeMounts:
|
||||
- name: hsm-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: hsm-data
|
||||
persistentVolumeClaim:
|
||||
claimName: infisical-data-pvc
|
||||
EOF
|
||||
```
|
||||
|
||||
The above command will create a pod named `hsm-setup-pod` with a busybox image. The pod will sleep for 3600 seconds _(one hour)_, which is enough time to upload the HSM client files to the PVC.
|
||||
|
||||
Ensure that the pod is running and is healthy by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl wait --for=condition=Ready pod/hsm-setup-pod --timeout=60s
|
||||
```
|
||||
|
||||
Next we need to copy the HSM client files into the PVC.
|
||||
|
||||
```bash
|
||||
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
|
||||
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
|
||||
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
|
||||
```
|
||||
|
||||
Finally, we are ready to delete the temporary pod, as we have successfully uploaded the HSM client files to the PVC. This step may take a few minutes to complete.
|
||||
|
||||
```bash
|
||||
kubectl delete pod hsm-setup-pod
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Updating your environment variables">
|
||||
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
|
||||
We need to update the secret with the following environment variables:
|
||||
|
||||
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_
|
||||
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
|
||||
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
|
||||
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
|
||||
The following is an example of the secret that you should update:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: infisical-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# ... Other environment variables ...
|
||||
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
|
||||
HSM_PIN: "<your-hsm-device-pin>"
|
||||
HSM_SLOT: "<hsm-device-slot>"
|
||||
HSM_KEY_LABEL: "<your-key-label>"
|
||||
```
|
||||
|
||||
Save the file after updating the environment variables, and apply the secret changes
|
||||
|
||||
```bash
|
||||
kubectl apply -f ./secret-file-name.yaml
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Updating the Deployment">
|
||||
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
||||
|
||||
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption.
|
||||
|
||||
```yaml
|
||||
# ... The rest of the values.yaml file ...
|
||||
|
||||
image:
|
||||
repository: infisical/infisical-fips # Very important: Must use "infisical/infisical-fips"
|
||||
tag: "v0.117.1-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: hsm-data
|
||||
mountPath: /hsm-client # The path we will mount the HSM client files to
|
||||
subPath: ./hsm-client
|
||||
|
||||
extraVolumes:
|
||||
- name: hsm-data
|
||||
persistentVolumeClaim:
|
||||
claimName: infisical-data-pvc # The PVC we created in the previous step
|
||||
|
||||
# ... The rest of the values.yaml file ...
|
||||
```
|
||||
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Upgrading the Helm Chart">
|
||||
After updating the values.yaml file, you need to upgrade the Helm chart in order for the changes to take effect.
|
||||
|
||||
```bash
|
||||
helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml
|
||||
```
|
||||
</Step>
|
||||
<Step title="Restarting the Deployment">
|
||||
After upgrading the Helm chart, you need to restart the deployment in order for the changes to take effect.
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/infisical-infisical
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Kubernetes setup will be ready to use HSM encryption.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Disabling HSM Encryption
|
||||
|
||||
To disable HSM encryption, navigate to Infisical's Server Admin Console and set the KMS encryption strategy to `Software-based Encryption`. This will revert the encryption strategy back to the default software-based encryption.
|
||||
|
BIN
docs/images/platform/access-controls/abac-policies-by-auth.png
Normal file
BIN
docs/images/platform/access-controls/abac-policies-by-auth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 478 KiB |
BIN
docs/images/platform/access-controls/abac-policy-oidc-format.png
Normal file
BIN
docs/images/platform/access-controls/abac-policy-oidc-format.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 KiB |
@ -43,8 +43,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
|
||||
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
|
||||
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
|
||||
<Note>Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.</Note>
|
||||
<Note>
|
||||
Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||

|
||||
|
@ -46,7 +46,11 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
|
||||
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
|
||||
- **Tags**: Optional tags to add to secrets synced by Infisical.
|
||||
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
|
||||
<Note>
|
||||
Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.
|
||||

|
||||
|
@ -6,7 +6,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration.
|
||||
- Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
|
||||
|
||||
<Note>
|
||||
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
|
||||
@ -50,6 +50,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
|
||||
- **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**.
|
||||

|
||||
|
@ -6,7 +6,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
|
||||
**Prerequisites:**
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault.
|
||||
- Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
|
||||
|
||||
<Note>
|
||||
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
|
||||
@ -52,6 +52,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
|
||||
- **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**.
|
||||

|
||||
|
@ -47,6 +47,7 @@ description: "Learn how to configure a Databricks Sync for Infisical."
|
||||
Databricks does not support importing secrets.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Databricks Sync, then click **Next**.
|
||||

|
||||
|
@ -43,6 +43,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
|
||||
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
|
||||

|
||||
|
@ -63,6 +63,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
|
||||
GitHub does not support importing secrets.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your GitHub Sync, then click **Next**.
|
||||

|
||||
|
@ -56,6 +56,7 @@ description: "Learn how to configure a Humanitec Sync for Infisical."
|
||||
Humanitec does not support importing secrets.
|
||||
</Note>
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
6. Configure the **Details** of your Humanitec Sync, then click **Next**.
|
||||

|
||||
|
@ -103,7 +103,7 @@ With standby regions and automated failovers in place, Infisical Cloud faces min
|
||||
|
||||
Infisical hires external third parties to perform regular security assessment and penetration testing of the platform.
|
||||
|
||||
Most recently, Infisical commissioned cybersecurity firm [Oneleet](https://www.oneleet.com) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
|
||||
Most recently, Infisical commissioned cybersecurity firm [Cure53](https://cure53.de/) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
|
||||
|
||||
Please email security@infisical.com to request any reports including a letter of attestation for the conducted penetration test.
|
||||
|
||||
|
@ -150,7 +150,14 @@
|
||||
"pages": [
|
||||
"documentation/platform/access-controls/overview",
|
||||
"documentation/platform/access-controls/role-based-access-controls",
|
||||
"documentation/platform/access-controls/attribute-based-access-controls",
|
||||
{
|
||||
"group": "Attribute based access controls",
|
||||
"pages": [
|
||||
"documentation/platform/access-controls/abac/overview",
|
||||
"documentation/platform/access-controls/abac/managing-user-metadata",
|
||||
"documentation/platform/access-controls/abac/managing-machine-identity-attributes"
|
||||
]
|
||||
},
|
||||
"documentation/platform/access-controls/additional-privileges",
|
||||
"documentation/platform/access-controls/temporary-access",
|
||||
"documentation/platform/access-controls/access-requests",
|
||||
|
10
flake.nix
10
flake.nix
@ -14,21 +14,11 @@
|
||||
git
|
||||
lazygit
|
||||
|
||||
go
|
||||
python312Full
|
||||
nodejs_20
|
||||
nodePackages.prettier
|
||||
infisical
|
||||
];
|
||||
|
||||
env = {
|
||||
GOROOT = "${pkgs.go}/share/go";
|
||||
};
|
||||
|
||||
shellHook = ''
|
||||
export GOPATH="$(pwd)/.go"
|
||||
mkdir -p "$GOPATH"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
@ -116,6 +116,44 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
</>
|
||||
)}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.disableSecretDeletion"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[11rem]">
|
||||
Disable Secret Deletion{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When enabled, Infisical will <span className="font-semibold">not</span>{" "}
|
||||
remove secrets from the destination during a sync.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Enable this option if you intend to manage some secrets manually outside
|
||||
of Infisical.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* <Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
|
@ -36,6 +36,7 @@ export const SecretSyncReviewFields = () => {
|
||||
secretPath,
|
||||
syncOptions: {
|
||||
// appendSuffix, prependPrefix,
|
||||
disableSecretDeletion,
|
||||
initialSyncBehavior
|
||||
},
|
||||
destination,
|
||||
@ -111,6 +112,11 @@ export const SecretSyncReviewFields = () => {
|
||||
{/* <SecretSyncLabel label="Prepend Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Append Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsFieldsComponent}
|
||||
{disableSecretDeletion && (
|
||||
<SecretSyncLabel label="Secret Deletion">
|
||||
<Badge variant="primary">Disabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
@ -7,7 +7,8 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
|
||||
additionalSyncOptions?: T
|
||||
) => {
|
||||
const baseSyncOptionsSchema = z.object({
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
|
||||
disableSecretDeletion: z.boolean().optional().default(false)
|
||||
// scott: removed temporarily for evaluation of template formatting
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
|
@ -462,6 +462,7 @@ export const useUpdateIdentityOidcAuth = () => {
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject
|
||||
}) => {
|
||||
const {
|
||||
@ -478,7 +479,8 @@ export const useUpdateIdentityOidcAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
claimMetadataMapping
|
||||
}
|
||||
);
|
||||
|
||||
@ -504,6 +506,7 @@ export const useAddIdentityOidcAuth = () => {
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@ -524,7 +527,8 @@ export const useAddIdentityOidcAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
claimMetadataMapping
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -194,6 +194,7 @@ export type IdentityOidcAuth = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -209,6 +210,7 @@ export type AddIdentityOidcAuthDTO = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -226,6 +228,7 @@ export type UpdateIdentityOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
@ -3,6 +3,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/
|
||||
|
||||
export type RootSyncOptions = {
|
||||
initialSyncBehavior: SecretSyncInitialSyncBehavior;
|
||||
disableSecretDeletion?: boolean;
|
||||
// prependPrefix?: string;
|
||||
// appendSuffix?: string;
|
||||
};
|
||||
|
@ -46,12 +46,22 @@ const schema = z.object({
|
||||
caCert: z.string().trim().default(""),
|
||||
boundIssuer: z.string().min(1),
|
||||
boundAudiences: z.string().optional().default(""),
|
||||
boundClaims: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
),
|
||||
boundClaims: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
claimMetadataMapping: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
boundSubject: z.string().optional().default("")
|
||||
});
|
||||
|
||||
@ -96,10 +106,11 @@ export const IdentityOidcAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
boundClaims: [],
|
||||
claimMetadataMapping: []
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: boundClaimsFields,
|
||||
append: appendBoundClaimField,
|
||||
@ -109,6 +120,15 @@ export const IdentityOidcAuthForm = ({
|
||||
name: "boundClaims"
|
||||
});
|
||||
|
||||
const {
|
||||
fields: claimMetadataMappingFields,
|
||||
append: appendClaimMetadataMappingField,
|
||||
remove: removeClaimMetadataMappingField
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: "claimMetadataMapping"
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
@ -126,6 +146,12 @@ export const IdentityOidcAuthForm = ({
|
||||
key,
|
||||
value
|
||||
})),
|
||||
claimMetadataMapping: data?.claimMetadataMapping
|
||||
? Object.entries(data.claimMetadataMapping).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
}))
|
||||
: undefined,
|
||||
boundSubject: data.boundSubject,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
@ -149,7 +175,8 @@ export const IdentityOidcAuthForm = ({
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
claimMetadataMapping: []
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@ -164,6 +191,7 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject
|
||||
}: FormData) => {
|
||||
try {
|
||||
@ -180,6 +208,9 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
claimMetadataMapping: claimMetadataMapping
|
||||
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
|
||||
: undefined,
|
||||
boundSubject,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
@ -194,6 +225,9 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
claimMetadataMapping: claimMetadataMapping
|
||||
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
|
||||
: undefined,
|
||||
boundSubject,
|
||||
organizationId: orgId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
@ -223,7 +257,9 @@ export const IdentityOidcAuthForm = ({
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
|
||||
["accessTokenTrustedIps", "caCert", "claimMetadataMapping"].includes(
|
||||
Object.keys(fields)[0]
|
||||
)
|
||||
? IdentityFormTab.Advanced
|
||||
: IdentityFormTab.Configuration
|
||||
);
|
||||
@ -313,63 +349,6 @@ export const IdentityOidcAuthForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
@ -449,6 +428,155 @@ export const IdentityOidcAuthForm = ({
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{claimMetadataMappingFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`claimMetadataMapping.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Token Claim Mapping" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<div className="w-[180px]">
|
||||
<p>Map OIDC token claims to metadata fields</p>
|
||||
<p className="mt-2 text-sm">Example:</p>
|
||||
<p className="mt-1 text-sm">
|
||||
'role' → 'token.groups'
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Becomes: identity.metadata.oidc.claims.role
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="Field name"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`claimMetadataMapping.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="Token claim"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => removeClaimMetadataMappingField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendClaimMetadataMappingField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Token Mapping
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
@ -519,24 +647,21 @@ export const IdentityOidcAuthForm = ({
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
variant="outline_bg"
|
||||
className="mr-4"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
{isUpdate ? "Update" : "Add"} Auth Method
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -111,6 +111,26 @@ export const ViewIdentityOidcAuthContent = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Claim Metadata Mapping">
|
||||
{data.claimMetadataMapping && Object.keys(data.claimMetadataMapping).length && (
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={
|
||||
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
|
||||
{JSON.stringify(data.claimMetadataMapping, null, 2)}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
@ -49,8 +50,10 @@ import {
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
@ -71,6 +74,7 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||
|
||||
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
@ -131,6 +135,7 @@ export const OverviewPage = () => {
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
|
||||
const secretPath = (routerSearch?.secretPath as string) || "/";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||
const [filterHistory, setFilterHistory] = useState<
|
||||
@ -178,6 +183,15 @@ export const OverviewPage = () => {
|
||||
}, []);
|
||||
|
||||
const userAvailableEnvs = currentWorkspace?.environments || [];
|
||||
const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) =>
|
||||
permission.can(
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: env.slug,
|
||||
secretPath
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
|
||||
|
||||
@ -249,7 +263,9 @@ export const OverviewPage = () => {
|
||||
"addSecretsInAllEnvs",
|
||||
"addFolder",
|
||||
"misc",
|
||||
"updateFolder"
|
||||
"updateFolder",
|
||||
"addDynamicSecret",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
@ -851,20 +867,43 @@ export const OverviewPage = () => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} className="pr-2" />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addFolder");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Tooltip
|
||||
content={
|
||||
userAvailableDynamicSecretEnvs.length === 0 ? "Access restricted" : ""
|
||||
}
|
||||
>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFingerprint} className="pr-2" />}
|
||||
onClick={() => {
|
||||
if (subscription?.dynamicSecret) {
|
||||
handlePopUpOpen("addDynamicSecret");
|
||||
handlePopUpClose("misc");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
isDisabled={userAvailableDynamicSecretEnvs.length === 0}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add Dynamic Secret
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -1170,6 +1209,24 @@ export const OverviewPage = () => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<CreateDynamicSecretForm
|
||||
isOpen={popUp.addDynamicSecret.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
|
||||
projectSlug={projectSlug}
|
||||
environments={userAvailableDynamicSecretEnvs}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
{subscription && (
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={
|
||||
subscription.slug === null
|
||||
? "You can perform this action under an Enterprise license"
|
||||
: "You can perform this action if you switch to Infisical's Team plan"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -110,6 +110,7 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
|
||||
const secretsToDelete = Object.values(selectedEntries.secret).reduce(
|
||||
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
|
||||
const entry = secretRecord[env.slug];
|
||||
if (!entry) return accum;
|
||||
const canDeleteSecret = permission.can(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
|
@ -655,8 +655,9 @@ export const ActionBar = ({
|
||||
isOpen={popUp.addDynamicSecret.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
|
||||
projectSlug={projectSlug}
|
||||
environment={environment}
|
||||
environments={[{ slug: environment, name: environment, id: "not-used" }]}
|
||||
secretPath={secretPath}
|
||||
isSingleEnvironmentMode
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp.addFolder.isOpen}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -50,7 +52,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -59,15 +62,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const AwsElastiCacheInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -87,13 +92,20 @@ export const AwsElastiCacheInputForm = ({
|
||||
revocationStatement: `{
|
||||
"UserId": "{{username}}"
|
||||
}`
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -104,7 +116,7 @@ export const AwsElastiCacheInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -299,6 +311,28 @@ export const AwsElastiCacheInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,9 +5,10 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -40,7 +41,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -49,29 +51,41 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const AwsIamInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema)
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
|
||||
@ -80,7 +94,7 @@ export const AwsIamInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -286,6 +300,29 @@ export const AwsIamInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -23,6 +23,7 @@ import { Tooltip } from "@app/components/v2/Tooltip";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
selectedUsers: z.array(
|
||||
@ -60,7 +61,8 @@ const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -69,15 +71,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const AzureEntraIdInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -85,7 +89,10 @@ export const AzureEntraIdInputForm = ({
|
||||
watch,
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema)
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
const tenantId = watch("provider.tenantId");
|
||||
const applicationId = watch("provider.applicationId");
|
||||
@ -107,7 +114,8 @@ export const AzureEntraIdInputForm = ({
|
||||
selectedUsers,
|
||||
provider,
|
||||
maxTTL,
|
||||
defaultTTL
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
@ -129,7 +137,7 @@ export const AzureEntraIdInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
});
|
||||
onCompleted();
|
||||
@ -373,6 +381,29 @@ export const AzureEntraIdInputForm = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -52,7 +54,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -61,7 +64,8 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
const getSqlStatements = () => {
|
||||
@ -76,9 +80,10 @@ const getSqlStatements = () => {
|
||||
export const CassandraInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -87,15 +92,23 @@ export const CassandraInputForm = ({
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: getSqlStatements()
|
||||
provider: getSqlStatements(),
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Cassandra, inputs: provider },
|
||||
@ -104,7 +117,7 @@ export const CassandraInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -345,6 +358,29 @@ export const CassandraInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
||||
import { AwsIamInputForm } from "./AwsIamInputForm";
|
||||
@ -38,8 +39,9 @@ type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
secretPath: string;
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
enum WizardSteps {
|
||||
@ -129,8 +131,9 @@ export const CreateDynamicSecretForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
projectSlug,
|
||||
environment,
|
||||
secretPath
|
||||
environments,
|
||||
secretPath,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
|
||||
const [selectedProvider, setSelectedProvider] = useState<DynamicSecretProviders | null>(null);
|
||||
@ -197,7 +200,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -215,7 +219,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -233,7 +238,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -251,7 +257,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -269,7 +276,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -287,7 +295,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -305,7 +314,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -323,7 +333,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -341,7 +352,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -359,7 +371,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -377,7 +390,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -395,7 +409,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -413,7 +428,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -432,7 +448,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -450,7 +467,8 @@ export const CreateDynamicSecretForm = ({
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
@ -9,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
@ -73,7 +75,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -82,15 +85,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const ElasticSearchInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -107,13 +112,20 @@ export const ElasticSearchInputForm = ({
|
||||
},
|
||||
roles: ["superuser"],
|
||||
port: 443
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -124,7 +136,7 @@ export const ElasticSearchInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -406,6 +418,29 @@ export const ElasticSearchInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,9 +7,18 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
@ -69,7 +78,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
@ -79,7 +89,8 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const LdapInputForm = ({
|
||||
@ -87,7 +98,8 @@ export const LdapInputForm = ({
|
||||
onCancel,
|
||||
secretPath,
|
||||
projectSlug,
|
||||
environment
|
||||
environments,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -107,7 +119,8 @@ export const LdapInputForm = ({
|
||||
revocationLdif: "",
|
||||
rollbackLdif: "",
|
||||
credentialType: CredentialType.Dynamic
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -115,7 +128,13 @@ export const LdapInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -126,7 +145,7 @@ export const LdapInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -369,6 +388,29 @@ export const LdapInputForm = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
@ -22,6 +23,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -63,7 +65,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -72,7 +75,8 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
const ATLAS_SCOPE_TYPES = [
|
||||
@ -93,9 +97,10 @@ const ATLAS_SCOPE_TYPES = [
|
||||
export const MongoAtlasInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -108,7 +113,8 @@ export const MongoAtlasInputForm = ({
|
||||
defaultValues: {
|
||||
provider: {
|
||||
roles: [{ databaseName: "", roleName: "" }]
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -124,7 +130,13 @@ export const MongoAtlasInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -135,7 +147,7 @@ export const MongoAtlasInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -438,6 +450,29 @@ export const MongoAtlasInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,9 +7,18 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -46,7 +55,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -55,15 +65,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const MongoDBDatabaseInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -76,7 +88,8 @@ export const MongoDBDatabaseInputForm = ({
|
||||
defaultValues: {
|
||||
provider: {
|
||||
roles: [{ roleName: "readWrite" }]
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -87,7 +100,13 @@ export const MongoDBDatabaseInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -105,7 +124,7 @@ export const MongoDBDatabaseInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -321,6 +340,29 @@ export const MongoDBDatabaseInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,9 +7,18 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -50,7 +59,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -59,15 +69,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const RabbitMqInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -89,13 +101,20 @@ export const RabbitMqInputForm = ({
|
||||
}
|
||||
},
|
||||
tags: []
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -106,7 +125,7 @@ export const RabbitMqInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -405,6 +424,29 @@ export const RabbitMqInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -50,7 +52,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -59,15 +62,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const RedisInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -81,13 +86,20 @@ export const RedisInputForm = ({
|
||||
port: 6379,
|
||||
creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all",
|
||||
revocationStatement: "ACL DELUSER {{username}}"
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -98,7 +110,7 @@ export const RedisInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -313,6 +325,29 @@ export const RedisInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,12 +11,14 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -48,7 +50,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -57,15 +60,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const SapAseInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -82,13 +87,20 @@ sp_adduser '{{username}}', '{{username}}', null;
|
||||
sp_role 'grant', 'mon_role', '{{username}}';`,
|
||||
revocationStatement: `sp_dropuser '{{username}}';
|
||||
sp_droplogin '{{username}}';`
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -99,7 +111,7 @@ sp_droplogin '{{username}}';`
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -291,6 +303,29 @@ sp_droplogin '{{username}}';`
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -50,7 +52,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -59,15 +62,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const SapHanaInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -82,13 +87,20 @@ GRANT "MONITORING" TO {{username}};`,
|
||||
revocationStatement: `REVOKE "MONITORING" FROM {{username}};
|
||||
DROP USER {{username}};`,
|
||||
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -99,7 +111,7 @@ DROP USER {{username}};`,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -311,6 +323,29 @@ DROP USER {{username}};`,
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,12 +13,14 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
@ -54,7 +56,8 @@ const formSchema = z.object({
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -63,15 +66,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const SnowflakeInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -85,13 +90,20 @@ export const SnowflakeInputForm = ({
|
||||
"CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};",
|
||||
revocationStatement: "DROP USER {{username}};",
|
||||
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -102,7 +114,7 @@ export const SnowflakeInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
@ -314,6 +326,29 @@ export const SnowflakeInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
@ -22,6 +23,7 @@ import {
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
const passwordRequirementsSchema = z
|
||||
.object({
|
||||
@ -79,7 +81,8 @@ const formSchema = z.object({
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@ -88,7 +91,8 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
const getSqlStatements = (provider: SqlProviders) => {
|
||||
@ -145,9 +149,10 @@ const getDefaultPort = (provider: SqlProviders) => {
|
||||
export const SqlDatabaseInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
@ -172,7 +177,8 @@ export const SqlDatabaseInputForm = ({
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
}
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -181,9 +187,16 @@ export const SqlDatabaseInputForm = ({
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
);
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
provider,
|
||||
defaultTTL,
|
||||
environment
|
||||
}: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
|
||||
@ -192,7 +205,7 @@ export const SqlDatabaseInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch {
|
||||
@ -649,6 +662,29 @@ export const SqlDatabaseInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,9 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
|
||||
enum ConfigType {
|
||||
URL = "url",
|
||||
@ -52,7 +60,8 @@ const formSchema = z.object({
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||
environment: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
@ -62,15 +71,17 @@ type Props = {
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
environments: WorkspaceEnv[];
|
||||
isSingleEnvironmentMode?: boolean;
|
||||
};
|
||||
|
||||
export const TotpInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
environments,
|
||||
secretPath,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
@ -82,7 +93,8 @@ export const TotpInputForm = ({
|
||||
defaultValues: {
|
||||
provider: {
|
||||
configType: ConfigType.URL
|
||||
}
|
||||
},
|
||||
environment: isSingleEnvironmentMode ? environments[0] : undefined
|
||||
}
|
||||
});
|
||||
|
||||
@ -90,7 +102,7 @@ export const TotpInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
|
||||
const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isPending) return;
|
||||
try {
|
||||
@ -101,7 +113,7 @@ export const TotpInputForm = ({
|
||||
path: secretPath,
|
||||
defaultTTL: "1m",
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
environmentSlug: environment.slug
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
@ -295,6 +307,29 @@ export const TotpInputForm = ({
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{!isSingleEnvironmentMode && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select the environment to create secret in..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -306,7 +306,10 @@ export const SecretItem = memo(
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div key="actions" className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2">
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretSyncLabel } from "@app/components/secret-syncs";
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { Badge, IconButton } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
|
||||
@ -24,7 +24,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
syncOptions: {
|
||||
// appendSuffix,
|
||||
// prependPrefix,
|
||||
initialSyncBehavior
|
||||
initialSyncBehavior,
|
||||
disableSecretDeletion
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
@ -58,24 +59,22 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||
{AdditionalSyncOptionsComponent && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit sync options"
|
||||
onClick={onEditOptions}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit sync options"
|
||||
onClick={onEditOptions}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
@ -85,6 +84,11 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
{/* <SecretSyncLabel label="Prefix">{prependPrefix}</SecretSyncLabel>
|
||||
<SecretSyncLabel label="Suffix">{appendSuffix}</SecretSyncLabel> */}
|
||||
{AdditionalSyncOptionsComponent}
|
||||
{disableSecretDeletion && (
|
||||
<SecretSyncLabel label="Secret Deletion">
|
||||
<Badge variant="primary">Disabled</Badge>
|
||||
</SecretSyncLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,8 @@
|
||||
## 1.4.1 (March 19, 2025)
|
||||
|
||||
Changes:
|
||||
* Added support for supplying extra volume mounts and volumes via `infisical.extraVolumeMounts` and `infisical.extraVolumes`
|
||||
|
||||
## 1.4.0 (November 06, 2024)
|
||||
|
||||
Changes:
|
||||
|
@ -7,7 +7,7 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.4.0
|
||||
version: 1.4.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
@ -77,6 +77,14 @@ spec:
|
||||
{{- if $infisicalValues.resources }}
|
||||
resources: {{- toYaml $infisicalValues.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $infisicalValues.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- with $infisicalValues.extraVolumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
|
@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.8.14
|
||||
version: v0.8.15
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.8.14"
|
||||
appVersion: "v0.8.15"
|
||||
|
@ -417,7 +417,6 @@ spec:
|
||||
- secretNamespace
|
||||
type: object
|
||||
required:
|
||||
- managedKubeConfigMapReferences
|
||||
- resyncInterval
|
||||
type: object
|
||||
status:
|
||||
|
@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.8.14
|
||||
tag: v0.8.15
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
87
sink/oidc-server/main.js
Normal file
87
sink/oidc-server/main.js
Normal file
@ -0,0 +1,87 @@
|
||||
import Provider from "oidc-provider";
|
||||
import express from "express";
|
||||
|
||||
const configuration = {
|
||||
jwks: {
|
||||
keys: [
|
||||
{
|
||||
kty: "RSA",
|
||||
use: "sig",
|
||||
alg: "RS256",
|
||||
d: "EF2Kky61jzvMYQ_B6ImXzCsQ8uQzbFJrGnB2azlpr_CFStjjUVKP4EKrSCVEasD6SGNJV2QSiNJr7j05nvuGmHMKa__rbU8fqP4qbDahUgCgWOq-zS5tGK6Ifk4II_cZ_V1F-TnrvmcOKMWBiSV-p8i72KpXXucbHGNRwASVs7--M55wp_m1UsybI2jSQ4IgyvGzTnvMmQ_GsX-XoD8u0zGU_4eN3DGc8l6hdxxuSymH0fEeL1Aj0LoCj6teRGF37a2sBQdU6mkNNAuyyirkoDqGZCGJToQLqX4F1FafnzjeIgfdneRa-vuaV380Hhr2rorWnQyBqOO27M5O_VAkJbfRaWJVrXTJ69ZgkU4GPdeYdklVL0HkU6laziTNqNMeAjnt4m51sWokVyJpvdWcb_vJ4NSCsRo7kHOz7g-UvWTXa8UW0DTDliq_TJ3rN4Gv0vn9tBlFfaeuLPpK4VNmRRDRXY_fcuzlnQwYExL9a4V_vCyGmabdb7PrUFPBcjR5",
|
||||
dp: "SX52TkZEc_eLIk5gYrKjAC643LJIw1RxMBWWewRSGLn_rbrH1he3hy7AGDUV6Uon7zkNh9R5GBVuxmlluBRAGbrhIXAAf8sWeyma3F6FIAt-MH_VkfW5K2p88PLOyVGljlv8-Z3wzdKYOlDP4yFU18LqGMqaRSDLDGhILkuZhjLYA40sfYJeJTi_HVP5UyWL4ohayqUWCT2W3DgeDDThYHmufOaqlrSLhUst6uez_cDz0BXAYIZvUuPVL_n1-_px",
|
||||
dq: "K1KYU77I6yyPA2u32rc0exp_TCG59hhpWxrmXN8yTXWyq_xYBhCJA_nHdY8UV25Hmd7q0iX2i8y2cCAFNWA5UWiSiNg9-fKRLI2nz53IM4dGfssOLwUk66wzX8r_u3XiLZsO7XNNtQZdcZmF0YuNTtzEdiNDhaOyHiwwHgShL36WNmUn00mZR__G5Qk60VvI8vsbvJU9xRnWuEVS1wRgyD7v6Nl9nIxb8N7oibCdTJLmgnRXPWvArsW0cJ-NURfr",
|
||||
e: "AQAB",
|
||||
n: "2QwX-NBMkQYedGpbPvHL7Ca0isvfmLC7lSc8XSOCLmCUIf6Bk_pdCNx2kxsmT81IoA8CfvJLHQj5vWKoVDFMLfwo4IujvsC3m2IrEg6jERE-YHfC3W5jKZtmzQYpfx5vC2_XTmcyPigtyaNVsftGfycES3B_tvphNsFmQcJjVGOsJQXXqh_TDv6FMcH4m9pngyw6wfe3GgAKA0dRTSfD0h7wLdNCeuid53lLpkQypTNdZ6_PiCMu2gr_cH5M0MPZtBb2TW12_2zOabExK1lI5-HvdPtbMT4Qzs2nd2NkjcWmlbKRZzq6IzyWt7W2EnfZDsi61PHECtTb-EQN2icl8Wnsp-0Bw66yviAOj0gn3X5hRLx-TknT_PnWMou17l5GoAojKDezcTW0iLlrfs2ixFlY28u7WklUN8uYhHvwgON6fsdefG-3bPpiRLBPZ_tgXa4doALsCwfXu2oz0vYktk31A-UYv92uJsKSUbK0_8ODTN0rslCqCYN_1a_aVt2P",
|
||||
p: "--L5BX8juLlGJk8hdPgEUmJjD7SsZuMrdq3cSibkkbaWUE5CQQ7vhLPr2dWCS1jUnY9WyoCx9QCZvhTHjORX50ykkOyBso9VJjWvYPjsrPpF7_Y6V0dKlblDmbbmRT9BW-MgjbwTivu3c2OpMXh2XLF-FOTq3t3Brs7SRnhTkD6GBDFf3X95J0PF7NELa9z2-kzPSDYz3k-9FepXnRPBM_ViDzlRw4eKUdylVuhzGbC2TRSmab9BRP0wipQKd-f5",
|
||||
q: "3Jd5CRJpQV3xUi3FiHHAwcjfsRkfXMrxfaXt0PjX2xWzxscYiDcyCF6VhHTAGsiq5SOtCp3l5mg6A9PzdR53AzM2-706D82fMwiUZvsLOVTepXkgriP_xw7rDlkOeAvjB80sL2G9scFliTzzRZ8I8E79A8DxZihfB75AIN9ijklEihnwxfhp2EgO5MYEyQRcqU1TT8wD8ekLMzd-kJUWyTz3BogiVJH__BQoB6kaDyjvQoxBgwh0hi72t9H5XqPH",
|
||||
qi: "cwK0jhzwbu8BaTmTQhwfGiqwNN3v9F4nUQ4dtnBYRI6zlki4cLb2Mf9-VhyEsUYhhdTm8R7RwO9m5Xct3gEfozdk35wuvkVwkZgL3Uho5asao0xi4aENeUk5DCkU-paO3yLSDhIs9YYuYIDjUX6QuMCPjomypuE3SRm-Dg1PGOxYvX3w_P-0kd5iBFrm4jwGTZViFOr8tl_dXgDRDWDgofOYOYcmUv2_0zt1aO3j5dhEpwdkyuDMLfVZNpJQyopJ",
|
||||
kid: "f262a3214213d194c92991d6735b153b",
|
||||
},
|
||||
],
|
||||
},
|
||||
features: {
|
||||
clientCredentials: {
|
||||
enabled: true,
|
||||
},
|
||||
introspection: {
|
||||
enabled: true,
|
||||
},
|
||||
resourceIndicators: {
|
||||
enabled: true,
|
||||
getResourceServerInfo(ctx, resourceIndicator) {
|
||||
if (resourceIndicator === "urn:api") {
|
||||
return {
|
||||
scope: "read",
|
||||
audience: "urn:api",
|
||||
accessTokenTTL: 1 * 60 * 60, // 1 hour
|
||||
accessTokenFormat: "jwt",
|
||||
};
|
||||
}
|
||||
|
||||
throw new errors.InvalidTarget();
|
||||
},
|
||||
},
|
||||
},
|
||||
clients: [
|
||||
{
|
||||
client_id: "app",
|
||||
client_secret: "a_secret",
|
||||
grant_types: ["client_credentials"],
|
||||
redirect_uris: [],
|
||||
response_types: [],
|
||||
},
|
||||
{
|
||||
client_id: "oidc_client",
|
||||
client_secret: "a_different_secret",
|
||||
grant_types: ["authorization_code"],
|
||||
response_types: ["code"],
|
||||
redirect_uris: ["http://localhost:3001/cb"],
|
||||
},
|
||||
],
|
||||
claims: {
|
||||
profile: [
|
||||
"birthdate",
|
||||
"family_name",
|
||||
"gender",
|
||||
"given_name",
|
||||
"locale",
|
||||
"middle_name",
|
||||
"name",
|
||||
"nickname",
|
||||
"picture",
|
||||
"preferred_username",
|
||||
"profile",
|
||||
"updated_at",
|
||||
"website",
|
||||
"zoneinfo",
|
||||
],
|
||||
email: ["email", "email_verified"],
|
||||
},
|
||||
};
|
||||
|
||||
const oidc = new Provider("http://localhost:3000", configuration);
|
||||
|
||||
const app = express();
|
||||
app.use("/oidc", oidc.callback());
|
||||
app.listen(3000);
|
1708
sink/oidc-server/package-lock.json
generated
Normal file
1708
sink/oidc-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
sink/oidc-server/package.json
Normal file
21
sink/oidc-server/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "oidc-server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node main.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
"jose": "^6.0.10",
|
||||
"oidc-provider": "^8.8.1"
|
||||
}
|
||||
}
|
80
sink/oidc-server/test-infisical.js
Normal file
80
sink/oidc-server/test-infisical.js
Normal file
@ -0,0 +1,80 @@
|
||||
import axios from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import querystring from "querystring";
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
issuer: "http://localhost:3000/oidc",
|
||||
tokenEndpoint: "http://localhost:3000/oidc/token",
|
||||
clientId: "app",
|
||||
clientSecret: "a_secret",
|
||||
};
|
||||
|
||||
// Client credentials flow for machine identity
|
||||
async function getMachineToken() {
|
||||
try {
|
||||
// Use application/x-www-form-urlencoded format as required by the OIDC spec
|
||||
const data = querystring.stringify({
|
||||
grant_type: "client_credentials",
|
||||
scope: "read",
|
||||
resource: "urn:api",
|
||||
});
|
||||
|
||||
const authHeader =
|
||||
"Basic " +
|
||||
Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
|
||||
"base64",
|
||||
);
|
||||
|
||||
const response = await axios.post(config.tokenEndpoint, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: authHeader,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Successfully obtained token:");
|
||||
console.log("Access Token:", response.data.access_token);
|
||||
console.log("Token Type:", response.data.token_type);
|
||||
console.log("Expires In:", response.data.expires_in, "seconds");
|
||||
console.log("Scope:", response.data.scope);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error obtaining token:");
|
||||
if (error.response && error.response.data) {
|
||||
console.error(error.response.data);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the machine identity authentication
|
||||
async function testMachineIdentity() {
|
||||
try {
|
||||
// Get token using client credentials
|
||||
const token = await getMachineToken();
|
||||
|
||||
const loginData = querystring.stringify({
|
||||
identityId: "5d81d5cc-602f-4af7-b242-ab7c1331b430",
|
||||
jwt: token.access_token,
|
||||
});
|
||||
|
||||
const response = await axios({
|
||||
method: "post",
|
||||
url: `http://localhost:8080/api/v1/auth/oidc-auth/login`,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: loginData,
|
||||
});
|
||||
console.log(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error in test:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testMachineIdentity();
|
Reference in New Issue
Block a user