mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-13 16:52:12 +00:00
Compare commits
41 Commits
address-sa
...
minor-chan
Author | SHA1 | Date | |
---|---|---|---|
36144d8c42 | |||
c487b2b34a | |||
8e20531b40 | |||
8ead2aa774 | |||
1b2128e3cc | |||
78f83cb478 | |||
c8a871de7c | |||
64c0951df3 | |||
c185414a3c | |||
f9695741f1 | |||
a7fe79c046 | |||
9eb89bb46d | |||
c4da1ce32d | |||
add97c9b38 | |||
768ba4f4dc | |||
18c32d872c | |||
1fd40ab6ab | |||
9d258f57ce | |||
45ccbaf4c9 | |||
8de7261c9a | |||
67b1b79fe3 | |||
31477f4d2b | |||
f200372d74 | |||
f955b68519 | |||
9269b63943 | |||
8f96653273 | |||
126b0ce7e7 | |||
e53439d586 | |||
3d6da1e548 | |||
7e46fe8148 | |||
3756a1901d | |||
9c8adf75ec | |||
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;
|
||||
|
@ -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.",
|
||||
|
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";
|
||||
@ -137,6 +138,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
identityName: identity.name,
|
||||
authMethod: null
|
||||
};
|
||||
if (token?.identityAuth?.oidc) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
oidc: token?.identityAuth?.oidc
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
@ -23,6 +23,7 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
|
||||
boundIssuer: true,
|
||||
boundAudiences: true,
|
||||
boundClaims: true,
|
||||
claimMetadataMapping: true,
|
||||
boundSubject: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
@ -104,6 +105,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({
|
||||
@ -161,6 +163,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,
|
||||
@ -200,6 +203,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({
|
||||
@ -258,6 +262,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";
|
||||
@ -177,8 +178,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`
|
||||
});
|
||||
|
@ -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";
|
||||
@ -77,7 +78,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;
|
||||
@ -91,7 +92,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const client = new JwksClient({
|
||||
jwksUri,
|
||||
requestAgent
|
||||
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
});
|
||||
|
||||
const { kid } = decodedToken.header;
|
||||
@ -108,7 +109,6 @@ export const identityOidcAuthServiceFactory = ({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -135,10 +135,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."
|
||||
});
|
||||
@ -146,6 +152,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(
|
||||
{
|
||||
@ -167,7 +187,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
|
||||
@ -188,6 +213,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@ -254,6 +280,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
@ -274,6 +301,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@ -335,6 +363,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;
|
||||
@ -21,6 +22,7 @@ export type TUpdateOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
@ -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({
|
||||
|
@ -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 {
|
||||
|
@ -232,6 +232,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 == "" {
|
||||
|
@ -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) {
|
||||
|
@ -15,15 +15,3 @@ Since Infisical's team is globally distributed, it is hard for us to keep track
|
||||
## Winter break
|
||||
|
||||
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
|
||||
|
||||
## Parental leave
|
||||
|
||||
At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. We’re proud to offer parental leave to everyone, regardless of gender, and whether you’ve become a parent through childbirth or adoption.
|
||||
|
||||
For team members who have been with Infisical for over a year by the time of your child’s birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
|
||||
|
||||
While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
|
||||
|
||||
When you’re ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
|
||||
|
||||
We’re here to support you as you embark on this exciting new chapter in your life!
|
||||
|
@ -6,7 +6,7 @@ description: "Learn how to manage secrets in local development environments."
|
||||
|
||||
## Problem at hand
|
||||
|
||||
There is a number of issues that arise with secret management in local development environment:
|
||||
There are a number of issues that arise with secret management in local development environment:
|
||||
1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another.
|
||||
2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time.
|
||||
3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability.
|
||||
|
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"
|
||||
>
|
||||
|
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 |
@ -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,6 +1,7 @@
|
||||
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
|
||||
import { User } from "../types";
|
||||
import {
|
||||
@ -10,7 +11,6 @@ import {
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
|
||||
export const adminStandaloneKeys = {
|
||||
getUsers: "get-users",
|
||||
|
@ -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
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -193,6 +193,7 @@ export type IdentityOidcAuth = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -208,6 +209,7 @@ export type AddIdentityOidcAuthDTO = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -225,6 +227,7 @@ export type UpdateIdentityOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
@ -28,13 +28,13 @@ import {
|
||||
useGetServerRootKmsEncryptionDetails,
|
||||
useUpdateServerConfig
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
import { AuthPanel } from "./components/AuthPanel";
|
||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||
import { RateLimitPanel } from "./components/RateLimitPanel";
|
||||
import { UserPanel } from "./components/UserPanel";
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
enum TabSections {
|
||||
Settings = "settings",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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.13
|
||||
version: v0.8.14
|
||||
# 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.13"
|
||||
appVersion: "v0.8.14"
|
||||
|
@ -504,4 +504,6 @@ status:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
storedVersions: []
|
||||
|
||||
{{- end }}
|
@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.8.13
|
||||
tag: v0.8.14
|
||||
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