Compare commits

..

6 Commits

39 changed files with 674 additions and 565 deletions

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,14 +263,21 @@ 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(),
@ -280,7 +288,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
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

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

View File

@ -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,8 +61,7 @@ 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;
@ -72,9 +71,7 @@ export type TSamlConfigServiceFactory = {
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 = {
@ -2427,8 +2430,7 @@ export const SecretSyncs = {
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
}, },
ONEPASS: { ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to.", vaultId: "The ID of the 1Password vault to sync secrets to."
valueLabel: "The label of the entry that holds the secret value."
}, },
HEROKU: { HEROKU: {
app: "The ID of the Heroku app to sync secrets to.", app: "The ID of the Heroku app to sync secrets to.",
@ -2651,3 +2653,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

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

View File

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

View File

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

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

@ -849,6 +849,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.

Before

Width:  |  Height:  |  Size: 641 KiB

After

Width:  |  Height:  |  Size: 715 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -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

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

View File

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

View File

@ -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

@ -445,7 +445,7 @@ export const ReviewAccessRequestModal = ({
onCheckedChange={(checked) => setBypassApproval(checked === true)} onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={bypassApproval} isChecked={bypassApproval}
id="byPassApproval" id="byPassApproval"
className={twMerge("mr-2", bypassApproval ? "!border-red/30 !bg-red/10" : "")} className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
> >
<span className="text-xs text-red"> <span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy protection) Approve without waiting for requirements to be met (bypass policy protection)

View File

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

View File

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

View File

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

View File

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