Compare commits

..

32 Commits

Author SHA1 Message Date
38c9242e5b misc: add plain support for user get token in CLI 2025-07-02 04:45:53 +08:00
cce2a54265 Merge pull request #3883 from Infisical/doc/add-mention-of-default-audience-support
doc: add mention of default audience support for CSI
2025-07-01 14:35:15 -04:00
d1033cb324 Merge pull request #3875 from Infisical/ENG-3009
feat(super-admin): Environment Overrides
2025-07-02 02:18:40 +08:00
7134e1dc66 misc: updated success notif 2025-07-02 02:18:04 +08:00
8aa26b77ed Fix check 2025-07-01 13:11:15 -04:00
4b06880320 Feedback fixes 2025-07-01 11:52:01 -04:00
124cd9f812 Merge pull request #3893 from Infisical/misc/added-missing-project-cert-endpoints-to-open-api-spec
misc: added missing project cert endpoints to open api spec
2025-07-01 23:39:37 +08:00
d531d069d1 Add azure app connection 2025-07-01 11:23:44 -04:00
522a5d477d Merge pull request #3889 from Infisical/minor-access-approval-modal-improvements
improvement(approval-policy): minor create policy layout adjustments
2025-07-01 08:21:26 -07:00
d2f0db669a Merge pull request #3894 from Infisical/fix/address-instance-of-github-dynamic-secret
fix: address instanceof check in github dynamic secret
2025-07-01 23:11:01 +08:00
4dd78d745b fix: address instanceof check in github dynamic secret 2025-07-01 20:45:00 +08:00
4fef5c305d misc: added missing project cert endpoints to open api spec 2025-07-01 18:53:13 +08:00
30f3543850 Merge pull request #3876 from Infisical/ENG-2977
feat(secret-sync): Allow custom field label on 1pass sync
2025-06-30 23:36:22 -04:00
114915f913 Merge pull request #3891 from Infisical/change-request-page-improvements
improvement(secret-approval-request): Color/layout styling adjustments to change request page
2025-06-30 19:35:40 -07:00
b5801af9a8 improvements: address feedback 2025-06-30 18:32:36 -07:00
20366a8c07 improvement: address feedback 2025-06-30 18:09:50 -07:00
447e28511c improvement: update stale/conflict text 2025-06-30 16:44:29 -07:00
650ed656e3 improvement: color/layout styling adjustments to change request page 2025-06-30 16:30:37 -07:00
54ac450b63 improvement: minor layout adjustments 2025-06-30 14:38:23 -07:00
3871fa552c Merge pull request #3888 from Infisical/revert-3885-misc/add-indices-for-referencing-columns-in-identity-access-token
Revert "misc: add indices for referencing columns in identity access token"
2025-06-30 17:27:31 -04:00
9c72ee7f10 Revert "misc: add indices for referencing columns in identity access token" 2025-07-01 05:23:51 +08:00
22e8617661 Merge pull request #3885 from Infisical/misc/add-indices-for-referencing-columns-in-identity-access-token
misc: add indices for referencing columns in identity access token
2025-06-30 17:01:20 -04:00
cb6c28ac26 UI updates 2025-06-30 14:08:27 -04:00
d3833c33b3 Merge pull request #3878 from Infisical/fix-approval-policy-bypassing
Fix bypassing approval policies
2025-06-30 13:37:28 -04:00
3723afe595 Merge branch 'main' into ENG-3009 2025-06-30 12:01:14 -04:00
14d6f6c048 doc: add mention of default audience support for CSI 2025-06-30 23:51:50 +08:00
58b61a861a Fix bypassing approval policies 2025-06-28 04:17:09 -04:00
d79a6b8f25 Lint fixes 2025-06-28 03:35:52 -04:00
217a09c97b Docs 2025-06-28 03:14:45 -04:00
a389ede03d Review fixes 2025-06-28 03:01:34 -04:00
10939fecc0 feat(super-admin): Environment Overrides 2025-06-28 02:35:38 -04:00
9af5a66bab feat(secret-sync): Allow custom field label on 1pass sync 2025-06-26 16:07:08 -04:00
42 changed files with 1300 additions and 403 deletions

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (!hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.binary("encryptedEnvOverrides").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("encryptedEnvOverrides");
});
}
}

View File

@ -1,29 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.raw(`
CREATE INDEX CONCURRENTLY IF NOT EXISTS ${TableName.IdentityAccessToken}_identityid_index
ON ${TableName.IdentityAccessToken} ("identityId")
`);
await knex.raw(`
CREATE INDEX CONCURRENTLY IF NOT EXISTS ${TableName.IdentityAccessToken}_identityuaclientsecretid_index
ON ${TableName.IdentityAccessToken} ("identityUAClientSecretId")
`);
}
export async function down(knex: Knex): Promise<void> {
await knex.raw(`
DROP INDEX IF EXISTS ${TableName.IdentityAccessToken}_identityid_index
`);
await knex.raw(`
DROP INDEX IF EXISTS ${TableName.IdentityAccessToken}_identityuaclientsecretid_index
`);
}
const config = { transaction: false };
export { config };

View File

@ -34,7 +34,8 @@ export const SuperAdminSchema = z.object({
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(), encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional() encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
encryptedEnvOverrides: zodBuffer.nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -350,6 +350,12 @@ export const accessApprovalRequestServiceFactory = ({
const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId); const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId);
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass); const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
// Calculate break glass attempt before sequence checks
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
const isApprover = policy.approvers.find((approver) => approver.userId === actorId); const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
// If user is (not an approver OR cant self approve) AND can't bypass policy // If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) { if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
@ -409,15 +415,14 @@ export const accessApprovalRequestServiceFactory = ({
const isApproverOfTheSequence = policy.approvers.find( const isApproverOfTheSequence = policy.approvers.find(
(el) => el.sequence === presentSequence.step && el.userId === actorId (el) => el.sequence === presentSequence.step && el.userId === actorId
); );
if (!isApproverOfTheSequence) throw new BadRequestError({ message: "You are not reviewer in this step" });
// Only throw if actor is not the approver and not bypassing
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt) {
throw new BadRequestError({ message: "You are not a reviewer in this step" });
}
} }
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => { const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
let reviewForThisActorProcessing: { let reviewForThisActorProcessing: {
id: string; id: string;
requestId: string; requestId: string;

View File

@ -1,5 +1,5 @@
import axios from "axios"; import axios from "axios";
import * as jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";

View File

@ -2427,7 +2427,8 @@ export const SecretSyncs = {
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
}, },
ONEPASS: { ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to." vaultId: "The ID of the 1Password vault to sync secrets to.",
valueLabel: "The label of the entry that holds the secret value."
}, },
HEROKU: { HEROKU: {
app: "The ID of the Heroku app to sync secrets to.", app: "The ID of the Heroku app to sync secrets to.",

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import { QueueWorkerProfile } from "@app/lib/types"; import { QueueWorkerProfile } from "@app/lib/types";
import { BadRequestError } from "../errors";
import { removeTrailingSlash } from "../fn"; import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger"; import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod"; import { zpStr } from "../zod";
@ -341,8 +342,11 @@ const envSchema = z
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>; export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
let envCfg: TEnvConfig; let envCfg: TEnvConfig;
let originalEnvConfig: TEnvConfig;
export const getConfig = () => envCfg; export const getConfig = () => envCfg;
export const getOriginalConfig = () => originalEnvConfig;
// cannot import singleton logger directly as it needs config to load various transport // cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: CustomLogger) => { export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env); const parsedEnv = envSchema.safeParse(process.env);
@ -352,10 +356,115 @@ export const initEnvConfig = (logger?: CustomLogger) => {
process.exit(-1); process.exit(-1);
} }
envCfg = Object.freeze(parsedEnv.data); const config = Object.freeze(parsedEnv.data);
envCfg = config;
if (!originalEnvConfig) {
originalEnvConfig = config;
}
return envCfg; return envCfg;
}; };
// A list of environment variables that can be overwritten
export const overwriteSchema: {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; description?: string }[];
};
} = {
azure: {
name: "Azure",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
google_sso: {
name: "Google SSO",
fields: [
{
key: "CLIENT_ID_GOOGLE_LOGIN",
description: "The Client ID of your GCP OAuth2 application."
},
{
key: "CLIENT_SECRET_GOOGLE_LOGIN",
description: "The Client Secret of your GCP OAuth2 application."
}
]
},
github_sso: {
name: "GitHub SSO",
fields: [
{
key: "CLIENT_ID_GITHUB_LOGIN",
description: "The Client ID of your GitHub OAuth application."
},
{
key: "CLIENT_SECRET_GITHUB_LOGIN",
description: "The Client Secret of your GitHub OAuth application."
}
]
},
gitlab_sso: {
name: "GitLab SSO",
fields: [
{
key: "CLIENT_ID_GITLAB_LOGIN",
description: "The Client ID of your GitLab application."
},
{
key: "CLIENT_SECRET_GITLAB_LOGIN",
description: "The Secret of your GitLab application."
},
{
key: "CLIENT_GITLAB_LOGIN_URL",
description:
"The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to https://gitlab.com."
}
]
}
};
export const overridableKeys = new Set(
Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key))
);
export const validateOverrides = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (!parsedResult.success) {
const errorDetails = parsedResult.error.issues
.map((issue) => `Key: "${issue.path.join(".")}", Error: ${issue.message}`)
.join("\n");
throw new BadRequestError({ message: errorDetails });
}
};
export const overrideEnvConfig = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (parsedResult.success) {
envCfg = Object.freeze(parsedResult.data);
}
};
export const formatSmtpConfig = () => { export const formatSmtpConfig = () => {
const tlsOptions: { const tlsOptions: {
rejectUnauthorized: boolean; rejectUnauthorized: boolean;

View File

@ -2045,6 +2045,10 @@ export const registerRoutes = async (
cronJobs.push(adminIntegrationsSyncJob); cronJobs.push(adminIntegrationsSyncJob);
} }
} }
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL, user: userDAL,

View File

@ -8,7 +8,7 @@ import {
SuperAdminSchema, SuperAdminSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig, overridableKeys } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@ -42,7 +42,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedGitHubAppConnectionClientSecret: true, encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true, encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true, encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true encryptedGitHubAppConnectionPrivateKey: true,
encryptedEnvOverrides: true
}).extend({ }).extend({
isMigrationModeOn: z.boolean(), isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgSlug: z.string().nullable(),
@ -110,11 +111,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
.refine((content) => DOMPurify.sanitize(content) === content, { .refine((content) => DOMPurify.sanitize(content) === content, {
message: "Page frame content contains unsafe HTML." message: "Page frame content contains unsafe HTML."
}) })
.optional() .optional(),
envOverrides: z.record(z.enum(Array.from(overridableKeys) as [string, ...string[]]), z.string()).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
config: SuperAdminSchema.extend({ config: SuperAdminSchema.omit({
encryptedEnvOverrides: true
}).extend({
defaultAuthOrgSlug: z.string().nullable() defaultAuthOrgSlug: z.string().nullable()
}) })
}) })
@ -381,6 +385,41 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/env-overrides",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.record(
z.string(),
z.object({
name: z.string(),
fields: z
.object({
key: z.string(),
value: z.string(),
hasEnvEntry: z.boolean(),
description: z.string().optional()
})
.array()
})
)
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const envOverrides = await server.services.superAdmin.getEnvOverridesOrganized();
return envOverrides;
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/user-management/users/:userId", url: "/user-management/users/:userId",

View File

@ -457,6 +457,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiAlerting],
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim()
}), }),
@ -487,6 +489,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateCollections],
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim()
}), }),
@ -549,6 +553,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim()
}), }),

View File

@ -6,7 +6,6 @@ import {
TOnePassListVariablesResponse, TOnePassListVariablesResponse,
TOnePassSyncWithCredentials, TOnePassSyncWithCredentials,
TOnePassVariable, TOnePassVariable,
TOnePassVariableDetails,
TPostOnePassVariable, TPostOnePassVariable,
TPutOnePassVariable TPutOnePassVariable
} from "@app/services/secret-sync/1password/1password-sync-types"; } from "@app/services/secret-sync/1password/1password-sync-types";
@ -14,7 +13,10 @@ import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassListVariables) => { // This should not be changed or it may break existing logic
const VALUE_LABEL_DEFAULT = "value";
const listOnePassItems = async ({ instanceUrl, apiToken, vaultId, valueLabel }: TOnePassListVariables) => {
const { data } = await request.get<TOnePassListVariablesResponse>(`${instanceUrl}/v1/vaults/${vaultId}/items`, { const { data } = await request.get<TOnePassListVariablesResponse>(`${instanceUrl}/v1/vaults/${vaultId}/items`, {
headers: { headers: {
Authorization: `Bearer ${apiToken}`, Authorization: `Bearer ${apiToken}`,
@ -22,36 +24,49 @@ const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassList
} }
}); });
const result: Record<string, TOnePassVariable & { value: string; fieldId: string }> = {}; const items: Record<string, TOnePassVariable & { value: string; fieldId: string }> = {};
const duplicates: Record<string, string> = {};
for await (const s of data) { for await (const s of data) {
const { data: secret } = await request.get<TOnePassVariableDetails>( // eslint-disable-next-line no-continue
`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`, if (s.category !== "API_CREDENTIAL") continue;
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
const value = secret.fields.find((f) => f.label === "value")?.value; if (items[s.title]) {
const fieldId = secret.fields.find((f) => f.label === "value")?.id; duplicates[s.id] = s.title;
// eslint-disable-next-line no-continue
continue;
}
const { data: secret } = await request.get<TOnePassVariable>(`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
const valueField = secret.fields.find((f) => f.label === valueLabel);
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!value || !fieldId) continue; if (!valueField || !valueField.value || !valueField.id) continue;
result[s.title] = { items[s.title] = {
...secret, ...secret,
value, value: valueField.value,
fieldId fieldId: valueField.id
}; };
} }
return result; return { items, duplicates };
}; };
const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, itemValue }: TPostOnePassVariable) => { const createOnePassItem = async ({
instanceUrl,
apiToken,
vaultId,
itemTitle,
itemValue,
valueLabel
}: TPostOnePassVariable) => {
return request.post( return request.post(
`${instanceUrl}/v1/vaults/${vaultId}/items`, `${instanceUrl}/v1/vaults/${vaultId}/items`,
{ {
@ -63,7 +78,7 @@ const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, it
tags: ["synced-from-infisical"], tags: ["synced-from-infisical"],
fields: [ fields: [
{ {
label: "value", label: valueLabel,
value: itemValue, value: itemValue,
type: "CONCEALED" type: "CONCEALED"
} }
@ -85,7 +100,9 @@ const updateOnePassItem = async ({
itemId, itemId,
fieldId, fieldId,
itemTitle, itemTitle,
itemValue itemValue,
valueLabel,
otherFields
}: TPutOnePassVariable) => { }: TPutOnePassVariable) => {
return request.put( return request.put(
`${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`, `${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`,
@ -98,9 +115,10 @@ const updateOnePassItem = async ({
}, },
tags: ["synced-from-infisical"], tags: ["synced-from-infisical"],
fields: [ fields: [
...otherFields,
{ {
id: fieldId, id: fieldId,
label: "value", label: valueLabel,
value: itemValue, value: itemValue,
type: "CONCEALED" type: "CONCEALED"
} }
@ -128,13 +146,18 @@ export const OnePassSyncFns = {
const { const {
connection, connection,
environment, environment,
destinationConfig: { vaultId } destinationConfig: { vaultId, valueLabel }
} = secretSync; } = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection); const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials; const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId }); const { items, duplicates } = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
for await (const entry of Object.entries(secretMap)) { for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry; const [key, { value }] = entry;
@ -148,10 +171,19 @@ export const OnePassSyncFns = {
itemTitle: key, itemTitle: key,
itemValue: value, itemValue: value,
itemId: items[key].id, itemId: items[key].id,
fieldId: items[key].fieldId fieldId: items[key].fieldId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT,
otherFields: items[key].fields.filter((field) => field.label !== (valueLabel || VALUE_LABEL_DEFAULT))
}); });
} else { } else {
await createOnePassItem({ instanceUrl, apiToken, vaultId, itemTitle: key, itemValue: value }); await createOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemTitle: key,
itemValue: value,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
} }
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
@ -163,7 +195,28 @@ export const OnePassSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(items)) { // Delete duplicate item entries
for await (const [itemId, key] of Object.entries(duplicates)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
try {
await deleteOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemId
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
// Delete item entries that are not in secretMap
for await (const [key, item] of Object.entries(items)) {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
@ -173,7 +226,7 @@ export const OnePassSyncFns = {
instanceUrl, instanceUrl,
apiToken, apiToken,
vaultId, vaultId,
itemId: variable.id itemId: item.id
}); });
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
@ -187,13 +240,18 @@ export const OnePassSyncFns = {
removeSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
const { const {
connection, connection,
destinationConfig: { vaultId } destinationConfig: { vaultId, valueLabel }
} = secretSync; } = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection); const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials; const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId }); const { items } = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
for await (const [key, item] of Object.entries(items)) { for await (const [key, item] of Object.entries(items)) {
if (key in secretMap) { if (key in secretMap) {
@ -216,12 +274,19 @@ export const OnePassSyncFns = {
getSecrets: async (secretSync: TOnePassSyncWithCredentials) => { getSecrets: async (secretSync: TOnePassSyncWithCredentials) => {
const { const {
connection, connection,
destinationConfig: { vaultId } destinationConfig: { vaultId, valueLabel }
} = secretSync; } = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection); const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials; const { apiToken } = connection.credentials;
return listOnePassItems({ instanceUrl, apiToken, vaultId }); const res = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
return Object.fromEntries(Object.entries(res.items).map(([key, item]) => [key, { value: item.value }]));
} }
}; };

View File

@ -11,7 +11,8 @@ import {
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const OnePassSyncDestinationConfigSchema = z.object({ const OnePassSyncDestinationConfigSchema = z.object({
vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId) vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId),
valueLabel: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.valueLabel)
}); });
const OnePassSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const OnePassSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };

View File

@ -14,29 +14,32 @@ export type TOnePassSyncWithCredentials = TOnePassSync & {
connection: TOnePassConnection; connection: TOnePassConnection;
}; };
type Field = {
id: string;
type: string; // CONCEALED, STRING
label: string;
value: string;
};
export type TOnePassVariable = { export type TOnePassVariable = {
id: string; id: string;
title: string; title: string;
category: string; // API_CREDENTIAL, SECURE_NOTE, LOGIN, etc category: string; // API_CREDENTIAL, SECURE_NOTE, LOGIN, etc
}; fields: Field[];
export type TOnePassVariableDetails = TOnePassVariable & {
fields: {
id: string;
type: string; // CONCEALED, STRING
label: string;
value: string;
}[];
}; };
export type TOnePassListVariablesResponse = TOnePassVariable[]; export type TOnePassListVariablesResponse = TOnePassVariable[];
export type TOnePassListVariables = { type TOnePassBase = {
apiToken: string; apiToken: string;
instanceUrl: string; instanceUrl: string;
vaultId: string; vaultId: string;
}; };
export type TOnePassListVariables = TOnePassBase & {
valueLabel: string;
};
export type TPostOnePassVariable = TOnePassListVariables & { export type TPostOnePassVariable = TOnePassListVariables & {
itemTitle: string; itemTitle: string;
itemValue: string; itemValue: string;
@ -47,8 +50,9 @@ export type TPutOnePassVariable = TOnePassListVariables & {
fieldId: string; fieldId: string;
itemTitle: string; itemTitle: string;
itemValue: string; itemValue: string;
otherFields: Field[];
}; };
export type TDeleteOnePassVariable = TOnePassListVariables & { export type TDeleteOnePassVariable = TOnePassBase & {
itemId: string; itemId: string;
}; };

View File

@ -5,7 +5,13 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import {
getConfig,
getOriginalConfig,
overrideEnvConfig,
overwriteSchema,
validateOverrides
} from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -33,6 +39,7 @@ import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
import { import {
CacheType, CacheType,
EnvOverrides,
LoginMethod, LoginMethod,
TAdminBootstrapInstanceDTO, TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO, TAdminGetIdentitiesDTO,
@ -234,6 +241,45 @@ export const superAdminServiceFactory = ({
adminIntegrationsConfig = config; adminIntegrationsConfig = config;
}; };
const getEnvOverrides = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg || !serverCfg.encryptedEnvOverrides) {
return {};
}
const decrypt = kmsService.decryptWithRootKey();
const overrides = JSON.parse(decrypt(serverCfg.encryptedEnvOverrides).toString()) as Record<string, string>;
return overrides;
};
const getEnvOverridesOrganized = async (): Promise<EnvOverrides> => {
const overrides = await getEnvOverrides();
const ogConfig = getOriginalConfig();
return Object.fromEntries(
Object.entries(overwriteSchema).map(([groupKey, groupDef]) => [
groupKey,
{
name: groupDef.name,
fields: groupDef.fields.map(({ key, description }) => ({
key,
description,
value: overrides[key] || "",
hasEnvEntry: !!(ogConfig as unknown as Record<string, string | undefined>)[key]
}))
}
])
);
};
const $syncEnvConfig = async () => {
const config = await getEnvOverrides();
overrideEnvConfig(config);
};
const updateServerCfg = async ( const updateServerCfg = async (
data: TSuperAdminUpdate & { data: TSuperAdminUpdate & {
slackClientId?: string; slackClientId?: string;
@ -246,6 +292,7 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSlug?: string; gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string; gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string; gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
}, },
userId: string userId: string
) => { ) => {
@ -374,6 +421,17 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSettingsUpdated = true; gitHubAppConnectionSettingsUpdated = true;
} }
let envOverridesUpdated = false;
if (data.envOverrides !== undefined) {
// Verify input format
validateOverrides(data.envOverrides);
const encryptedEnvOverrides = encryptWithRoot(Buffer.from(JSON.stringify(data.envOverrides)));
updatedData.encryptedEnvOverrides = encryptedEnvOverrides;
updatedData.envOverrides = undefined;
envOverridesUpdated = true;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData); const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
@ -382,6 +440,10 @@ export const superAdminServiceFactory = ({
await $syncAdminIntegrationConfig(); await $syncAdminIntegrationConfig();
} }
if (envOverridesUpdated) {
await $syncEnvConfig();
}
if ( if (
updatedServerCfg.encryptedMicrosoftTeamsAppId && updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret && updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
@ -814,6 +876,18 @@ export const superAdminServiceFactory = ({
return job; return job;
}; };
const initializeEnvConfigSync = async () => {
logger.info("Setting up background sync process for environment overrides");
await $syncEnvConfig();
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", $syncEnvConfig);
job.start();
return job;
};
return { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
@ -833,6 +907,9 @@ export const superAdminServiceFactory = ({
getOrganizations, getOrganizations,
deleteOrganization, deleteOrganization,
deleteOrganizationMembership, deleteOrganizationMembership,
initializeAdminIntegrationConfigSync initializeAdminIntegrationConfigSync,
initializeEnvConfigSync,
getEnvOverrides,
getEnvOverridesOrganized
}; };
}; };

View File

@ -1,3 +1,5 @@
import { TEnvConfig } from "@app/lib/config/env";
export type TAdminSignUpDTO = { export type TAdminSignUpDTO = {
email: string; email: string;
password: string; password: string;
@ -74,3 +76,10 @@ export type TAdminIntegrationConfig = {
privateKey: string; privateKey: string;
}; };
}; };
export interface EnvOverrides {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; value: string; hasEnvEntry: boolean; description?: string }[];
};
}

View File

@ -114,6 +114,11 @@ var userGetTokenCmd = &cobra.Command{
loggedInUserDetails = util.EstablishUserLoginSession() loggedInUserDetails = util.EstablishUserLoginSession()
} }
plain, err := cmd.Flags().GetBool("plain")
if err != nil {
util.HandleError(err, "[infisical user get token]: Unable to get plain flag")
}
if err != nil { if err != nil {
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token") util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
} }
@ -135,8 +140,12 @@ var userGetTokenCmd = &cobra.Command{
util.HandleError(err, "[infisical user get token]: Unable to parse token payload") util.HandleError(err, "[infisical user get token]: Unable to parse token payload")
} }
fmt.Println("Session ID:", tokenPayload.TokenVersionId) if plain {
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken) fmt.Println(loggedInUserDetails.UserCredentials.JTWToken)
} else {
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
}
}, },
} }
@ -240,7 +249,10 @@ var domainCmd = &cobra.Command{
func init() { func init() {
updateCmd.AddCommand(domainCmd) updateCmd.AddCommand(domainCmd)
userCmd.AddCommand(updateCmd) userCmd.AddCommand(updateCmd)
userGetTokenCmd.Flags().Bool("plain", false, "print token without formatting")
userGetCmd.AddCommand(userGetTokenCmd) userGetCmd.AddCommand(userGetTokenCmd)
userCmd.AddCommand(userGetCmd) userCmd.AddCommand(userGetCmd)
userCmd.AddCommand(switchCmd) userCmd.AddCommand(switchCmd)
rootCmd.AddCommand(userCmd) rootCmd.AddCommand(userCmd)

View File

@ -35,19 +35,40 @@ infisical user update domain
<Accordion title="infisical user get token"> <Accordion title="infisical user get token">
Use this command to get your current Infisical access token and session information. This command requires you to be logged in. Use this command to get your current Infisical access token and session information. This command requires you to be logged in.
The command will display: The command will display:
- Your session ID - Your session ID
- Your full JWT access token - Your full JWT access token
```bash ```bash
infisical user get token infisical user get token
``` ```
Example output: Example output:
```bash
Session ID: abc123-xyz-456
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Flags
<Accordion title="--plain">
Output only the JWT token without formatting (no session ID)
Default value: `false`
```bash
# Example
infisical user get token --plain
```
Example output:
```bash
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
</Accordion>
```bash
Session ID: abc123-xyz-456
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
</Accordion> </Accordion>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 641 KiB

View File

@ -44,8 +44,11 @@ Currently, the Infisical CSI provider only supports static secrets.
### Install Secrets Store CSI Driver ### Install Secrets Store CSI Driver
In order to use the Infisical CSI provider, you will first have to install the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation) to your cluster. It is important that you define In order to use the Infisical CSI provider, you will first have to install the [Secrets Store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation) to your cluster.
the audience value for token requests as demonstrated below. The Infisical CSI provider will **NOT WORK** if this is not set.
#### Standard Installation
For most Kubernetes clusters, use the following installation:
```bash ```bash
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
@ -62,7 +65,7 @@ helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
The flags configure the following: The flags configure the following:
- `tokenRequests[0].audience=infisical`: Sets the audience value for service account token authentication (required) - `tokenRequests[0].audience=infisical`: Sets the audience value for service account token authentication (recommended for environments that support custom audiences)
- `enableSecretRotation=true`: Enables automatic secret updates from Infisical - `enableSecretRotation=true`: Enables automatic secret updates from Infisical
- `rotationPollInterval=2m`: Checks for secret updates every 2 minutes - `rotationPollInterval=2m`: Checks for secret updates every 2 minutes
- `syncSecret.enabled=true`: Enables syncing secrets to Kubernetes secrets - `syncSecret.enabled=true`: Enables syncing secrets to Kubernetes secrets
@ -76,6 +79,25 @@ The flags configure the following:
for the CSI driver. for the CSI driver.
</Info> </Info>
#### Installation for Environments Without Custom Audience Support
Some Kubernetes environments (such as AWS EKS) don't support custom audiences and will reject tokens with non-default audiences. For these environments, use this installation instead:
```bash
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
--namespace=kube-system \
--set enableSecretRotation=true \
--set rotationPollInterval=2m \
--set "syncSecret.enabled=true" \
```
<Warning>
**Environments without custom audience support**: Do not set a custom audience
when installing the CSI driver in environments that reject custom audiences.
Instead, use the installation above and set `useDefaultAudience: "true"` in
your SecretProviderClass configuration.
</Warning>
### Install Infisical CSI Provider ### Install Infisical CSI Provider
You would then have to install the Infisical CSI provider to your cluster. You would then have to install the Infisical CSI provider to your cluster.
@ -107,9 +129,12 @@ a machine identity with [Kubernetes authentication](https://infisical.com/docs/d
You can refer to the documentation for setting it up [here](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth#guide). You can refer to the documentation for setting it up [here](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth#guide).
<Warning> <Warning>
The allowed audience field of the Kubernetes authentication settings should **Important**: The "Allowed Audience" field in your machine identity's
match the audience specified for the Secrets Store CSI driver during Kubernetes authentication settings must match your CSI driver installation. If
installation. you used the standard installation with `tokenRequests[0].audience=infisical`,
set the "Allowed Audience" field to `infisical`. If you used the installation
for environments without custom audience support, leave the "Allowed Audience"
field empty.
</Warning> </Warning>
### Creating Secret Provider Class ### Creating Secret Provider Class
@ -117,6 +142,8 @@ You can refer to the documentation for setting it up [here](https://infisical.co
With the Secrets Store CSI driver and the Infisical CSI provider installed, create a Kubernetes [SecretProviderClass](https://secrets-store-csi-driver.sigs.k8s.io/concepts.html#secretproviderclass) resource to establish With the Secrets Store CSI driver and the Infisical CSI provider installed, create a Kubernetes [SecretProviderClass](https://secrets-store-csi-driver.sigs.k8s.io/concepts.html#secretproviderclass) resource to establish
the connection between the CSI driver and the Infisical CSI provider for secret retrieval. You can create as many Secret Provider Classes as needed for your cluster. the connection between the CSI driver and the Infisical CSI provider for secret retrieval. You can create as many Secret Provider Classes as needed for your cluster.
#### Standard Configuration
```yaml ```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1 apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass kind: SecretProviderClass
@ -139,6 +166,41 @@ spec:
secretKey: "APP_SECRET" secretKey: "APP_SECRET"
``` ```
#### Configuration for Environments Without Custom Audience Support
For environments that don't support custom audiences (such as AWS EKS), use this configuration instead:
```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: my-infisical-app-csi-provider
spec:
provider: infisical
parameters:
infisicalUrl: "https://app.infisical.com"
authMethod: "kubernetes"
useDefaultAudience: "true"
identityId: "ad2f8c67-cbe2-417a-b5eb-1339776ec0b3"
projectId: "09eda1f8-85a3-47a9-8a6f-e27f133b2a36"
envSlug: "prod"
secrets: |
- secretPath: "/"
fileName: "dbPassword"
secretKey: "DB_PASSWORD"
- secretPath: "/app"
fileName: "appSecret"
secretKey: "APP_SECRET"
```
<Note>
**Key difference**: The only change from the standard configuration is the
addition of `useDefaultAudience: "true"`. This parameter tells the CSI
provider to use the default Kubernetes audience instead of a custom
"infisical" audience, which is required for environments that reject custom
audiences.
</Note>
<Note> <Note>
The SecretProviderClass should be provisioned in the same namespace as the pod The SecretProviderClass should be provisioned in the same namespace as the pod
you intend to mount secrets to. you intend to mount secrets to.
@ -189,6 +251,19 @@ spec:
`infisical`. `infisical`.
</Accordion> </Accordion>
<Accordion title="useDefaultAudience">
When set to `"true"`, the Infisical CSI provider will use the default
Kubernetes audience instead of a custom audience. This is required for
environments that don't support custom audiences (such as AWS EKS), which
reject tokens with non-default audiences. When using this option, do not set a
custom audience in the CSI driver installation. This defaults to `false`.
<Note>
When enabled, the CSI provider will dynamically create service account
tokens on-demand using the default Kubernetes audience, rather than using
pre-existing tokens from the CSI driver.
</Note>
</Accordion>
### Using Secret Provider Class ### Using Secret Provider Class
A pod can use the Secret Provider Class by mounting it as a CSI volume: A pod can use the Secret Provider Class by mounting it as a CSI volume:
@ -252,6 +327,11 @@ kubectl logs csi-secrets-store-csi-driver-7h4jp -n=kube-system
- Invalid machine identity configuration - Invalid machine identity configuration
- Incorrect secret paths or keys - Incorrect secret paths or keys
**Issues in environments without custom audience support:**
- **Token authentication failed with custom audience**: If you're seeing authentication errors in environments that don't support custom audiences (such as AWS EKS), ensure you're using the installation without custom audience and have set `useDefaultAudience: "true"` in your SecretProviderClass
- **Audience not allowed errors**: Make sure the "Allowed Audience" field is left empty in your machine identity's Kubernetes authentication configuration when using environments that don't support custom audiences
## Best Practices ## Best Practices
For additional guidance on setting this up for your production cluster, you can refer to the Secrets Store CSI driver documentation [here](https://secrets-store-csi-driver.sigs.k8s.io/topics/best-practices). For additional guidance on setting this up for your production cluster, you can refer to the Secrets Store CSI driver documentation [here](https://secrets-store-csi-driver.sigs.k8s.io/topics/best-practices).

View File

@ -36,6 +36,7 @@ description: "Learn how to configure a 1Password Sync for Infisical."
- **1Password Connection**: The 1Password Connection to authenticate with. - **1Password Connection**: The 1Password Connection to authenticate with.
- **Vault**: The 1Password vault to sync secrets to. - **Vault**: The 1Password vault to sync secrets to.
- **Value Label**: The label of the 1Password item field that will hold your secret value.
</Step> </Step>
<Step title="Configure sync options"> <Step title="Configure sync options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
@ -94,7 +95,8 @@ description: "Learn how to configure a 1Password Sync for Infisical."
"initialSyncBehavior": "overwrite-destination" "initialSyncBehavior": "overwrite-destination"
}, },
"destinationConfig": { "destinationConfig": {
"vaultId": "..." "vaultId": "...",
"valueLabel": "value"
} }
}' }'
``` ```
@ -145,7 +147,8 @@ description: "Learn how to configure a 1Password Sync for Infisical."
}, },
"destination": "1password", "destination": "1password",
"destinationConfig": { "destinationConfig": {
"vaultId": "..." "vaultId": "...",
"valueLabel": "value"
} }
} }
} }
@ -160,4 +163,7 @@ description: "Learn how to configure a 1Password Sync for Infisical."
Infisical can only perform CRUD operations on the following item types: Infisical can only perform CRUD operations on the following item types:
- API Credentials - API Credentials
</Accordion> </Accordion>
<Accordion title="What is a 'Value Label'?">
It's the label of the 1Password item field which will hold your secret value. For example, if you were to sync Infisical secret 'foo: bar', the 1Password item equivalent would have an item title of 'foo', and a field on that item 'value: bar'. The field label 'value' is what gets changed by this option.
</Accordion>
</AccordionGroup> </AccordionGroup>

View File

@ -4,7 +4,7 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
import { import {
TOnePassVault, TOnePassVault,
useOnePassConnectionListVaults useOnePassConnectionListVaults
@ -32,6 +32,7 @@ export const OnePassSyncFields = () => {
<SecretSyncConnectionField <SecretSyncConnectionField
onChange={() => { onChange={() => {
setValue("destinationConfig.vaultId", ""); setValue("destinationConfig.vaultId", "");
setValue("destinationConfig.valueLabel", "");
}} }}
/> />
@ -69,6 +70,22 @@ export const OnePassSyncFields = () => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
isOptional
label="Value Label"
tooltipText="It's the label of the 1Password item field which will hold your secret value. For example, if you were to sync Infisical secret 'foo: bar', the 1Password item equivalent would have an item title of 'foo', and a field on that item 'value: bar'. The field label 'value' is what gets changed by this option."
>
<Input value={value} onChange={onChange} placeholder="value" />
</FormControl>
)}
control={control}
name="destinationConfig.valueLabel"
/>
</> </>
); );
}; };

View File

@ -6,7 +6,15 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
export const OnePassSyncReviewFields = () => { export const OnePassSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OnePass }>(); const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OnePass }>();
const vaultId = watch("destinationConfig.vaultId"); const [vaultId, valueLabel] = watch([
"destinationConfig.vaultId",
"destinationConfig.valueLabel"
]);
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>; return (
<>
<GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>
<GenericFieldLabel label="Value Key">{valueLabel || "value"}</GenericFieldLabel>
</>
);
}; };

View File

@ -7,7 +7,8 @@ export const OnePassSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({ z.object({
destination: z.literal(SecretSync.OnePass), destination: z.literal(SecretSync.OnePass),
destinationConfig: z.object({ destinationConfig: z.object({
vaultId: z.string().trim().min(1, "Vault ID required") vaultId: z.string().trim().min(1, "Vault ID required"),
valueLabel: z.string().trim().optional()
}) })
}) })
); );

View File

@ -0,0 +1,42 @@
export const HighlightText = ({
text,
highlight,
highlightClassName
}: {
text: string | undefined | null;
highlight: string;
highlightClassName?: string;
}) => {
if (!text) return null;
const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) return <span>{text}</span>;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedSearchTerm, "gi");
text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) {
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
}
parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{match}
</span>
);
lastIndex = offset + match.length;
return match;
});
if (lastIndex < text.length) {
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
}
return parts;
};

View File

@ -0,0 +1 @@
export { HighlightText } from "./HighlightText";

View File

@ -10,6 +10,7 @@ import {
AdminGetUsersFilters, AdminGetUsersFilters,
AdminIntegrationsConfig, AdminIntegrationsConfig,
OrganizationWithProjects, OrganizationWithProjects,
TGetEnvOverrides,
TGetInvalidatingCacheStatus, TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails, TGetServerRootKmsEncryptionDetails,
TServerConfig TServerConfig
@ -31,7 +32,8 @@ export const adminQueryKeys = {
getAdminSlackConfig: () => ["admin-slack-config"] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const, getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getEnvOverrides: () => ["env-overrides"] as const
}; };
export const fetchServerConfig = async () => { export const fetchServerConfig = async () => {
@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => {
refetchInterval: (data) => (data ? 3000 : false) refetchInterval: (data) => (data ? 3000 : false)
}); });
}; };
export const useGetEnvOverrides = () => {
return useQuery({
queryKey: adminQueryKeys.getEnvOverrides(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetEnvOverrides>("/api/v1/admin/env-overrides");
return data;
}
});
};

View File

@ -48,6 +48,7 @@ export type TServerConfig = {
authConsentContent?: string; authConsentContent?: string;
pageFrameContent?: string; pageFrameContent?: string;
invalidatingCache: boolean; invalidatingCache: boolean;
envOverrides?: Record<string, string>;
}; };
export type TUpdateServerConfigDTO = { export type TUpdateServerConfigDTO = {
@ -61,6 +62,7 @@ export type TUpdateServerConfigDTO = {
gitHubAppConnectionSlug?: string; gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string; gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string; gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
} & Partial<TServerConfig>; } & Partial<TServerConfig>;
export type TCreateAdminUserDTO = { export type TCreateAdminUserDTO = {
@ -138,3 +140,10 @@ export type TInvalidateCacheDTO = {
export type TGetInvalidatingCacheStatus = { export type TGetInvalidatingCacheStatus = {
invalidating: boolean; invalidating: boolean;
}; };
export interface TGetEnvOverrides {
[key: string]: {
name: string;
fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[];
};
}

View File

@ -6,6 +6,7 @@ export type TOnePassSync = TRootSecretSync & {
destination: SecretSync.OnePass; destination: SecretSync.OnePass;
destinationConfig: { destinationConfig: {
vaultId: string; vaultId: string;
valueLabel?: string;
}; };
connection: { connection: {
app: AppConnection.OnePass; app: AppConnection.OnePass;

View File

@ -41,6 +41,11 @@ const generalTabs = [
label: "Caching", label: "Caching",
icon: "note", icon: "note",
link: "/admin/caching" link: "/admin/caching"
},
{
label: "Environment",
icon: "unlock",
link: "/admin/environment"
} }
]; ];

View File

@ -164,7 +164,10 @@ export const MinimizedOrgSidebar = () => {
const handleCopyToken = async () => { const handleCopyToken = async () => {
try { try {
await window.navigator.clipboard.writeText(getAuthToken()); await window.navigator.clipboard.writeText(getAuthToken());
createNotification({ type: "success", text: "Copied current login session token to clipboard" }); createNotification({
type: "success",
text: "Copied current login session token to clipboard"
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
createNotification({ type: "error", text: "Failed to copy user token to clipboard" }); createNotification({ type: "error", text: "Failed to copy user token to clipboard" });

View File

@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { EnvironmentPageForm } from "./components";
export const EnvironmentPage = () => {
const { t } = useTranslation();
return (
<div className="bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Environment"
description="Manage the environment for your Infisical instance."
/>
<EnvironmentPageForm />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,245 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
faChevronRight,
faExclamationTriangle,
faMagnifyingGlass
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
type TForm = Record<string, string>;
export const GroupContainer = ({
group,
control,
search
}: {
group: {
fields: {
key: string;
value: string;
hasEnvEntry: boolean;
description?: string;
}[];
name: string;
};
control: Control<TForm, any, TForm>;
search: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div
key={group.name}
className="overflow-clip border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md last:border-b"
>
<div
className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
role="button"
tabIndex={0}
onClick={() => setOpen((o) => !o)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setOpen((o) => !o);
}
}}
>
<FontAwesomeIcon
className={`mr-8 transition-transform duration-100 ${open || search ? "rotate-90" : ""}`}
icon={faChevronRight}
/>
<div className="flex-grow select-none text-base">{group.name}</div>
</div>
{(open || search) && (
<div className="flex flex-col">
{group.fields.map((field) => (
<div
key={field.key}
className="flex items-center justify-between gap-4 border-t border-mineshaft-500 bg-mineshaft-700/50 p-4"
>
<div className="flex max-w-lg flex-col">
<span className="text-sm">
<HighlightText text={field.key} highlight={search} />
</span>
<span className="text-sm text-mineshaft-400">
<HighlightText text={field.description} highlight={search} />
</span>
</div>
<div className="flex grow items-center justify-end gap-2">
{field.hasEnvEntry && (
<Tooltip
content="Setting this value will override an existing environment variable"
className="text-center"
>
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow" />
</Tooltip>
)}
<Controller
control={control}
name={field.key}
render={({ field: formGenField, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-0 w-full max-w-sm"
>
<SecretInput
{...formGenField}
autoComplete="off"
containerClassName="text-bunker-300 hover:border-mineshaft-400 border border-mineshaft-600 bg-bunker-600 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
))}
</div>
)}
</div>
);
};
export const EnvironmentPageForm = () => {
const { data: envOverrides } = useGetEnvOverrides();
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const [search, setSearch] = useState("");
const allFields = useMemo(() => {
if (!envOverrides) return [];
return Object.values(envOverrides).flatMap((group) => group.fields);
}, [envOverrides]);
const formSchema = useMemo(() => {
return z.object(Object.fromEntries(allFields.map((field) => [field.key, z.string()])));
}, [allFields]);
const defaultValues = useMemo(() => {
const values: Record<string, string> = {};
allFields.forEach((field) => {
values[field.key] = field.value ?? "";
});
return values;
}, [allFields]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues
});
const formValues = useWatch({ control });
const filteredData = useMemo(() => {
if (!envOverrides) return [];
const searchTerm = search.toLowerCase().trim();
if (!searchTerm) {
return Object.values(envOverrides);
}
return Object.values(envOverrides)
.map((group) => {
const filteredFields = group.fields.filter(
(field) =>
field.key.toLowerCase().includes(searchTerm) ||
(field.description ?? "").toLowerCase().includes(searchTerm)
);
if (filteredFields.length > 0) {
return { ...group, fields: filteredFields };
}
return null;
})
.filter(Boolean);
}, [search, formValues, envOverrides]);
useEffect(() => {
reset(defaultValues);
}, [defaultValues, reset]);
const onSubmit = useCallback(
async (formData: TForm) => {
try {
const filteredFormData = Object.fromEntries(
Object.entries(formData).filter(([, value]) => value !== "")
);
await updateServerConfig({
envOverrides: filteredFormData
});
createNotification({
type: "success",
text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect."
});
reset(formData);
} catch (error) {
const errorMessage =
(error as any)?.response?.data?.message ||
(error as any)?.message ||
"An unknown error occurred";
createNotification({
type: "error",
title: "Failed to update environment overrides",
text: errorMessage
});
}
},
[reset, updateServerConfig]
);
return (
<form
className="flex flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex w-full flex-row items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Overrides</p>
</div>
<p className="text-sm text-bunker-300">Override specific environment variables.</p>
</div>
<div className="flex flex-row gap-2">
<Button
type="submit"
variant="outline_bg"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save Overrides
</Button>
</div>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search for keys, descriptions, and values..."
className="flex-1"
/>
<div className="flex flex-col">
{filteredData.map((group) => (
<GroupContainer group={group!} control={control} search={search} />
))}
</div>
</form>
);
};

View File

@ -0,0 +1 @@
export { EnvironmentPageForm } from "./EnvironmentPageForm";

View File

@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { EnvironmentPage } from "./EnvironmentPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/environment"
)({
component: EnvironmentPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Environment",
link: linkOptions({
to: "/admin/environment"
})
}
]
};
}
});

View File

@ -439,39 +439,35 @@ export const ReviewAccessRequestModal = ({
</div> </div>
) : ( ) : (
<> <>
{isSoftEnforcement && {isSoftEnforcement && request.isRequestedByCurrentUser && canBypass && (
request.isRequestedByCurrentUser && <div className="mt-2 flex flex-col space-y-2">
!(request.isApprover && request.isSelfApproveAllowed) && <Checkbox
canBypass && ( onCheckedChange={(checked) => setBypassApproval(checked === true)}
<div className="mt-2 flex flex-col space-y-2"> isChecked={bypassApproval}
<Checkbox id="byPassApproval"
onCheckedChange={(checked) => setBypassApproval(checked === true)} className={twMerge("mr-2", bypassApproval ? "!border-red/30 !bg-red/10" : "")}
isChecked={bypassApproval} >
id="byPassApproval" <span className="text-xs text-red">
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")} Approve without waiting for requirements to be met (bypass policy protection)
</span>
</Checkbox>
{bypassApproval && (
<FormControl
label="Reason for bypass"
className="mt-2"
isRequired
tooltipText="Enter a reason for bypassing the policy"
> >
<span className="text-xs text-red"> <Input
Approve without waiting for requirements to be met (bypass policy value={bypassReason}
protection) onChange={(e) => setBypassReason(e.currentTarget.value)}
</span> placeholder="Enter reason for bypass (min 10 chars)"
</Checkbox> leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
{bypassApproval && ( />
<FormControl </FormControl>
label="Reason for bypass" )}
className="mt-2" </div>
isRequired )}
tooltipText="Enter a reason for bypassing the secret change policy"
>
<Input
value={bypassReason}
onChange={(e) => setBypassReason(e.currentTarget.value)}
placeholder="Enter reason for bypass (min 10 chars)"
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
/>
</FormControl>
)}
</div>
)}
<div className="space-x-2"> <div className="space-x-2">
<Button <Button
isLoading={isLoading === "approved"} isLoading={isLoading === "approved"}

View File

@ -400,7 +400,7 @@ const Form = ({
isError={Boolean(error)} isError={Boolean(error)}
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path." tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
errorText={error?.message} errorText={error?.message}
className="flex-grow" className="flex-1"
> >
<Select <Select
isDisabled={isEditMode} isDisabled={isEditMode}
@ -419,6 +419,20 @@ const Form = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Policy Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
{!isAccessPolicyType && ( {!isAccessPolicyType && (
<Controller <Controller
control={control} control={control}
@ -429,7 +443,7 @@ const Form = ({
label="Min. Approvals Required" label="Min. Approvals Required"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-grow" className="flex-shrink"
> >
<Input <Input
{...field} {...field}
@ -443,20 +457,6 @@ const Form = ({
)} )}
</div> </div>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Policy Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
name="secretPath" name="secretPath"
@ -467,35 +467,36 @@ const Form = ({
label="Secret Path" label="Secret Path"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-grow" className="flex-1"
> >
<Input {...field} value={field.value || ""} /> <Input {...field} value={field.value || ""} />
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<FilterableSelect
isDisabled={isEditMode}
value={value}
onChange={onChange}
placeholder="Select environment..."
options={environments}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</div> </div>
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
isDisabled={isEditMode}
value={value}
onChange={onChange}
placeholder="Select environment..."
options={environments}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<div className="mb-2"> <div className="mb-2">
<p>Approvers</p> <p>Approvers</p>
<p className="font-inter text-xs text-mineshaft-300 opacity-90"> <p className="font-inter text-xs text-mineshaft-300 opacity-90">
@ -504,11 +505,11 @@ const Form = ({
</div> </div>
{isAccessPolicyType ? ( {isAccessPolicyType ? (
<> <>
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded"> <div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded border border-mineshaft-600 bg-mineshaft-900 p-2">
{sequenceApproversFieldArray.fields.map((el, index) => ( {sequenceApproversFieldArray.fields.map((el, index) => (
<div <div
className={twMerge( className={twMerge(
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0", "rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0 shadow-inner",
dragOverItem === index ? "border-2 border-blue-400" : "", dragOverItem === index ? "border-2 border-blue-400" : "",
draggedItem === index ? "opacity-50" : "" draggedItem === index ? "opacity-50" : ""
)} )}
@ -567,7 +568,7 @@ const Form = ({
label="User Approvers" label="User Approvers"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-grow" className="flex-1"
> >
<FilterableSelect <FilterableSelect
menuPortalTarget={modalContainer.current} menuPortalTarget={modalContainer.current}
@ -597,7 +598,7 @@ const Form = ({
label="Group Approvers" label="Group Approvers"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
className="flex-grow" className="flex-1"
> >
<FilterableSelect <FilterableSelect
menuPortalTarget={modalContainer.current} menuPortalTarget={modalContainer.current}
@ -800,10 +801,15 @@ const Form = ({
</> </>
)} )}
<div className="mt-8 flex items-center space-x-4"> <div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}> <Button
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Save Save
</Button> </Button>
<Button onClick={() => onToggle(false)} variant="outline_bg"> <Button onClick={() => onToggle(false)} colorSchema="secondary" variant="plain">
Close Close
</Button> </Button>
</div> </div>

View File

@ -102,43 +102,73 @@ export const SecretApprovalRequestAction = ({
if (!hasMerged && status === "open") { if (!hasMerged && status === "open") {
return ( return (
<div className="flex w-full flex-col items-start justify-between py-4 transition-all"> <div className="flex w-full flex-col items-start justify-between py-4 text-mineshaft-100 transition-all">
<div className="flex items-center space-x-4 px-4"> <div className="flex w-full flex-col justify-between xl:flex-row xl:items-center">
<div <div className="mr-auto flex items-center space-x-4 px-4">
className={`flex items-center justify-center rounded-full ${isMergable ? "h-10 w-10 bg-green" : "h-11 w-11 bg-red-600"}`} <div
> className={`flex items-center justify-center rounded-full ${isMergable ? "h-8 w-8 bg-green" : "h-10 w-10 bg-red-600"}`}
<FontAwesomeIcon >
icon={isMergable ? faCheck : faXmark} <FontAwesomeIcon
className={isMergable ? "text-lg text-black" : "text-2xl text-white"} icon={isMergable ? faCheck : faXmark}
/> className={isMergable ? "text-lg text-white" : "text-2xl text-white"}
/>
</div>
<span className="flex flex-col">
<p className={`text-md font-medium ${isMergable && "text-lg"}`}>
{isMergable ? "Good to merge" : "Merging is blocked"}
</p>
{!isMergable && (
<span className="inline-block text-xs text-mineshaft-300">
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by
eligible reviewers.
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
</span>
)}
</span>
</div> </div>
<span className="flex flex-col"> <div className="mt-4 flex items-center justify-end space-x-2 px-4 xl:mt-0">
<p className={`text-md font-medium ${isMergable && "text-lg"}`}> {canApprove || isSoftEnforcement ? (
{isMergable ? "Good to merge" : "Merging is blocked"} <div className="flex items-center space-x-4">
</p> <Button
{!isMergable && ( onClick={() => handleSecretApprovalStatusChange("close")}
<span className="inline-block text-xs text-bunker-200"> isLoading={isStatusChanging}
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by variant="outline_bg"
eligible reviewers. colorSchema="primary"
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`} leftIcon={<FontAwesomeIcon icon={faClose} />}
</span> className="hover:border-red/60 hover:bg-red/10"
>
Close request
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
isDisabled={
!(
(isMergable && canApprove) ||
(isSoftEnforcement && byPassApproval && isValidBypassReason(bypassReason))
)
}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
variant="outline_bg"
>
Merge
</Button>
</div>
) : (
<div className="text-sm text-mineshaft-400">Only approvers can merge</div>
)} )}
</span> </div>
</div> </div>
{isSoftEnforcement && !isMergable && isBypasser && ( {isSoftEnforcement && !isMergable && isBypasser && (
<div <div className="mt-4 w-full border-t border-mineshaft-600 px-5">
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
>
<div className="mt-2 flex flex-col space-y-2 pt-2"> <div className="mt-2 flex flex-col space-y-2 pt-2">
<Checkbox <Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)} onCheckedChange={(checked) => setByPassApproval(checked === true)}
isChecked={byPassApproval} isChecked={byPassApproval}
id="byPassApproval" id="byPassApproval"
checkIndicatorBg="text-white" checkIndicatorBg="text-white"
className={twMerge( className={twMerge("mr-2", byPassApproval ? "!border-red/30 !bg-red/10" : "")}
"mr-2",
byPassApproval ? "border-red bg-red hover:bg-red-600" : ""
)}
> >
<span className="text-sm"> <span className="text-sm">
Merge without waiting for approval (bypass secret change policy) Merge without waiting for approval (bypass secret change policy)
@ -162,51 +192,18 @@ export const SecretApprovalRequestAction = ({
</div> </div>
</div> </div>
)} )}
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
{canApprove || isSoftEnforcement ? (
<div className="flex items-center space-x-4">
<Button
onClick={() => handleSecretApprovalStatusChange("close")}
isLoading={isStatusChanging}
variant="outline_bg"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faClose} />}
className="hover:border-red/60 hover:bg-red/10"
>
Close request
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
isDisabled={
!(
(isMergable && canApprove) ||
(isSoftEnforcement && byPassApproval && isValidBypassReason(bypassReason))
)
}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
variant="solid"
>
Merge
</Button>
</div>
) : (
<div>Only approvers can merge</div>
)}
</div>
</div> </div>
); );
} }
if (hasMerged && status === "close") if (hasMerged && status === "close")
return ( return (
<div className="flex w-full items-center justify-between rounded-md border border-primary/60 bg-primary/10"> <div className="flex w-full items-center justify-between rounded-md border border-green/60 bg-green/10">
<div className="flex items-start space-x-4 p-4"> <div className="flex items-start space-x-2 p-4">
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" /> <FontAwesomeIcon icon={faCheck} className="mt-0.5 text-xl text-green" />
<span className="flex flex-col"> <span className="flex flex-col">
Change request merged Change request merged
<span className="inline-block text-xs text-bunker-200"> <span className="inline-block text-xs text-mineshaft-300">
Merged by {statusChangeByEmail}. Merged by {statusChangeByEmail}.
</span> </span>
</span> </span>
@ -215,26 +212,26 @@ export const SecretApprovalRequestAction = ({
); );
return ( return (
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between rounded-md border border-yellow/60 bg-yellow/10">
<div className="flex items-start space-x-4"> <div className="flex items-start space-x-2 p-4">
<FontAwesomeIcon icon={faUserLock} className="pt-1 text-2xl text-primary" /> <FontAwesomeIcon icon={faUserLock} className="mt-0.5 text-xl text-yellow" />
<span className="flex flex-col"> <span className="flex flex-col">
Secret approval has been closed Secret approval has been closed
<span className="inline-block text-xs text-bunker-200"> <span className="inline-block text-xs text-mineshaft-300">
Closed by {statusChangeByEmail} Closed by {statusChangeByEmail}
</span> </span>
</span> </span>
</div> </div>
<div className="flex items-center space-x-6"> <Button
<Button onClick={() => handleSecretApprovalStatusChange("open")}
onClick={() => handleSecretApprovalStatusChange("open")} isLoading={isStatusChanging}
isLoading={isStatusChanging} variant="plain"
variant="outline_bg" colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faLockOpen} />} className="mr-4 text-yellow/60 hover:text-yellow"
> leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
Reopen request >
</Button> Reopen request
</div> </Button>
</div> </div>
); );
}; };

View File

@ -3,11 +3,12 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { useState } from "react"; import { useState } from "react";
import { import {
faCircleCheck,
faCircleXmark, faCircleXmark,
faExclamationTriangle, faExclamationTriangle,
faEye, faEye,
faEyeSlash, faEyeSlash,
faInfo, faInfoCircle,
faKey faKey
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -29,14 +30,14 @@ export type Props = {
}; };
const generateItemTitle = (op: CommitType) => { const generateItemTitle = (op: CommitType) => {
let text = { label: "", color: "" }; let text = { label: "", className: "" };
if (op === CommitType.CREATE) text = { label: "create", color: "#60DD00" }; if (op === CommitType.CREATE) text = { label: "create", className: "text-green-600" };
else if (op === CommitType.UPDATE) text = { label: "change", color: "#F8EB30" }; else if (op === CommitType.UPDATE) text = { label: "change", className: "text-yellow-600" };
else text = { label: "deletion", color: "#F83030" }; else text = { label: "deletion", className: "text-red-600" };
return ( return (
<div className="text-md pb-2 font-medium"> <div className="text-md pb-2 font-medium">
Request for <span style={{ color: text.color }}>secret {text.label}</span> Request for <span className={text.className}>secret {text.label}</span>
</div> </div>
); );
}; };
@ -68,15 +69,15 @@ export const SecretApprovalRequestChangeItem = ({
<div className="flex items-center px-1 py-1"> <div className="flex items-center px-1 py-1">
<div className="flex-grow">{generateItemTitle(op)}</div> <div className="flex-grow">{generateItemTitle(op)}</div>
{!hasMerged && isStale && ( {!hasMerged && isStale && (
<div className="flex items-center"> <div className="flex items-center text-mineshaft-300">
<FontAwesomeIcon icon={faInfo} className="text-sm text-primary-600" /> <FontAwesomeIcon icon={faInfoCircle} className="text-xs" />
<span className="ml-2 text-xs">Secret has been changed(stale)</span> <span className="ml-1 text-xs">Secret has been changed (stale)</span>
</div> </div>
)} )}
{hasMerged && hasConflict && ( {hasMerged && hasConflict && (
<div className="flex items-center space-x-2 text-sm text-bunker-300"> <div className="flex items-center space-x-1 text-xs text-bunker-300">
<Tooltip content="Merge Conflict"> <Tooltip content="Merge Conflict">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red-700" /> <FontAwesomeIcon icon={faExclamationTriangle} className="text-xs text-red" />
</Tooltip> </Tooltip>
<div>{generateConflictText(op)}</div> <div>{generateConflictText(op)}</div>
</div> </div>
@ -95,7 +96,7 @@ export const SecretApprovalRequestChangeItem = ({
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Key</div> <div className="text-sm font-medium text-mineshaft-300">Key</div>
<div className="text-sm">{secretVersion?.secretKey} </div> <p className="max-w-lg break-words text-sm">{secretVersion?.secretKey}</p>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Value</div> <div className="text-sm font-medium text-mineshaft-300">Value</div>
@ -147,7 +148,7 @@ export const SecretApprovalRequestChangeItem = ({
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Comment</div> <div className="text-sm font-medium text-mineshaft-300">Comment</div>
<div className="max-h-[5rem] overflow-y-auto text-sm"> <div className="thin-scrollbar max-h-[5rem] max-w-[34rem] overflow-y-auto break-words text-sm xl:max-w-[28rem]">
{secretVersion?.secretComment || ( {secretVersion?.secretComment || (
<span className="text-sm text-mineshaft-300">-</span> <span className="text-sm text-mineshaft-300">-</span>
)}{" "} )}{" "}
@ -186,15 +187,27 @@ export const SecretApprovalRequestChangeItem = ({
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500" className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
> >
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" /> <FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div> <Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.key}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.key}
</div>
</Tooltip>
</Tag> </Tag>
<Tag <Tag
size="xs" size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1" className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
> >
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap"> <Tooltip
{el.value} className="max-w-lg whitespace-normal break-words"
</div> content={el.value}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tooltip>
</Tag> </Tag>
</div> </div>
))} ))}
@ -215,13 +228,13 @@ export const SecretApprovalRequestChangeItem = ({
<div className="mb-4 flex flex-row justify-between"> <div className="mb-4 flex flex-row justify-between">
<span className="text-md font-medium">New Secret</span> <span className="text-md font-medium">New Secret</span>
<div className="rounded-full bg-green-600 px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium"> <div className="rounded-full bg-green-600 px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" /> <FontAwesomeIcon icon={faCircleCheck} className="pr-1 text-white" />
New New
</div> </div>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Key</div> <div className="text-sm font-medium text-mineshaft-300">Key</div>
<div className="text-sm">{newVersion?.secretKey} </div> <div className="max-w-md break-words text-sm">{newVersion?.secretKey} </div>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Value</div> <div className="text-sm font-medium text-mineshaft-300">Value</div>
@ -273,7 +286,7 @@ export const SecretApprovalRequestChangeItem = ({
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Comment</div> <div className="text-sm font-medium text-mineshaft-300">Comment</div>
<div className="max-h-[5rem] overflow-y-auto text-sm"> <div className="thin-scrollbar max-h-[5rem] max-w-[34rem] overflow-y-auto break-words text-sm xl:max-w-[28rem]">
{newVersion?.secretComment || ( {newVersion?.secretComment || (
<span className="text-sm text-mineshaft-300">-</span> <span className="text-sm text-mineshaft-300">-</span>
)}{" "} )}{" "}
@ -281,15 +294,15 @@ export const SecretApprovalRequestChangeItem = ({
</div> </div>
<div className="mb-2"> <div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Tags</div> <div className="text-sm font-medium text-mineshaft-300">Tags</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-y-2">
{(newVersion?.tags?.length ?? 0) ? ( {(newVersion?.tags?.length ?? 0) ? (
newVersion?.tags?.map(({ slug, id: tagId, color }) => ( newVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-1.5 border border-mineshaft-500 bg-mineshaft-800"
key={`${newVersion.id}-${tagId}`} key={`${newVersion.id}-${tagId}`}
> >
<div <div
className="h-3 w-3 rounded-full" className="h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{slug}</div> <div className="text-sm">{slug}</div>
@ -311,15 +324,27 @@ export const SecretApprovalRequestChangeItem = ({
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500" className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
> >
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" /> <FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div> <Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.key}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.key}
</div>
</Tooltip>
</Tag> </Tag>
<Tag <Tag
size="xs" size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1" className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
> >
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap"> <Tooltip
{el.value} className="max-w-lg whitespace-normal break-words"
</div> content={el.value}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tooltip>
</Tag> </Tag>
</div> </div>
))} ))}

View File

@ -3,12 +3,12 @@ import { Controller, useForm } from "react-hook-form";
import { import {
faAngleDown, faAngleDown,
faArrowLeft, faArrowLeft,
faCheckCircle, faBan,
faCircle, faCheck,
faCodeBranch, faCodeBranch,
faComment, faComment,
faFolder, faFolder,
faXmarkCircle faHourglass
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -26,6 +26,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
FormControl, FormControl,
GenericFieldLabel,
IconButton, IconButton,
TextArea, TextArea,
Tooltip Tooltip
@ -81,10 +82,10 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
const getReviewedStatusSymbol = (status?: ApprovalStatus) => { const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
if (status === ApprovalStatus.APPROVED) if (status === ApprovalStatus.APPROVED)
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />; return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
if (status === ApprovalStatus.REJECTED) if (status === ApprovalStatus.REJECTED)
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />; return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />; return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
}; };
type Props = { type Props = {
@ -223,8 +224,8 @@ export const SecretApprovalRequestChanges = ({
const hasMerged = secretApprovalRequestDetails?.hasMerged; const hasMerged = secretApprovalRequestDetails?.hasMerged;
return ( return (
<div className="flex space-x-6"> <div className="flex flex-col space-x-6 lg:flex-row">
<div className="flex-grow"> <div className="flex-1 lg:max-w-[calc(100%-17rem)]">
<div className="sticky top-0 z-20 flex items-center space-x-4 bg-bunker-800 pb-6 pt-2"> <div className="sticky top-0 z-20 flex items-center space-x-4 bg-bunker-800 pb-6 pt-2">
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}> <IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faArrowLeft} />
@ -242,17 +243,17 @@ export const SecretApprovalRequestChanges = ({
: secretApprovalRequestDetails.status} : secretApprovalRequestDetails.status}
</span> </span>
</div> </div>
<div className="flex-grow flex-col"> <div className="-mt-0.5 flex-grow flex-col">
<div className="text-xl"> <div className="text-xl">
{generateCommitText( {generateCommitText(
secretApprovalRequestDetails.commits, secretApprovalRequestDetails.commits,
secretApprovalRequestDetails.isReplicated secretApprovalRequestDetails.isReplicated
)} )}
</div> </div>
<div className="flex items-center space-x-2 text-xs text-gray-400"> <span className="-mt-1 flex items-center space-x-2 text-xs text-gray-400">
By {secretApprovalRequestDetails?.committerUser?.firstName} ( By {secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email}) {secretApprovalRequestDetails?.committerUser?.email})
</div> </span>
</div> </div>
{!hasMerged && {!hasMerged &&
secretApprovalRequestDetails.status === "open" && secretApprovalRequestDetails.status === "open" &&
@ -262,7 +263,10 @@ export const SecretApprovalRequestChanges = ({
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
> >
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}> <Button
colorSchema="secondary"
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
>
Review Review
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -279,82 +283,87 @@ export const SecretApprovalRequestChanges = ({
{...field} {...field}
placeholder="Leave a comment..." placeholder="Leave a comment..."
reSize="none" reSize="none"
className="text-md mt-2 h-40 border border-mineshaft-600 bg-bunker-800" className="text-md mt-2 h-40 border border-mineshaft-600 bg-mineshaft-800 placeholder:text-mineshaft-400"
/> />
</FormControl> </FormControl>
)} )}
/> />
<Controller <div className="flex justify-between">
control={control} <Controller
name="status" control={control}
defaultValue={ApprovalStatus.APPROVED} name="status"
render={({ field, fieldState: { error } }) => ( defaultValue={ApprovalStatus.APPROVED}
<FormControl errorText={error?.message} isError={Boolean(error)}> render={({ field, fieldState: { error } }) => (
<RadioGroup <FormControl
value={field.value} className="mb-0"
onValueChange={field.onChange} errorText={error?.message}
className="mb-4 space-y-2" isError={Boolean(error)}
aria-label="Status"
> >
<div className="flex items-center gap-2"> <RadioGroup
<RadioGroupItem value={field.value}
id="approve" onValueChange={field.onChange}
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500" className="space-y-2"
value={ApprovalStatus.APPROVED} aria-label="Status"
aria-labelledby="approve-label" >
> <div className="flex items-center gap-2">
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" /> <RadioGroupItem
</RadioGroupItem> id="approve"
<span className="h-4 w-4 rounded-full border border-gray-400 text-green focus:ring-2 focus:ring-mineshaft-500"
id="approve-label" value={ApprovalStatus.APPROVED}
className="cursor-pointer" aria-labelledby="approve-label"
onClick={() => field.onChange(ApprovalStatus.APPROVED)} >
onKeyDown={(e) => { <RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
if (e.key === "Enter" || e.key === " ") { </RadioGroupItem>
e.preventDefault(); <span
field.onChange(ApprovalStatus.APPROVED); id="approve-label"
} className="cursor-pointer"
}} onClick={() => field.onChange(ApprovalStatus.APPROVED)}
tabIndex={0} onKeyDown={(e) => {
role="button" if (e.key === "Enter" || e.key === " ") {
> e.preventDefault();
Approve field.onChange(ApprovalStatus.APPROVED);
</span> }
</div> }}
<div className="flex items-center gap-2"> tabIndex={0}
<RadioGroupItem role="button"
id="reject" >
className="h-4 w-4 rounded-full border border-gray-300 text-red focus:ring-2 focus:ring-mineshaft-500" Approve
value={ApprovalStatus.REJECTED} </span>
aria-labelledby="reject-label" </div>
> <div className="flex items-center gap-2">
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" /> <RadioGroupItem
</RadioGroupItem> id="reject"
<span className="h-4 w-4 rounded-full border border-gray-400 text-red focus:ring-2 focus:ring-mineshaft-500"
id="reject-label" value={ApprovalStatus.REJECTED}
className="cursor-pointer" aria-labelledby="reject-label"
onClick={() => field.onChange(ApprovalStatus.REJECTED)} >
onKeyDown={(e) => { <RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
if (e.key === "Enter" || e.key === " ") { </RadioGroupItem>
e.preventDefault(); <span
field.onChange(ApprovalStatus.REJECTED); id="reject-label"
} className="cursor-pointer"
}} onClick={() => field.onChange(ApprovalStatus.REJECTED)}
tabIndex={0} onKeyDown={(e) => {
role="button" if (e.key === "Enter" || e.key === " ") {
> e.preventDefault();
Reject field.onChange(ApprovalStatus.REJECTED);
</span> }
</div> }}
</RadioGroup> tabIndex={0}
</FormControl> role="button"
)} >
/> Reject
<div className="flex justify-end"> </span>
</div>
</RadioGroup>
</FormControl>
)}
/>
<Button <Button
type="submit" type="submit"
isLoading={isApproving || isRejecting || isSubmitting} isLoading={isApproving || isRejecting || isSubmitting}
variant="outline_bg" variant="outline_bg"
className="mt-auto h-min"
> >
Submit Review Submit Review
</Button> </Button>
@ -371,14 +380,14 @@ export const SecretApprovalRequestChanges = ({
<div className="text-sm text-bunker-300"> <div className="text-sm text-bunker-300">
A secret import in A secret import in
<p <p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300" className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }} style={{ padding: "2px 4px" }}
> >
{secretApprovalRequestDetails?.environment} {secretApprovalRequestDetails?.environment}
</p> </p>
<div className="mr-2 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2"> <div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1"> <p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" /> <FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p> </p>
<Tooltip content={approvalSecretPath}> <Tooltip content={approvalSecretPath}>
<p <p
@ -391,14 +400,14 @@ export const SecretApprovalRequestChanges = ({
</div> </div>
has pending changes to be accepted from its source at{" "} has pending changes to be accepted from its source at{" "}
<p <p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300" className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }} style={{ padding: "2px 4px" }}
> >
{replicatedImport?.importEnv?.slug} {replicatedImport?.importEnv?.slug}
</p> </p>
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2"> <div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1"> <p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" /> <FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p> </p>
<Tooltip content={replicatedImport?.importPath}> <Tooltip content={replicatedImport?.importPath}>
<p <p
@ -415,14 +424,14 @@ export const SecretApprovalRequestChanges = ({
<div className="text-sm text-bunker-300"> <div className="text-sm text-bunker-300">
<p className="inline">Secret(s) in</p> <p className="inline">Secret(s) in</p>
<p <p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300" className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }} style={{ padding: "2px 4px" }}
> >
{secretApprovalRequestDetails?.environment} {secretApprovalRequestDetails?.environment}
</p> </p>
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2"> <div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1"> <p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" /> <FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p> </p>
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}> <Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
<p <p
@ -463,7 +472,7 @@ export const SecretApprovalRequestChanges = ({
const reviewer = reviewedUsers?.[requiredApprover.userId]; const reviewer = reviewedUsers?.[requiredApprover.userId];
return ( return (
<div <div
className="flex w-full flex-col rounded-md bg-mineshaft-800 p-4" className="flex w-full flex-col rounded-md bg-mineshaft-800 p-4 text-sm text-mineshaft-100"
key={`required-approver-${requiredApprover.userId}`} key={`required-approver-${requiredApprover.userId}`}
> >
<div> <div>
@ -477,14 +486,16 @@ export const SecretApprovalRequestChanges = ({
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"} {reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
</span>{" "} </span>{" "}
the request on{" "} the request on{" "}
{format(new Date(secretApprovalRequestDetails.createdAt), "PPpp zzz")}. {format(
new Date(secretApprovalRequestDetails.createdAt),
"MM/dd/yyyy h:mm:ss aa"
)}
.
</div> </div>
{reviewer?.comment && ( {reviewer?.comment && (
<FormControl label="Comment" className="mb-0 mt-4"> <GenericFieldLabel label="Comment" className="mt-2 max-w-4xl break-words">
<TextArea value={reviewer.comment} isDisabled reSize="none"> {reviewer?.comment && reviewer.comment}
{reviewer?.comment && reviewer.comment} </GenericFieldLabel>
</TextArea>
</FormControl>
)} )}
</div> </div>
); );
@ -505,7 +516,7 @@ export const SecretApprovalRequestChanges = ({
/> />
</div> </div>
</div> </div>
<div className="sticky top-0 w-1/5 cursor-default pt-4" style={{ minWidth: "240px" }}> <div className="sticky top-0 z-[51] w-1/5 cursor-default pt-2" style={{ minWidth: "240px" }}>
<div className="text-sm text-bunker-300">Reviewers</div> <div className="text-sm text-bunker-300">Reviewers</div>
<div className="mt-2 flex flex-col space-y-2 text-sm"> <div className="mt-2 flex flex-col space-y-2 text-sm">
{secretApprovalRequestDetails?.policy?.approvers {secretApprovalRequestDetails?.policy?.approvers
@ -526,17 +537,17 @@ export const SecretApprovalRequestChanges = ({
requiredApprover.lastName || "" requiredApprover.lastName || ""
}`} }`}
> >
<span>{requiredApprover?.email} </span> <span>{requiredApprover?.email}</span>
</Tooltip> </Tooltip>
<span className="text-red">*</span> <span className="text-red">*</span>
</div> </div>
<div> <div>
{reviewer?.comment && ( {reviewer?.comment && (
<Tooltip content={reviewer.comment}> <Tooltip className="max-w-lg break-words" content={reviewer.comment}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faComment} icon={faComment}
size="xs" size="xs"
className="mr-1 text-mineshaft-300" className="mr-1.5 text-mineshaft-300"
/> />
</Tooltip> </Tooltip>
)} )}

View File

@ -7,8 +7,13 @@ type Props = {
export const OnePassSyncDestinationSection = ({ secretSync }: Props) => { export const OnePassSyncDestinationSection = ({ secretSync }: Props) => {
const { const {
destinationConfig: { vaultId } destinationConfig: { vaultId, valueLabel }
} = secretSync; } = secretSync;
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>; return (
<>
<GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>
<GenericFieldLabel label="Value Key">{valueLabel || "value"}</GenericFieldLabel>
</>
);
}; };

View File

@ -43,6 +43,7 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route' import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route' import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
import { Route as adminIntegrationsPageRouteImport } from './pages/admin/IntegrationsPage/route' import { Route as adminIntegrationsPageRouteImport } from './pages/admin/IntegrationsPage/route'
import { Route as adminEnvironmentPageRouteImport } from './pages/admin/EnvironmentPage/route'
import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route' import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route'
import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route' import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route'
import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route' import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route'
@ -607,6 +608,12 @@ const adminIntegrationsPageRouteRoute = adminIntegrationsPageRouteImport.update(
} as any, } as any,
) )
const adminEnvironmentPageRouteRoute = adminEnvironmentPageRouteImport.update({
id: '/environment',
path: '/environment',
getParentRoute: () => adminLayoutRoute,
} as any)
const adminEncryptionPageRouteRoute = adminEncryptionPageRouteImport.update({ const adminEncryptionPageRouteRoute = adminEncryptionPageRouteImport.update({
id: '/encryption', id: '/encryption',
path: '/encryption', path: '/encryption',
@ -2353,6 +2360,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof adminEncryptionPageRouteImport preLoaderRoute: typeof adminEncryptionPageRouteImport
parentRoute: typeof adminLayoutImport parentRoute: typeof adminLayoutImport
} }
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
path: '/environment'
fullPath: '/admin/environment'
preLoaderRoute: typeof adminEnvironmentPageRouteImport
parentRoute: typeof adminLayoutImport
}
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': { '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' id: '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
path: '/integrations' path: '/integrations'
@ -4484,6 +4498,7 @@ interface adminLayoutRouteChildren {
adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute
adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute
adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute
adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute
adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute
adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute
adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute
@ -4495,6 +4510,7 @@ const adminLayoutRouteChildren: adminLayoutRouteChildren = {
adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute, adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute,
adminCachingPageRouteRoute: adminCachingPageRouteRoute, adminCachingPageRouteRoute: adminCachingPageRouteRoute,
adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute, adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute,
adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute,
adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute, adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute,
adminMachineIdentitiesResourcesPageRouteRoute: adminMachineIdentitiesResourcesPageRouteRoute:
adminMachineIdentitiesResourcesPageRouteRoute, adminMachineIdentitiesResourcesPageRouteRoute,
@ -4697,6 +4713,7 @@ export interface FileRoutesByFullPath {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren
@ -4918,6 +4935,7 @@ export interface FileRoutesByTo {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute '/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute '/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute '/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute '/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren
@ -5139,6 +5157,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
@ -5371,6 +5390,7 @@ export interface FileRouteTypes {
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
| '/admin/environment'
| '/admin/integrations' | '/admin/integrations'
| '/cert-manager/$projectId' | '/cert-manager/$projectId'
| '/kms/$projectId' | '/kms/$projectId'
@ -5591,6 +5611,7 @@ export interface FileRouteTypes {
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
| '/admin/environment'
| '/admin/integrations' | '/admin/integrations'
| '/cert-manager/$projectId' | '/cert-manager/$projectId'
| '/kms/$projectId' | '/kms/$projectId'
@ -5810,6 +5831,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout/authentication' | '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
| '/_authenticate/_inject-org-details/admin/_admin-layout/caching' | '/_authenticate/_inject-org-details/admin/_admin-layout/caching'
| '/_authenticate/_inject-org-details/admin/_admin-layout/encryption' | '/_authenticate/_inject-org-details/admin/_admin-layout/encryption'
| '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations' | '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
@ -6267,6 +6289,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/admin/_admin-layout/authentication", "/_authenticate/_inject-org-details/admin/_admin-layout/authentication",
"/_authenticate/_inject-org-details/admin/_admin-layout/caching", "/_authenticate/_inject-org-details/admin/_admin-layout/caching",
"/_authenticate/_inject-org-details/admin/_admin-layout/encryption", "/_authenticate/_inject-org-details/admin/_admin-layout/encryption",
"/_authenticate/_inject-org-details/admin/_admin-layout/environment",
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations", "/_authenticate/_inject-org-details/admin/_admin-layout/integrations",
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities", "/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities",
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations", "/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations",
@ -6309,6 +6332,10 @@ export const routeTree = rootRoute
"filePath": "admin/EncryptionPage/route.tsx", "filePath": "admin/EncryptionPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout" "parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
}, },
"/_authenticate/_inject-org-details/admin/_admin-layout/environment": {
"filePath": "admin/EnvironmentPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
},
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations": { "/_authenticate/_inject-org-details/admin/_admin-layout/integrations": {
"filePath": "admin/IntegrationsPage/route.tsx", "filePath": "admin/IntegrationsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout" "parent": "/_authenticate/_inject-org-details/admin/_admin-layout"

View File

@ -8,6 +8,7 @@ const adminRoute = route("/admin", [
index("admin/GeneralPage/route.tsx"), index("admin/GeneralPage/route.tsx"),
route("/encryption", "admin/EncryptionPage/route.tsx"), route("/encryption", "admin/EncryptionPage/route.tsx"),
route("/authentication", "admin/AuthenticationPage/route.tsx"), route("/authentication", "admin/AuthenticationPage/route.tsx"),
route("/environment", "admin/EnvironmentPage/route.tsx"),
route("/integrations", "admin/IntegrationsPage/route.tsx"), route("/integrations", "admin/IntegrationsPage/route.tsx"),
route("/caching", "admin/CachingPage/route.tsx"), route("/caching", "admin/CachingPage/route.tsx"),
route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"), route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"),