Compare commits

..

37 Commits

Author SHA1 Message Date
0191eb48f3 Merge pull request #3974 from Infisical/fix-email-invite-notifications
Improve + fix invitation reminder logic
2025-07-07 14:47:50 -04:00
9d39910152 Minor fix to prevent setting lastInvitedAt for invitees who weren’t actually sent an invitation 2025-07-07 15:35:49 -03:00
9137fa4ca5 Improve + fix invitation reminder logic 2025-07-07 13:31:20 -04:00
78da7ec343 Merge pull request #3972 from Infisical/fix/telemetryOrgIdentify
feat(telemetry): improve Posthog org identity logic
2025-07-07 10:15:59 -03:00
a678ebb4ac Fix Cloud telemetry queue initialization 2025-07-07 10:10:30 -03:00
83dd38db49 feat(telemetry): reduce TELEMETRY_AGGREGATED_KEY_EXP to 10 mins and avoid sending org identitfy events for batch events on sendPostHogEvents 2025-07-07 08:36:15 -03:00
06f5af1200 Merge pull request #3890 from Infisical/daniel/sso-endpoints-docs
docs(api-reference/organizations): document SSO configuration endpoints
2025-07-04 05:33:52 +04:00
f903e5b3d4 Update saml-router.ts 2025-07-04 05:23:05 +04:00
c6f8915d3f Update saml-config-service.ts 2025-07-04 05:21:54 +04:00
65b1354ef1 fix: remove undefined return type from get saml endpoint 2025-07-04 05:07:54 +04:00
cda8579ca4 fix: requested changes 2025-07-04 04:51:14 +04:00
1b1acdcb0b Merge pull request #3917 from Infisical/cli-add-bitbucket-platform
Add BitBucket platform to secret scanning
2025-07-03 20:06:48 -04:00
a8f08730a1 Merge pull request #3908 from Infisical/fix/ui-small-catches
feat: added autoplay to loading lottie and fixed tooltip in project select
2025-07-03 19:35:59 -04:00
9af9050aa2 Merge pull request #3921 from Infisical/misc/allow-users-with-create-identity-to-invite-no-access
misc: allow users with create permission to add identities with no access
2025-07-03 19:27:04 -04:00
cc564119e0 misc: allow users with create permission to add identities with no access 2025-07-04 04:24:15 +08:00
189b0dd5ee Merge pull request #3920 from Infisical/fix-secret-sync-remove-and-import-audit-logs
fix(secret-syncs): pass audit log info from import/delete secrets for sync endpoint
2025-07-03 13:02:04 -07:00
9cbef2c07b fix: pass audit log info from import/delete secrets for sync endpoint 2025-07-03 12:37:28 -07:00
9a960a85cd Merge pull request #3905 from Infisical/password-reset-ui
improvement(password-reset): re-vamp password reset flow pages/steps to match login
2025-07-03 10:31:58 -07:00
2a9e31d305 Few nits 2025-07-03 13:11:53 -04:00
fb2f1731dd Merge branch 'main' into password-reset-ui 2025-07-03 13:02:48 -04:00
defb66ce65 Merge pull request #3918 from Infisical/revert-3901-revert-3875-ENG-3009-test
Undo Environment Variables Override PR Revert + SSO Fix
2025-07-03 12:18:10 -04:00
a3d06fdf1b misc: added reference to server admin 2025-07-03 21:21:06 +08:00
9049c441d6 Greptile review fix 2025-07-03 03:18:37 -04:00
51ecc9dfa0 Merge branch 'revert-3899-revert-3896-misc/final-changes-for-self-serve-en' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 03:08:42 -04:00
13c9879fb6 Merge branch 'main' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 02:54:28 -04:00
=
7ab67db84d feat: fixed black color in tooltip 2025-07-03 01:18:52 +05:30
=
3a17281e37 feat: resolved tooltip overflow 2025-07-03 00:41:47 +05:30
=
abfe185a5b feat: added autoplay to loading lottie and fixed tooltip in project select 2025-07-02 22:13:37 +05:30
f6c10683a5 misc: add sync for passport middleware 2025-07-02 20:48:24 +08:00
69157cb912 improvement: add period 2025-07-01 19:23:13 -07:00
44eb761d5b improvement: re-vamp password reset flow pages/steps to match login design 2025-07-01 19:19:27 -07:00
abbf541c9f Docs link on UI 2025-07-01 19:01:39 -04:00
fcdd121a58 Docs & UI update 2025-07-01 18:46:06 -04:00
5bfd92bf8d Revert "Revert "feat(super-admin): Environment Overrides"" 2025-07-01 17:43:52 -04:00
45af2c0b49 Revert "Revert "misc: updated sidebar name"" 2025-07-01 17:42:54 -04:00
13d2cbd8b0 Update docs.json 2025-07-01 02:09:14 +04:00
abfc5736fd docs(api-reference/organizations): document SSO configuration endpoints 2025-07-01 02:05:53 +04:00
64 changed files with 1451 additions and 386 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

@ -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.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().defaultTo(knex.fn.now()).alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().alter();
});
}
}

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

@ -17,6 +17,7 @@ import { z } from "zod";
import { LdapGroupMapsSchema } from "@app/db/schemas"; import { LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { ApiDocsTags, LdapSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -132,10 +133,18 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Get LDAP config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(LdapSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -172,23 +181,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Create LDAP config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string().trim(), organizationId: z.string().trim().describe(LdapSso.CREATE_CONFIG.organizationId),
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.CREATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.CREATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim().default("uidNumber"), uniqueUserAttribute: z.string().trim().default("uidNumber").describe(LdapSso.CREATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.searchBase),
searchFilter: z.string().trim().default("(uid={{username}})"), searchFilter: z.string().trim().default("(uid={{username}})").describe(LdapSso.CREATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.groupSearchBase),
groupSearchFilter: z groupSearchFilter: z
.string() .string()
.trim() .trim()
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"), .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))")
caCert: z.string().trim().default("") .describe(LdapSso.CREATE_CONFIG.groupSearchFilter),
caCert: z.string().trim().default("").describe(LdapSso.CREATE_CONFIG.caCert)
}), }),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
@ -214,23 +232,31 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Update LDAP config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
isActive: z.boolean(), isActive: z.boolean().describe(LdapSso.UPDATE_CONFIG.isActive),
url: z.string().trim(), url: z.string().trim().describe(LdapSso.UPDATE_CONFIG.url),
bindDN: z.string().trim(), bindDN: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindDN),
bindPass: z.string().trim(), bindPass: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim(), uniqueUserAttribute: z.string().trim().describe(LdapSso.UPDATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim(), searchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchBase),
searchFilter: z.string().trim(), searchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim(), groupSearchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchBase),
groupSearchFilter: z.string().trim(), groupSearchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchFilter),
caCert: z.string().trim() caCert: z.string().trim().describe(LdapSso.UPDATE_CONFIG.caCert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(LdapSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedLdapConfigSchema 200: SanitizedLdapConfigSchema
} }

View File

@ -13,6 +13,7 @@ import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas"; import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types"; import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { ApiDocsTags, OidcSSo } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -153,10 +154,18 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Get OIDC config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
orgSlug: z.string().trim() organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
@ -180,9 +189,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { orgSlug } = req.query;
const oidc = await server.services.oidc.getOidc({ const oidc = await server.services.oidc.getOidc({
orgSlug, organizationId: req.query.organizationId,
type: "external", type: "external",
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@ -200,8 +208,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Update OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -216,22 +232,26 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
discoveryURL: z.string().trim(), .describe(OidcSSo.UPDATE_CONFIG.allowedEmailDomains),
configurationType: z.nativeEnum(OIDCConfigurationType), discoveryURL: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.discoveryURL),
issuer: z.string().trim(), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.UPDATE_CONFIG.configurationType),
authorizationEndpoint: z.string().trim(), issuer: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.issuer),
jwksUri: z.string().trim(), authorizationEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.authorizationEndpoint),
tokenEndpoint: z.string().trim(), jwksUri: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.jwksUri),
userinfoEndpoint: z.string().trim(), tokenEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.tokenEndpoint),
clientId: z.string().trim(), userinfoEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.userinfoEndpoint),
clientSecret: z.string().trim(), clientId: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientId),
isActive: z.boolean(), clientSecret: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientSecret),
manageGroupMemberships: z.boolean().optional(), isActive: z.boolean().describe(OidcSSo.UPDATE_CONFIG.isActive),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional() manageGroupMemberships: z.boolean().optional().describe(OidcSSo.UPDATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.describe(OidcSSo.UPDATE_CONFIG.jwtSignatureAlgorithm)
}) })
.partial() .partial()
.merge(z.object({ orgSlug: z.string() })), .merge(z.object({ organizationId: z.string().describe(OidcSSo.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedOidcConfigSchema.pick({ 200: SanitizedOidcConfigSchema.pick({
id: true, id: true,
@ -267,8 +287,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Create OIDC config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
allowedEmailDomains: z allowedEmailDomains: z
@ -283,23 +311,34 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",") .split(",")
.map((id) => id.trim()) .map((id) => id.trim())
.join(", "); .join(", ");
}), })
configurationType: z.nativeEnum(OIDCConfigurationType), .describe(OidcSSo.CREATE_CONFIG.allowedEmailDomains),
issuer: z.string().trim().optional().default(""), configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.CREATE_CONFIG.configurationType),
discoveryURL: z.string().trim().optional().default(""), issuer: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.issuer),
authorizationEndpoint: z.string().trim().optional().default(""), discoveryURL: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.discoveryURL),
jwksUri: z.string().trim().optional().default(""), authorizationEndpoint: z
tokenEndpoint: z.string().trim().optional().default(""), .string()
userinfoEndpoint: z.string().trim().optional().default(""), .trim()
clientId: z.string().trim(), .optional()
clientSecret: z.string().trim(), .default("")
isActive: z.boolean(), .describe(OidcSSo.CREATE_CONFIG.authorizationEndpoint),
orgSlug: z.string().trim(), jwksUri: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.jwksUri),
manageGroupMemberships: z.boolean().optional().default(false), tokenEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.tokenEndpoint),
userinfoEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.userinfoEndpoint),
clientId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientId),
clientSecret: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientSecret),
isActive: z.boolean().describe(OidcSSo.CREATE_CONFIG.isActive),
organizationId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.organizationId),
manageGroupMemberships: z
.boolean()
.optional()
.default(false)
.describe(OidcSSo.CREATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm) .nativeEnum(OIDCJWTSignatureAlgorithm)
.optional() .optional()
.default(OIDCJWTSignatureAlgorithm.RS256) .default(OIDCJWTSignatureAlgorithm.RS256)
.describe(OidcSSo.CREATE_CONFIG.jwtSignatureAlgorithm)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@ -13,6 +13,7 @@ import { FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -149,8 +150,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
firstName, firstName,
lastName: lastName as string, lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string, orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata metadata: userMetadata
}); });
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
@ -262,25 +263,31 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Get SAML config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({ querystring: z.object({
organizationId: z.string().trim() organizationId: z.string().trim().describe(SamlSso.GET_CONFIG.organizationId)
}), }),
response: { response: {
200: z 200: z.object({
.object({ id: z.string(),
id: z.string(), organization: z.string(),
organization: z.string(), orgId: z.string(),
orgId: z.string(), authProvider: z.string(),
authProvider: z.string(), isActive: z.boolean(),
isActive: z.boolean(), entryPoint: z.string(),
entryPoint: z.string(), issuer: z.string(),
issuer: z.string(), cert: z.string(),
cert: z.string(), lastUsed: z.date().nullable().optional()
lastUsed: z.date().nullable().optional() })
})
.optional()
} }
}, },
handler: async (req) => { handler: async (req) => {
@ -302,15 +309,23 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Create SAML config",
security: [
{
bearerAuth: []
}
],
body: z.object({ body: z.object({
organizationId: z.string(), organizationId: z.string().trim().describe(SamlSso.CREATE_CONFIG.organizationId),
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.CREATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert)
}), }),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
@ -341,18 +356,26 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Update SAML config",
security: [
{
bearerAuth: []
}
],
body: z body: z
.object({ .object({
authProvider: z.nativeEnum(SamlProviders), authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.UPDATE_CONFIG.authProvider),
isActive: z.boolean(), isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive),
entryPoint: z.string(), entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint),
issuer: z.string(), issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer),
cert: z.string() cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert)
}) })
.partial() .partial()
.merge(z.object({ organizationId: z.string() })), .merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })),
response: { response: {
200: SanitizedSamlConfigSchema 200: SanitizedSamlConfigSchema
} }

View File

@ -107,34 +107,26 @@ export const oidcConfigServiceFactory = ({
kmsService kmsService
}: TOidcConfigServiceFactoryDep) => { }: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => { const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug }); const oidcCfg = await oidcConfigDAL.findOne({
if (!org) { orgId: dto.organizationId
});
if (!oidcCfg) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`, message: `OIDC configuration for organization with ID '${dto.organizationId}' not found`
name: "OrgNotFound"
}); });
} }
if (dto.type === "external") { if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
dto.actor, dto.actor,
dto.actorId, dto.actorId,
org.id, dto.organizationId,
dto.actorAuthMethod, dto.actorAuthMethod,
dto.actorOrgId dto.actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
} }
const oidcCfg = await oidcConfigDAL.findOne({
orgId: org.id
});
if (!oidcCfg) {
throw new NotFoundError({
message: `OIDC configuration for organization with slug '${dto.orgSlug}' not found`
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: oidcCfg.orgId orgId: oidcCfg.orgId
@ -465,7 +457,7 @@ export const oidcConfigServiceFactory = ({
}; };
const updateOidcCfg = async ({ const updateOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -484,13 +476,11 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => { }: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found` message: `Organization with ID '${organizationId}' not found`
}); });
} }
@ -555,7 +545,7 @@ export const oidcConfigServiceFactory = ({
}; };
const createOidcCfg = async ({ const createOidcCfg = async ({
orgSlug, organizationId,
allowedEmailDomains, allowedEmailDomains,
configurationType, configurationType,
discoveryURL, discoveryURL,
@ -574,12 +564,10 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => { }: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({ const org = await orgDAL.findOne({ id: organizationId });
slug: orgSlug
});
if (!org) { if (!org) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found` message: `Organization with ID '${organizationId}' not found`
}); });
} }
@ -639,7 +627,7 @@ export const oidcConfigServiceFactory = ({
const oidcCfg = await getOidc({ const oidcCfg = await getOidc({
type: "internal", type: "internal",
orgSlug organizationId: org.id
}); });
if (!oidcCfg || !oidcCfg.isActive) { if (!oidcCfg || !oidcCfg.isActive) {

View File

@ -26,11 +26,11 @@ export type TOidcLoginDTO = {
export type TGetOidcCfgDTO = export type TGetOidcCfgDTO =
| ({ | ({
type: "external"; type: "external";
orgSlug: string; organizationId: string;
} & TGenericPermission) } & TGenericPermission)
| { | {
type: "internal"; type: "internal";
orgSlug: string; organizationId: string;
}; };
export type TCreateOidcCfgDTO = { export type TCreateOidcCfgDTO = {
@ -45,7 +45,7 @@ export type TCreateOidcCfgDTO = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission; } & TGenericPermission;
@ -62,7 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
manageGroupMemberships: boolean; manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> & }> &

View File

@ -148,10 +148,18 @@ export const samlConfigServiceFactory = ({
let samlConfig: TSamlConfigs | undefined; let samlConfig: TSamlConfigs | undefined;
if (dto.type === "org") { if (dto.type === "org") {
samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId });
if (!samlConfig) return; if (!samlConfig) {
throw new NotFoundError({
message: `SAML configuration for organization with ID '${dto.orgId}' not found`
});
}
} else if (dto.type === "orgSlug") { } else if (dto.type === "orgSlug") {
const org = await orgDAL.findOne({ slug: dto.orgSlug }); const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) return; if (!org) {
throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`
});
}
samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); samlConfig = await samlConfigDAL.findOne({ orgId: org.id });
} else if (dto.type === "ssoId") { } else if (dto.type === "ssoId") {
// TODO: // TODO:

View File

@ -61,20 +61,17 @@ export type TSamlLoginDTO = {
export type TSamlConfigServiceFactory = { export type TSamlConfigServiceFactory = {
createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>; createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>;
updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>; updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>;
getSaml: (arg: TGetSamlCfgDTO) => Promise< getSaml: (arg: TGetSamlCfgDTO) => Promise<{
| { id: string;
id: string; organization: string;
organization: string; orgId: string;
orgId: string; authProvider: string;
authProvider: string; isActive: boolean;
isActive: boolean; entryPoint: string;
entryPoint: string; issuer: string;
issuer: string; cert: string;
cert: string; lastUsed: Date | null | undefined;
lastUsed: Date | null | undefined; }>;
}
| undefined
>;
samlLogin: (arg: TSamlLoginDTO) => Promise<{ samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;

View File

@ -66,7 +66,10 @@ export enum ApiDocsTags {
KmsKeys = "KMS Keys", KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption", KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing", KmsSigning = "KMS Signing",
SecretScanning = "Secret Scanning" SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO",
SamlSso = "SAML SSO",
LdapSso = "LDAP SSO"
} }
export const GROUPS = { export const GROUPS = {
@ -2662,3 +2665,113 @@ export const SecretScanningConfigs = {
content: "The contents of the Secret Scanning Configuration file." content: "The contents of the Secret Scanning Configuration file."
} }
}; };
export const OidcSSo = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the OIDC config for."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the OIDC config for.",
allowedEmailDomains:
"A list of allowed email domains that users can use to authenticate with. This field is comma separated. Example: 'example.com,acme.com'",
discoveryURL: "The URL of the OIDC discovery endpoint.",
configurationType: "The configuration type to use for the OIDC configuration.",
issuer:
"The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.",
authorizationEndpoint:
"The endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.",
jwksUri: "The URL of the OIDC JWKS endpoint.",
tokenEndpoint: "The token endpoint to use for OIDC token exchange.",
userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.",
clientId: "The client ID to use for OIDC authentication.",
clientSecret: "The client secret to use for OIDC authentication.",
isActive: "Whether to enable or disable this OIDC configuration.",
manageGroupMemberships:
"Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.",
jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the OIDC config for.",
allowedEmailDomains:
"A list of allowed email domains that users can use to authenticate with. This field is comma separated.",
discoveryURL: "The URL of the OIDC discovery endpoint.",
configurationType: "The configuration type to use for the OIDC configuration.",
issuer:
"The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.",
authorizationEndpoint:
"The authorization endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.",
jwksUri: "The URL of the OIDC JWKS endpoint.",
tokenEndpoint: "The token endpoint to use for OIDC token exchange.",
userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.",
clientId: "The client ID to use for OIDC authentication.",
clientSecret: "The client secret to use for OIDC authentication.",
isActive: "Whether to enable or disable this OIDC configuration.",
manageGroupMemberships:
"Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.",
jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification."
}
};
export const SamlSso = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the SAML config for."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the SAML config for.",
authProvider: "Authentication provider to use for SAML authentication.",
isActive: "Whether to enable or disable this SAML configuration.",
entryPoint:
"The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.",
issuer: "The SAML provider issuer URL or entity ID.",
cert: "The certificate to use for SAML authentication."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the SAML config for.",
authProvider: "Authentication provider to use for SAML authentication.",
isActive: "Whether to enable or disable this SAML configuration.",
entryPoint:
"The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.",
issuer: "The SAML provider issuer URL or entity ID.",
cert: "The certificate to use for SAML authentication."
}
};
export const LdapSso = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the LDAP config for."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the LDAP config for.",
isActive: "Whether to enable or disable this LDAP configuration.",
url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.",
bindDN:
"The distinguished name of the object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`",
bindPass: "The password to use along with Bind DN when performing the user search.",
searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`",
uniqueUserAttribute:
"The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber",
searchFilter:
"The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.",
groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`",
groupSearchFilter:
"The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.",
caCert: "The CA certificate to use when verifying the LDAP server certificate."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the LDAP config for.",
isActive: "Whether to enable or disable this LDAP configuration.",
url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.",
bindDN:
"The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`",
bindPass: "The password to use along with Bind DN when performing the user search.",
uniqueUserAttribute:
"The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber",
searchFilter:
"The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.",
searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`",
groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`",
groupSearchFilter:
"The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.",
caCert: "The CA certificate to use when verifying the LDAP server certificate."
}
};

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

@ -300,6 +300,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission"; import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits"; import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { initializeOauthConfigSync } from "./v1/sso-router";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
@ -1910,6 +1911,7 @@ export const registerRoutes = async (
await hsmService.startService(); await hsmService.startService();
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.startCleanUp(); await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob(); await pkiSubscriberQueue.startDailyAutoRenewalJob();
@ -2046,6 +2048,16 @@ export const registerRoutes = async (
} }
} }
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
const oauthConfigSyncJob = await initializeOauthConfigSync();
if (oauthConfigSyncJob) {
cronJobs.push(oauthConfigSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", { server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL, user: userDAL,
kmipClient: kmipClientDAL kmipClient: kmipClientDAL

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

@ -382,7 +382,8 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
{ {
syncId, syncId,
destination, destination,
importBehavior importBehavior,
auditLogInfo: req.auditLogInfo
}, },
req.permission req.permission
)) as T; )) as T;
@ -415,7 +416,8 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
const secretSync = (await server.services.secretSync.triggerSecretSyncRemoveSecretsById( const secretSync = (await server.services.secretSync.triggerSecretSyncRemoveSecretsById(
{ {
syncId, syncId,
destination destination,
auditLogInfo: req.auditLogInfo
}, },
req.permission req.permission
)) as T; )) as T;

View File

@ -9,6 +9,7 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { CronJob } from "cron";
import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2"; import { Strategy as OAuth2Strategy } from "passport-oauth2";
@ -25,27 +26,14 @@ import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types"; import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export const registerSsoRouter = async (server: FastifyZodProvider) => { const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
let serverInstance: FastifyZodProvider | null = null;
export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
serverInstance = server;
const appCfg = getConfig(); const appCfg = getConfig();
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// passport oauth strategy for Google // passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN); const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
if (isGoogleOauthActive) { if (isGoogleOauthActive) {
@ -176,6 +164,49 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
) )
); );
} }
};
export const refreshOauthConfig = () => {
if (!serverInstance) {
logger.warn("Cannot refresh OAuth config: server instance not available");
return;
}
logger.info("Refreshing OAuth configuration...");
registerOauthMiddlewares(serverInstance);
};
export const initializeOauthConfigSync = async () => {
logger.info("Setting up background sync process for oauth configuration");
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", refreshOauthConfig);
job.start();
return job;
};
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
registerOauthMiddlewares(server);
server.route({ server.route({
url: "/redirect/google", url: "/redirect/google",

View File

@ -93,23 +93,25 @@ export const identityProjectServiceFactory = ({
projectId projectId
); );
const permissionBoundary = validatePrivilegeChangeOperation( if (requestedRoleChange !== ProjectMembershipRole.NoAccess) {
membership.shouldUseNewPrivilegeSystem, const permissionBoundary = validatePrivilegeChangeOperation(
ProjectPermissionIdentityActions.GrantPrivileges, membership.shouldUseNewPrivilegeSystem,
ProjectPermissionSub.Identity, ProjectPermissionIdentityActions.GrantPrivileges,
permission, ProjectPermissionSub.Identity,
rolePermission permission,
); rolePermission
if (!permissionBoundary.isValid) );
throw new PermissionBoundaryError({ if (!permissionBoundary.isValid)
message: constructPermissionErrorMessage( throw new PermissionBoundaryError({
"Failed to assign to role", message: constructPermissionErrorMessage(
membership.shouldUseNewPrivilegeSystem, "Failed to assign to role",
ProjectPermissionIdentityActions.GrantPrivileges, membership.shouldUseNewPrivilegeSystem,
ProjectPermissionSub.Identity ProjectPermissionIdentityActions.GrantPrivileges,
), ProjectPermissionSub.Identity
details: { missingPermissions: permissionBoundary.missingPermissions } ),
}); details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
} }
// validate custom roles input // validate custom roles input

View File

@ -69,23 +69,25 @@ export const identityServiceFactory = ({
orgId orgId
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
const permissionBoundary = validatePrivilegeChangeOperation( if (role !== OrgMembershipRole.NoAccess) {
membership.shouldUseNewPrivilegeSystem, const permissionBoundary = validatePrivilegeChangeOperation(
OrgPermissionIdentityActions.GrantPrivileges, membership.shouldUseNewPrivilegeSystem,
OrgPermissionSubjects.Identity, OrgPermissionIdentityActions.GrantPrivileges,
permission, OrgPermissionSubjects.Identity,
rolePermission permission,
); rolePermission
if (!permissionBoundary.isValid) );
throw new PermissionBoundaryError({ if (!permissionBoundary.isValid)
message: constructPermissionErrorMessage( throw new PermissionBoundaryError({
"Failed to create identity", message: constructPermissionErrorMessage(
membership.shouldUseNewPrivilegeSystem, "Failed to create identity",
OrgPermissionIdentityActions.GrantPrivileges, membership.shouldUseNewPrivilegeSystem,
OrgPermissionSubjects.Identity OrgPermissionIdentityActions.GrantPrivileges,
), OrgPermissionSubjects.Identity
details: { missingPermissions: permissionBoundary.missingPermissions } ),
}); details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@ -187,6 +189,7 @@ export const identityServiceFactory = ({
), ),
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
}); });
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }

View File

@ -122,8 +122,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
.orWhere((qb) => { .orWhere((qb) => {
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago // lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
void qb void qb
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneMonthAgo) .where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
.where(`${TableName.OrgMembership}.createdAt`, ">", oneWeekAgo); .where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
}); });
return memberships; return memberships;
@ -135,9 +135,22 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
} }
}; };
const updateLastInvitedAtByIds = async (membershipIds: string[]) => {
try {
if (membershipIds.length === 0) return;
await db(TableName.OrgMembership).whereIn("id", membershipIds).update({ lastInvitedAt: new Date() });
} catch (error) {
throw new DatabaseError({
error,
name: "Update last invited at by ids"
});
}
};
return { return {
...orgMembershipOrm, ...orgMembershipOrm,
findOrgMembershipById, findOrgMembershipById,
findRecentInvitedMemberships findRecentInvitedMemberships,
updateLastInvitedAtByIds
}; };
}; };

View File

@ -109,7 +109,12 @@ type TOrgServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
orgMembershipDAL: Pick< orgMembershipDAL: Pick<
TOrgMembershipDALFactory, TOrgMembershipDALFactory,
"findOrgMembershipById" | "findOne" | "findById" | "findRecentInvitedMemberships" | "updateById" | "findOrgMembershipById"
| "findOne"
| "findById"
| "findRecentInvitedMemberships"
| "updateById"
| "updateLastInvitedAtByIds"
>; >;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
@ -763,6 +768,10 @@ export const orgServiceFactory = ({
} }
}); });
await orgMembershipDAL.updateById(inviteeOrgMembership.id, {
lastInvitedAt: new Date()
});
return { signupToken: undefined }; return { signupToken: undefined };
}; };
@ -1433,6 +1442,7 @@ export const orgServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const orgCache: Record<string, { name: string; id: string } | undefined> = {}; const orgCache: Record<string, { name: string; id: string } | undefined> = {};
const notifiedUsers: string[] = [];
await Promise.all( await Promise.all(
invitedUsers.map(async (invitedUser) => { invitedUsers.map(async (invitedUser) => {
@ -1463,13 +1473,12 @@ export const orgServiceFactory = ({
callback_url: `${appCfg.SITE_URL}/signupinvite` callback_url: `${appCfg.SITE_URL}/signupinvite`
} }
}); });
notifiedUsers.push(invitedUser.id);
} }
await orgMembershipDAL.updateById(invitedUser.id, {
lastInvitedAt: new Date()
});
}) })
); );
await orgMembershipDAL.updateLastInvitedAtByIds(notifiedUsers);
}; };
return { return {

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

@ -71,6 +71,15 @@ export const telemetryQueueServiceFactory = ({
QueueName.TelemetryInstanceStats // just a job id QueueName.TelemetryInstanceStats // just a job id
); );
if (postHog) {
await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, {
jobId: QueueName.TelemetryInstanceStats,
repeat: { pattern: "0 0 * * *", utc: true }
});
}
};
const startAggregatedEventsJob = async () => {
// clear previous aggregated events job // clear previous aggregated events job
await queueService.stopRepeatableJob( await queueService.stopRepeatableJob(
QueueName.TelemetryAggregatedEvents, QueueName.TelemetryAggregatedEvents,
@ -80,11 +89,6 @@ export const telemetryQueueServiceFactory = ({
); );
if (postHog) { if (postHog) {
await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, {
jobId: QueueName.TelemetryInstanceStats,
repeat: { pattern: "0 0 * * *", utc: true }
});
// Start aggregated events job (runs every five minutes) // Start aggregated events job (runs every five minutes)
await queueService.queue(QueueName.TelemetryAggregatedEvents, QueueJobs.TelemetryAggregatedEvents, undefined, { await queueService.queue(QueueName.TelemetryAggregatedEvents, QueueJobs.TelemetryAggregatedEvents, undefined, {
jobId: QueueName.TelemetryAggregatedEvents, jobId: QueueName.TelemetryAggregatedEvents,
@ -102,6 +106,7 @@ export const telemetryQueueServiceFactory = ({
}); });
return { return {
startTelemetryCheck startTelemetryCheck,
startAggregatedEventsJob
}; };
}; };

View File

@ -14,7 +14,7 @@ export const TELEMETRY_SECRET_PROCESSED_KEY = "telemetry-secret-processed";
export const TELEMETRY_SECRET_OPERATIONS_KEY = "telemetry-secret-operations"; export const TELEMETRY_SECRET_OPERATIONS_KEY = "telemetry-secret-operations";
export const POSTHOG_AGGREGATED_EVENTS = [PostHogEventTypes.SecretPulled]; export const POSTHOG_AGGREGATED_EVENTS = [PostHogEventTypes.SecretPulled];
const TELEMETRY_AGGREGATED_KEY_EXP = 900; // 15mins const TELEMETRY_AGGREGATED_KEY_EXP = 600; // 10mins
// Bucket configuration // Bucket configuration
const TELEMETRY_BUCKET_COUNT = 30; const TELEMETRY_BUCKET_COUNT = 30;
@ -102,13 +102,6 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
const instanceType = licenseService.getInstanceType(); const instanceType = licenseService.getInstanceType();
// capture posthog only when its cloud or signup event happens in self-hosted // capture posthog only when its cloud or signup event happens in self-hosted
if (instanceType === InstanceType.Cloud || event.event === PostHogEventTypes.UserSignedUp) { if (instanceType === InstanceType.Cloud || event.event === PostHogEventTypes.UserSignedUp) {
if (event.organizationId) {
try {
postHog.groupIdentify({ groupType: "organization", groupKey: event.organizationId });
} catch (error) {
logger.error(error, "Failed to identify PostHog organization");
}
}
if (POSTHOG_AGGREGATED_EVENTS.includes(event.event)) { if (POSTHOG_AGGREGATED_EVENTS.includes(event.event)) {
const eventKey = createTelemetryEventKey(event.event, event.distinctId); const eventKey = createTelemetryEventKey(event.event, event.distinctId);
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
@ -122,6 +115,13 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
}) })
); );
} else { } else {
if (event.organizationId) {
try {
postHog.groupIdentify({ groupType: "organization", groupKey: event.organizationId });
} catch (error) {
logger.error(error, "Failed to identify PostHog organization");
}
}
postHog.capture({ postHog.capture({
event: event.event, event: event.event,
distinctId: event.distinctId, distinctId: event.distinctId,

View File

@ -0,0 +1,4 @@
---
title: "Create LDAP SSO Config"
openapi: "POST /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get LDAP SSO Config"
openapi: "GET /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update LDAP SSO Config"
openapi: "PATCH /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create OIDC Config"
openapi: "POST /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get OIDC Config"
openapi: "GET /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update OIDC Config"
openapi: "PATCH /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create SAML SSO Config"
openapi: "POST /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get SAML SSO Config"
openapi: "GET /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update SAML SSO Config"
openapi: "PATCH /api/v1/sso/config"
---

View File

@ -851,6 +851,30 @@
{ {
"group": "Organizations", "group": "Organizations",
"pages": [ "pages": [
{
"group": "OIDC SSO",
"pages": [
"api-reference/endpoints/organizations/oidc-sso/get-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/update-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/create-oidc-config"
]
},
{
"group": "LDAP SSO",
"pages": [
"api-reference/endpoints/organizations/ldap-sso/get-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/update-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/create-ldap-config"
]
},
{
"group": "SAML SSO",
"pages": [
"api-reference/endpoints/organizations/saml-sso/get-saml-config",
"api-reference/endpoints/organizations/saml-sso/update-saml-config",
"api-reference/endpoints/organizations/saml-sso/create-saml-config"
]
},
"api-reference/endpoints/organizations/memberships", "api-reference/endpoints/organizations/memberships",
"api-reference/endpoints/organizations/update-membership", "api-reference/endpoints/organizations/update-membership",
"api-reference/endpoints/organizations/delete-membership", "api-reference/endpoints/organizations/delete-membership",

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

View File

@ -794,3 +794,9 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
The TLS header used to propagate the client certificate from the load balancer The TLS header used to propagate the client certificate from the load balancer
to the server. to the server.
</ParamField> </ParamField>
## Environment Variable Overrides
If you can't directly access and modify environment variables, you can update them using the [Server Admin Console](/documentation/platform/admin-panel/server-admin).
![Environment Variables Overrides Page](../../images/self-hosting/configuration/overrides/page.png)

View File

@ -33,7 +33,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
className className
)} )}
> >
<Lottie icon="infisical_loading" className="h-32 w-32" /> <Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
{text && isTextArray && ( {text && isTextArray && (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div <motion.div

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

@ -43,23 +43,25 @@ export const Tooltip = ({
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
> >
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger> <TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content <TooltipPrimitive.Portal>
side={position} <TooltipPrimitive.Content
align="center" side={position}
sideOffset={5} align="center"
{...props} sideOffset={5}
className={twMerge( {...props}
"z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade", className={twMerge(
isDisabled && "!hidden", "z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade",
center && "text-center", isDisabled && "!hidden",
size === "sm" && "rounded-sm px-2 py-1 text-xs", center && "text-center",
size === "md" && "rounded-md px-4 py-2 text-sm", size === "sm" && "rounded-sm px-2 py-1 text-xs",
className size === "md" && "rounded-md px-4 py-2 text-sm",
)} className
> )}
{content} >
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" /> {content}
</TooltipPrimitive.Content> <TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Root> </TooltipPrimitive.Root>
) : ( ) : (
// eslint-disable-next-line react/jsx-no-useless-fragment // eslint-disable-next-line react/jsx-no-useless-fragment

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

@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: { }: {
@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => {
clientSecret?: string; clientSecret?: string;
isActive?: boolean; isActive?: boolean;
configurationType?: string; configurationType?: string;
orgSlug: string; organizationId: string;
manageGroupMemberships?: boolean; manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
}) => { }) => {
@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => {
tokenEndpoint, tokenEndpoint,
userinfoEndpoint, userinfoEndpoint,
clientId, clientId,
orgSlug, organizationId,
clientSecret, clientSecret,
isActive, isActive,
manageGroupMemberships, manageGroupMemberships,
@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => {
return data; return data;
}, },
onSuccess(_, dto) { onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations }); queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
} }
}); });
@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}: { }: {
@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
isActive: boolean; isActive: boolean;
orgSlug: string; organizationId: string;
allowedEmailDomains?: string; allowedEmailDomains?: string;
manageGroupMemberships?: boolean; manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm; jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => {
clientId, clientId,
clientSecret, clientSecret,
isActive, isActive,
orgSlug, organizationId,
manageGroupMemberships, manageGroupMemberships,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => {
return data; return data;
}, },
onSuccess(_, dto) { onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) }); queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
} }
}); });
}; };

View File

@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types"; import { OIDCConfigData } from "./types";
export const oidcConfigKeys = { export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const, getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const,
getOIDCManageGroupMembershipsEnabled: (orgId: string) => getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
["oidc-manage-group-memberships", orgId] as const ["oidc-manage-group-memberships", orgId] as const
}; };
export const useGetOIDCConfig = (orgSlug: string) => { export const useGetOIDCConfig = (orgId: string) => {
return useQuery({ return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug), queryKey: oidcConfigKeys.getOIDCConfig(orgId),
queryFn: async () => { queryFn: async () => {
try { try {
const { data } = await apiRequest.get<OIDCConfigData>( const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}` `/api/v1/sso/oidc/config?organizationId=${orgId}`
); );
return data; return data;

View File

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

View File

@ -113,7 +113,7 @@ export const ProjectSelect = () => {
<div> <div>
<FontAwesomeIcon icon={faCube} className="text-xs" /> <FontAwesomeIcon icon={faCube} className="text-xs" />
</div> </div>
<Tooltip content={currentWorkspace.name}> <Tooltip content={currentWorkspace.name} className="max-w-96">
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap"> <div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
{currentWorkspace?.name} {currentWorkspace?.name}
</div> </div>
@ -176,7 +176,7 @@ export const ProjectSelect = () => {
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="flex flex-1 items-center justify-between overflow-hidden"> <div className="flex flex-1 items-center justify-between overflow-hidden">
<Tooltip content={workspace.name}> <Tooltip side="right" className="break-words" content={workspace.name}>
<div className="max-w-40 overflow-hidden truncate whitespace-nowrap"> <div className="max-w-40 overflow-hidden truncate whitespace-nowrap">
{workspace.name} {workspace.name}
</div> </div>

View File

@ -63,7 +63,7 @@ const router = createRouter({
context: { serverConfig: null, queryClient }, context: { serverConfig: null, queryClient },
defaultPendingComponent: () => ( defaultPendingComponent: () => (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800"> <div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<Lottie icon="infisical_loading" className="h-32 w-32" /> <Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div> </div>
), ),
defaultNotFoundComponent: NotFoundPage, defaultNotFoundComponent: NotFoundPage,

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 Variables"
description="Manage the environment variables for your Infisical instance."
/>
<EnvironmentPageForm />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,264 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
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>
<a
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Override specific environment variables. After saving, it may take up to 5 minutes for
variables to propagate throughout every container.
</p>
</div>
<div className="flex flex-row gap-2">
<Button
type="submit"
variant="outline_bg"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</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: () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Environment",
link: linkOptions({
to: "/admin/environment"
})
}
]
};
}
});

View File

@ -1,19 +1,33 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
export const EmailNotVerifiedPage = () => { export const EmailNotVerifiedPage = () => {
return ( return (
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen"> <div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Helmet> <Helmet>
<title>Request a New Invite</title> <title>Request a New Invite</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200"> <Link to="/">
<p className="text-6xl">Oops.</p> <div className="mb-4 mt-20 flex justify-center">
<p className="mb-1 mt-2 text-xl">Your email was not verified. </p> <img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
<p className="text-xl">Please try again.</p> </div>
<p className="text-md mt-8 max-w-sm text-center text-gray-600"> </Link>
Note: If it still doesn&apos;t work, please reach out to us at support@infisical.com <div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Your email was not verified
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Please try again. <br /> Note: If it still doesn&apos;t work, please reach out to us at
support@infisical.com
</p> </p>
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link to="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Back to Login
</span>
</Link>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
@ -36,7 +36,12 @@ export const PasswordResetPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800"> <div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Link to="/">
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
</div>
</Link>
{step === Steps.ConfirmEmail && ( {step === Steps.ConfirmEmail && (
<ConfirmEmailStep <ConfirmEmailStep
onComplete={(verifyToken, userEncryptionVersion) => { onComplete={(verifyToken, userEncryptionVersion) => {

View File

@ -19,17 +19,21 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
isPending: isVerifyPasswordResetLoading isPending: isVerifyPasswordResetLoading
} = useVerifyPasswordResetCode(); } = useVerifyPasswordResetCode();
return ( return (
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6"> <div className="mx-auto flex w-full flex-col items-center justify-center">
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent"> <h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Confirm your email Confirm your email
</h1>
<p className="mb-8 w-max justify-center text-center text-sm text-gray-400">
Reset password for <span className="italic">{email}</span>.
</p> </p>
<img <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
src="/images/envelope.svg"
style={{ height: "262px", width: "410px" }}
alt="verify email"
/>
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<Button <Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
onClick={async () => { onClick={async () => {
try { try {
const response = await verifyPasswordResetCodeMutateAsync({ const response = await verifyPasswordResetCodeMutateAsync({
@ -44,7 +48,6 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
} }
}} }}
isLoading={isVerifyPasswordResetLoading} isLoading={isVerifyPasswordResetLoading}
size="lg"
> >
Confirm Email Confirm Email
</Button> </Button>

View File

@ -1,7 +1,7 @@
import crypto from "crypto"; import crypto from "crypto";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faXmark } 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";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
@ -169,17 +169,15 @@ export const EnterPasswordStep = ({
return ( return (
<form <form
onSubmit={handleSubmit(resetPasswordHandler)} onSubmit={handleSubmit(resetPasswordHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6" className="mx-auto flex w-full flex-col items-center justify-center"
> >
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Enter new password Enter new password
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p> </p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<Controller <Controller
control={control} control={control}
name="password" name="password"
@ -202,6 +200,20 @@ export const EnterPasswordStep = ({
)} )}
/> />
</div> </div>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={isSubmitting || isLoading || isLoadingV2}
isDisabled={isSubmitting || isLoading || isLoadingV2}
>
Change Password
</Button>
</div>
{passwordErrorTooShort || {passwordErrorTooShort ||
passwordErrorTooLong || passwordErrorTooLong ||
passwordErrorNoLetterChar || passwordErrorNoLetterChar ||
@ -210,33 +222,33 @@ export const EnterPasswordStep = ({
passwordErrorEscapeChar || passwordErrorEscapeChar ||
passwordErrorLowEntropy || passwordErrorLowEntropy ||
passwordErrorBreached ? ( passwordErrorBreached ? (
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4 drop-shadow">
<div className="mb-1 text-sm text-gray-400">Password should contain:</div> <div className="mb-1 ml-2 text-sm text-gray-300">Password should contain:</div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorTooShort ? ( {passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}> <div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
at least 14 characters at least 14 characters
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorTooLong ? ( {passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}> <div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
at most 100 characters at most 100 characters
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? ( {passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div <div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`} className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -244,11 +256,11 @@ export const EnterPasswordStep = ({
at least 1 letter character at least 1 letter character
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? ( {passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div <div
className={`${ className={`${
@ -258,11 +270,11 @@ export const EnterPasswordStep = ({
at least 1 number or special character at least 1 number or special character
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? ( {passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div <div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`} className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -270,11 +282,11 @@ export const EnterPasswordStep = ({
at most 3 repeated, consecutive characters at most 3 repeated, consecutive characters
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? ( {passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div <div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`} className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -282,11 +294,11 @@ export const EnterPasswordStep = ({
No escape characters allowed. No escape characters allowed.
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? ( {passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div <div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`} className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -294,32 +306,18 @@ export const EnterPasswordStep = ({
Password contains personal info. Password contains personal info.
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorBreached ? ( {passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)} )}
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}> <div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
Password was found in a data breach. Password was found in a data breach.
</div> </div>
</div> </div>
</div> </div>
) : ( ) : null}
<div className="py-2" />
)}
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isLoading || isLoadingV2}
isDisabled={isSubmitting || isLoading || isLoadingV2}
>
Change Password
</Button>
</div>
</div>
</form> </form>
); );
}; };

View File

@ -43,18 +43,15 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
return ( return (
<form <form
onSubmit={handleSubmit(getEncryptedKeyHandler)} onSubmit={handleSubmit(getEncryptedKeyHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6" className="mx-auto flex w-full flex-col items-center justify-center"
> >
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100"> <h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Enter your backup key Enter your backup key
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
You can find it in your emergency kit you downloaded during signup.
</p> </p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<p className="flex w-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<Controller <Controller
control={control} control={control}
name="backupKey" name="backupKey"
@ -75,12 +72,17 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
)} )}
/> />
</div> </div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2"> <div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg"> <Button
<Button type="submit" colorSchema="secondary"> type="submit"
Submit Backup Key size="sm"
</Button> isFullWidth
</div> className="h-10"
colorSchema="primary"
variant="solid"
>
Submit Backup Key
</Button>
</div> </div>
</form> </form>
); );

View File

@ -2,8 +2,7 @@ import { FormEvent, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import InputField from "@app/components/basic/InputField"; import { Button, EmailServiceSetupModal, Input } from "@app/components/v2";
import { Button, EmailServiceSetupModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useSendPasswordResetEmail } from "@app/hooks/api"; import { useSendPasswordResetEmail } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@ -44,9 +43,9 @@ export const VerifyEmailPage = () => {
}; };
return ( return (
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6"> <div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Helmet> <Helmet>
<title>Login</title> <title>Reset Password</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Verify your email in Infisical" /> <meta property="og:title" content="Verify your email in Infisical" />
@ -56,66 +55,80 @@ export const VerifyEmailPage = () => {
/> />
</Helmet> </Helmet>
<Link to="/"> <Link to="/">
<div className="mb-8 mt-20 flex cursor-pointer justify-center"> <div className="mb-4 mt-20 flex justify-center">
<img <img
src="/images/biglogo.png" src="/images/gradientLogo.svg"
style={{ style={{
height: "90px", height: "90px",
width: "120px" width: "120px"
}} }}
alt="long logo" alt="Infisical Logo"
/> />
</div> </div>
</Link> </Link>
{step === 1 && ( {step === 1 && (
<form <form
onSubmit={onSubmit} onSubmit={onSubmit}
className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl" className="mx-auto flex w-full flex-col items-center justify-center"
> >
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Forgot your password? Forgot your password?
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Enter your email to start the password reset process. <br /> You will receive an email
with instructions.
</p> </p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<p className="flex w-max justify-center text-center text-sm text-gray-400"> <Input
Enter your email to start the password reset process. You will receive an email with
instructions.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<InputField
label="Email"
onChangeHandler={setEmail}
type="email"
value={email} value={email}
placeholder="" onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired isRequired
autoComplete="username" autoComplete="username"
className="h-10"
/> />
</div> </div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2"> <div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg"> <Button
<Button type="submit" size="lg" onClick={() => {}} isLoading={loading}> type="submit"
Continue size="sm"
</Button> isFullWidth
</div> className="h-10"
colorSchema="primary"
variant="solid"
isLoading={loading}
>
Continue
</Button>
</div>
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link to="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Back to Login
</span>
</Link>
</div> </div>
</form> </form>
)} )}
{step === 2 && ( {step === 2 && (
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl"> <div className="mx-auto flex w-full flex-col items-center justify-center">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-semibold text-bunker-100 md:text-2xl"> <h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Look for an email in your inbox. Look for an email in your inbox
</h1>
<p className="w-max max-w-lg justify-center text-center text-sm text-gray-400">
If the email is in our system, you will receive an email at{" "}
<span className="italic">{email}</span> with instructions on how to reset your password.
</p> </p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <div className="mt-6 flex flex-row text-sm text-bunker-400">
<p className="w-max text-center text-sm text-gray-400"> <Link to="/login">
If the email is in our system, you will receive an email at{" "} <span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
<span className="italic">{email}</span> with instructions on how to reset your Back to Login
password. </span>
</p> </Link>
</div> </div>
</div> </div>
)} )}
<EmailServiceSetupModal <EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen} isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}

View File

@ -105,7 +105,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig(); const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig();
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false); const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false);
const { data } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { data } = useGetOIDCConfig(currentOrg?.id ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({ const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@ -134,7 +134,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId: "", clientId: "",
clientSecret: "", clientSecret: "",
isActive: false, isActive: false,
orgSlug: currentOrg.slug organizationId: currentOrg.id
}); });
createNotification({ createNotification({
@ -196,7 +196,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId, clientId,
clientSecret, clientSecret,
isActive: true, isActive: true,
orgSlug: currentOrg.slug, organizationId: currentOrg.id,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
} else { } else {
@ -212,7 +212,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId, clientId,
clientSecret, clientSecret,
isActive: true, isActive: true,
orgSlug: currentOrg.slug, organizationId: currentOrg.id,
jwtSignatureAlgorithm jwtSignatureAlgorithm
}); });
} }

View File

@ -21,7 +21,7 @@ export const OrgOIDCSection = (): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data, isPending } = useGetOIDCConfig(currentOrg?.slug ?? ""); const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateOIDCConfig(); const { mutateAsync } = useUpdateOIDCConfig();
const { mutateAsync: updateOrg } = useUpdateOrg(); const { mutateAsync: updateOrg } = useUpdateOrg();
@ -41,7 +41,7 @@ export const OrgOIDCSection = (): JSX.Element => {
} }
await mutateAsync({ await mutateAsync({
orgSlug: currentOrg?.slug, organizationId: currentOrg?.id,
isActive: value isActive: value
}); });
@ -114,7 +114,7 @@ export const OrgOIDCSection = (): JSX.Element => {
} }
await mutateAsync({ await mutateAsync({
orgSlug: currentOrg?.slug, organizationId: currentOrg?.id,
manageGroupMemberships: value manageGroupMemberships: value
}); });

View File

@ -38,7 +38,7 @@ export const OrgSsoTab = withPermission(
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig( const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig(
currentOrg?.slug ?? "" currentOrg?.id ?? ""
); );
const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig( const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig(
currentOrg?.id ?? "" currentOrg?.id ?? ""

View File

@ -891,7 +891,7 @@ export const OverviewPage = () => {
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) { if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
return ( return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
<Lottie icon="infisical_loading" className="h-32 w-32" /> <Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div> </div>
); );
} }

View File

@ -253,7 +253,7 @@ export const SecretDropzone = ({
> >
{isLoading ? ( {isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16"> <div className="mb-16 flex items-center justify-center pt-16">
<Lottie icon="infisical_loading" className="h-32 w-32" /> <Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center space-y-2"> <div className="flex flex-col items-center justify-center space-y-2">

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'
@ -565,6 +566,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',
@ -2180,6 +2187,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'
@ -4120,6 +4134,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
@ -4131,6 +4146,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,
@ -4333,6 +4349,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
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren '/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
@ -4529,6 +4546,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
'/projects/$projectId': typeof projectLayoutGeneralRouteWithChildren '/projects/$projectId': typeof projectLayoutGeneralRouteWithChildren
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren '/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
@ -4725,6 +4743,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/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
@ -4934,6 +4953,7 @@ export interface FileRouteTypes {
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
| '/admin/environment'
| '/admin/integrations' | '/admin/integrations'
| '/organization/app-connections' | '/organization/app-connections'
| '/organization/gateways' | '/organization/gateways'
@ -5129,6 +5149,7 @@ export interface FileRouteTypes {
| '/admin/authentication' | '/admin/authentication'
| '/admin/caching' | '/admin/caching'
| '/admin/encryption' | '/admin/encryption'
| '/admin/environment'
| '/admin/integrations' | '/admin/integrations'
| '/projects/$projectId' | '/projects/$projectId'
| '/secret-manager/$projectId' | '/secret-manager/$projectId'
@ -5323,6 +5344,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/organization/app-connections' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways' | '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
@ -5729,6 +5751,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",
@ -5775,6 +5798,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"),