Compare commits
29 Commits
misc/impro
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
7090eea716 | ||
|
01d3443139 | ||
|
c4b23a8d4f | ||
|
90a2a11fff | ||
|
95d7c2082c | ||
|
ab5eb4c696 | ||
|
65aeb81934 | ||
|
a406511405 | ||
|
61da0db49e | ||
|
59666740ca | ||
|
9cc7edc869 | ||
|
e1b016f76d | ||
|
1175b9b5af | ||
|
09521144ec | ||
|
8759944077 | ||
|
aac3c355e9 | ||
|
2a28a462a5 | ||
|
3328e0850f | ||
|
216cae9b33 | ||
|
89d4d4bc92 | ||
|
cffcb28bc9 | ||
|
61388753cf | ||
|
a6145120e6 | ||
|
a5945204ad | ||
|
2c75e23acf | ||
|
907dd4880a | ||
|
92f697e195 | ||
|
8062f0238b | ||
|
645dfafba0 |
3
Makefile
@@ -30,3 +30,6 @@ reviewable-api:
|
||||
npm run type:check
|
||||
|
||||
reviewable: reviewable-ui reviewable-api
|
||||
|
||||
up-dev-sso:
|
||||
docker compose -f docker-compose.dev.yml --profile sso up --build
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
|
||||
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||
if (!hasManageGroupMembershipsCol) {
|
||||
tb.boolean("manageGroupMemberships").notNullable().defaultTo(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
|
||||
|
||||
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||
if (hasManageGroupMembershipsCol) {
|
||||
t.dropColumn("manageGroupMemberships");
|
||||
}
|
||||
});
|
||||
}
|
@@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional()
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
manageGroupMemberships: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||
|
@@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
discoveryURL: true,
|
||||
isActive: true,
|
||||
orgId: true,
|
||||
allowedEmailDomains: true
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
}).extend({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
@@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: z.string().trim(),
|
||||
clientId: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean()
|
||||
isActive: z.boolean(),
|
||||
manageGroupMemberships: z.boolean().optional()
|
||||
})
|
||||
.partial()
|
||||
.merge(z.object({ orgSlug: z.string() })),
|
||||
@@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: true,
|
||||
orgId: true,
|
||||
allowedEmailDomains: true,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
manageGroupMemberships: true
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
clientId: z.string().trim(),
|
||||
clientSecret: z.string().trim(),
|
||||
isActive: z.boolean(),
|
||||
orgSlug: z.string().trim()
|
||||
orgSlug: z.string().trim(),
|
||||
manageGroupMemberships: z.boolean().optional().default(false)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||
@@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
userinfoEndpoint: true,
|
||||
orgId: true,
|
||||
isActive: true,
|
||||
allowedEmailDomains: true
|
||||
allowedEmailDomains: true,
|
||||
manageGroupMemberships: true
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
return oidc;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/manage-group-memberships",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
orgId: z.string().trim().min(1, "Org ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
isEnabled: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission);
|
||||
|
||||
return { isEnabled };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -249,7 +249,9 @@ export enum EventType {
|
||||
DELETE_SECRET_SYNC = "delete-secret-sync",
|
||||
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -2044,6 +2046,26 @@ interface SecretSyncRemoveSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface OidcGroupMembershipMappingAssignUserEvent {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER;
|
||||
metadata: {
|
||||
assignedToGroups: { id: string; name: string }[];
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userGroupsClaim: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface OidcGroupMembershipMappingRemoveUserEvent {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER;
|
||||
metadata: {
|
||||
removedFromGroups: { id: string; name: string }[];
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userGroupsClaim: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -2232,4 +2254,6 @@ export type Event =
|
||||
| DeleteSecretSyncEvent
|
||||
| SecretSyncSyncSecretsEvent
|
||||
| SecretSyncImportSecretsEvent
|
||||
| SecretSyncRemoveSecretsEvent;
|
||||
| SecretSyncRemoveSecretsEvent
|
||||
| OidcGroupMembershipMappingAssignUserEvent
|
||||
| OidcGroupMembershipMappingRemoveUserEvent;
|
||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||
@@ -59,7 +61,8 @@ export const groupServiceFactory = ({
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
oidcConfigDAL
|
||||
}: TGroupServiceFactoryDep) => {
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
@@ -311,6 +314,18 @@ export const groupServiceFactory = ({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId: group.orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (oidcConfig?.manageGroupMemberships) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
@@ -366,6 +381,18 @@ export const groupServiceFactory = ({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId: group.orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (oidcConfig?.manageGroupMemberships) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
|
@@ -5,6 +5,11 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
|
||||
|
||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -18,13 +23,18 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
@@ -45,7 +55,14 @@ import {
|
||||
type TOidcConfigServiceFactoryDep = {
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "updateById"
|
||||
| "findById"
|
||||
| "findUserEncKeyByUserId"
|
||||
| "findUserEncKeyByUserIdsBatch"
|
||||
| "find"
|
||||
| "transaction"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
orgDAL: Pick<
|
||||
@@ -57,8 +74,23 @@ type TOidcConfigServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getUserOrgPermission">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findByOrgId">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "findGroupMembershipsByUserIdInOrg"
|
||||
| "delete"
|
||||
| "filterProjectsByUserMembership"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
||||
@@ -73,7 +105,14 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
smtpService,
|
||||
oidcConfigDAL
|
||||
oidcConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
auditLogService
|
||||
}: TOidcConfigServiceFactoryDep) => {
|
||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||
@@ -156,11 +195,21 @@ export const oidcConfigServiceFactory = ({
|
||||
isActive: oidcCfg.isActive,
|
||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
};
|
||||
};
|
||||
|
||||
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
|
||||
const oidcLogin = async ({
|
||||
externalId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
orgId,
|
||||
callbackPort,
|
||||
groups = [],
|
||||
manageGroupMemberships
|
||||
}: TOidcLoginDTO) => {
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||
@@ -315,6 +364,83 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (manageGroupMemberships) {
|
||||
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId);
|
||||
const orgGroups = await groupDAL.findByOrgId(orgId);
|
||||
|
||||
const userGroupsNames = userGroups.map((membership) => membership.groupName);
|
||||
const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName));
|
||||
const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name));
|
||||
|
||||
for await (const group of groupsToAddUserTo) {
|
||||
await addUsersToGroupByUserIds({
|
||||
userIds: [user.id],
|
||||
group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
}
|
||||
|
||||
if (groupsToAddUserTo.length) {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId,
|
||||
event: {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })),
|
||||
userGroupsClaim: groups
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const membershipsToRemove = userGroups
|
||||
.filter((membership) => !groups.includes(membership.groupName))
|
||||
.map((membership) => membership.groupId);
|
||||
const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id));
|
||||
|
||||
for await (const group of groupsToRemoveUserFrom) {
|
||||
await removeUsersFromGroupByUserIds({
|
||||
userIds: [user.id],
|
||||
group,
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
}
|
||||
|
||||
if (groupsToRemoveUserFrom.length) {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId,
|
||||
event: {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
userEmail: user.email ?? user.username,
|
||||
removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })),
|
||||
userGroupsClaim: groups
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
@@ -385,7 +511,8 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
}: TUpdateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@@ -448,7 +575,8 @@ export const oidcConfigServiceFactory = ({
|
||||
userinfoEndpoint,
|
||||
jwksUri,
|
||||
isActive,
|
||||
lastUsed: null
|
||||
lastUsed: null,
|
||||
manageGroupMemberships
|
||||
};
|
||||
|
||||
if (clientId !== undefined) {
|
||||
@@ -491,7 +619,8 @@ export const oidcConfigServiceFactory = ({
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
clientSecret
|
||||
clientSecret,
|
||||
manageGroupMemberships
|
||||
}: TCreateOidcCfgDTO) => {
|
||||
const org = await orgDAL.findOne({
|
||||
slug: orgSlug
|
||||
@@ -589,7 +718,8 @@ export const oidcConfigServiceFactory = ({
|
||||
clientIdTag,
|
||||
encryptedClientSecret,
|
||||
clientSecretIV,
|
||||
clientSecretTag
|
||||
clientSecretTag,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return oidcCfg;
|
||||
@@ -683,7 +813,9 @@ export const oidcConfigServiceFactory = ({
|
||||
firstName: claims.given_name ?? "",
|
||||
lastName: claims.family_name ?? "",
|
||||
orgId: org.id,
|
||||
callbackPort
|
||||
groups: claims.groups as string[] | undefined,
|
||||
callbackPort,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
})
|
||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
@@ -697,5 +829,16 @@ export const oidcConfigServiceFactory = ({
|
||||
return strategy;
|
||||
};
|
||||
|
||||
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
|
||||
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => {
|
||||
await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId);
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
return Boolean(oidcConfig?.manageGroupMemberships);
|
||||
};
|
||||
|
||||
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled };
|
||||
};
|
||||
|
@@ -12,6 +12,8 @@ export type TOidcLoginDTO = {
|
||||
lastName?: string;
|
||||
orgId: string;
|
||||
callbackPort?: string;
|
||||
groups?: string[];
|
||||
manageGroupMemberships?: boolean | null;
|
||||
};
|
||||
|
||||
export type TGetOidcCfgDTO =
|
||||
@@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = {
|
||||
clientSecret: string;
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateOidcCfgDTO = Partial<{
|
||||
@@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
||||
clientSecret: string;
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships: boolean;
|
||||
}> &
|
||||
TGenericPermission;
|
||||
|
@@ -163,6 +163,27 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine(
|
||||
(val) => val.startsWith("/"),
|
||||
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine(
|
||||
(val) => val.startsWith("/"),
|
||||
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine(
|
||||
(val) => val.every((el) => el.startsWith("/")),
|
||||
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]);
|
||||
// akhilmhdh: don't modify this for v2
|
||||
// if you want to update create a new schema
|
||||
const SecretConditionV1Schema = z
|
||||
@@ -177,17 +198,7 @@ const SecretConditionV1Schema = z
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -204,17 +215,7 @@ const SecretConditionV2Schema = z
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||
secretName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
|
@@ -467,7 +467,8 @@ export const registerRoutes = async (
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
oidcConfigDAL
|
||||
});
|
||||
const groupProjectService = groupProjectServiceFactory({
|
||||
groupDAL,
|
||||
@@ -1337,7 +1338,14 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
orgBotDAL,
|
||||
permissionService,
|
||||
oidcConfigDAL
|
||||
oidcConfigDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
groupDAL,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const userEngagementService = userEngagementServiceFactory({
|
||||
|
@@ -110,7 +110,7 @@ var secretsCmd = &cobra.Command{
|
||||
|
||||
if plainOutput {
|
||||
for _, secret := range secrets {
|
||||
fmt.Println(secret.Value)
|
||||
fmt.Println(fmt.Sprintf("%s=%s", secret.Key, secret.Value))
|
||||
}
|
||||
} else {
|
||||
visualize.PrintAllSecretDetails(secrets)
|
||||
|
@@ -192,6 +192,17 @@ services:
|
||||
depends_on:
|
||||
- openldap
|
||||
profiles: [ldap]
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.1.0
|
||||
restart: always
|
||||
environment:
|
||||
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
|
||||
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||
command: start-dev
|
||||
ports:
|
||||
- 8088:8080
|
||||
profiles: [ sso ]
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Install'
|
||||
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
|
||||
description: "Infisical's CLI is one of the best ways to manage environments and secrets. Install it here"
|
||||
---
|
||||
|
||||
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
|
||||
@@ -127,4 +127,4 @@ You can use it across various environments, whether it's local development, CI/C
|
||||
## Quick Usage Guide
|
||||
<Card color="#00A300" href="./usage">
|
||||
Now that you have the CLI installed on your system, follow this guide to make the best use of it
|
||||
</Card>
|
||||
</Card>
|
||||
|
@@ -1,112 +0,0 @@
|
||||
---
|
||||
title: "Keycloak OIDC"
|
||||
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
||||
---
|
||||
|
||||
<Info>
|
||||
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
|
||||
is available under the **Pro Tier**. If you're self-hosting Infisical, then
|
||||
you should contact sales@infisical.com to purchase an enterprise license to
|
||||
use it.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OIDC client application in Keycloak">
|
||||
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
|
||||
</Info>
|
||||
|
||||
1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**.
|
||||
|
||||

|
||||
|
||||
1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section.
|
||||
|
||||

|
||||
|
||||
1.4. In the Login Settings step, set the following values:
|
||||
- Root URL: `https://app.infisical.com`.
|
||||
- Home URL: `https://app.infisical.com`.
|
||||
- Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`.
|
||||
- Web origins: `https://app.infisical.com`.
|
||||
|
||||

|
||||
<Info>
|
||||
If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
|
||||
</Info>
|
||||
|
||||
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
|
||||
|
||||

|
||||
|
||||
1.6. Next, click **Add predefined mapper**.
|
||||
|
||||

|
||||
|
||||
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
||||
|
||||

|
||||

|
||||
|
||||
Once you've completed the above steps, the list of mappers should look like the following:
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
|
||||
2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`.
|
||||

|
||||
|
||||
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Finish configuring OIDC in Infisical">
|
||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||

|
||||
|
||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||

|
||||
|
||||
Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||
</Step>
|
||||
<Step title="Enable OIDC SSO in Infisical">
|
||||
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Enforce OIDC SSO in Infisical">
|
||||
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||
by logging into the organization via Keycloak.
|
||||
|
||||
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
|
||||
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||
|
||||
<Warning>
|
||||
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
|
||||
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
|
||||
work:
|
||||
<div class="height:1px;"/>
|
||||
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
|
||||
can be a random 32-byte base64 string generated with `openssl rand -base64
|
||||
32`.
|
||||
<div class="height:1px;"/>
|
||||
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
||||
</Note>
|
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "Keycloak OIDC Group Membership Mapping"
|
||||
sidebarTitle: "Group Membership Mapping"
|
||||
description: "Learn how to sync Keycloak group members to matching groups in Infisical."
|
||||
---
|
||||
|
||||
You can have Infisical automatically sync group
|
||||
memberships between Keycloak and Infisical by configuring a group membership mapper in Keycloak.
|
||||
When a user logs in via OIDC, they will be added to Infisical groups that match their Keycloak groups names, and removed from any
|
||||
Infisical groups not present in their groups claim.
|
||||
|
||||
<Info>
|
||||
When enabled, manual
|
||||
management of Infisical group memberships will be disabled.
|
||||
</Info>
|
||||
|
||||
<Warning>
|
||||
Group membership changes in the Keycloak only sync with Infisical when a
|
||||
user logs in via OIDC. For example, if you remove a user from a group in Keycloak, this change will not be reflected in Infisical until their next OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC
|
||||
SSO in the OIDC settings.
|
||||
</Warning>
|
||||
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure a group membership mapper in Keycloak">
|
||||
1.1. In your realm, navigate to the **Clients** tab and select your Infisical client.
|
||||
|
||||

|
||||
|
||||
1.2. Select the **Client Scopes** tab.
|
||||
|
||||

|
||||
|
||||
1.3. Next, select the dedicated scope for your Infisical client.
|
||||
|
||||

|
||||
|
||||
1.4. Click on the **Add mapper** button, and select the **By configuration** option.
|
||||
|
||||

|
||||
|
||||
1.5. Select the **Group Membership** option.
|
||||
|
||||

|
||||
|
||||
1.6. Give your mapper a name and ensure the following properties are set to the following before saving:
|
||||
- **Token Claim Name** is set to `groups`
|
||||
- **Full group path** is disabled
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Setup groups in Infisical and enable OIDC Group Membership Mapping">
|
||||
2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name.
|
||||

|
||||
|
||||
2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security.
|
||||

|
||||
|
||||
2.3. The next time a user logs in they will be synced to their matching Keycloak groups.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
113
docs/documentation/platform/sso/keycloak-oidc/overview.mdx
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "Keycloak OIDC Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
||||
---
|
||||
|
||||
<Info>
|
||||
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
|
||||
is available under the **Pro Tier**. If you're self-hosting Infisical, then
|
||||
you should contact sales@infisical.com to purchase an enterprise license to
|
||||
use it.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OIDC client application in Keycloak">
|
||||
1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
|
||||
</Info>
|
||||
|
||||
1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**.
|
||||
|
||||

|
||||
|
||||
1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section.
|
||||
|
||||

|
||||
|
||||
1.4. In the Login Settings step, set the following values:
|
||||
- Root URL: `https://app.infisical.com`.
|
||||
- Home URL: `https://app.infisical.com`.
|
||||
- Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`.
|
||||
- Web origins: `https://app.infisical.com`.
|
||||
|
||||

|
||||
<Info>
|
||||
If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain.
|
||||
</Info>
|
||||
|
||||
1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope.
|
||||
|
||||

|
||||
|
||||
1.6. Next, click **Add predefined mapper**.
|
||||
|
||||

|
||||
|
||||
1.7. Select the **email**, **given name**, **family name** attributes and click **Add**.
|
||||
|
||||

|
||||

|
||||
|
||||
Once you've completed the above steps, the list of mappers should look like the following:
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Retrieve Identity Provider (IdP) Information from Keycloak">
|
||||
2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`.
|
||||

|
||||
|
||||
2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps.
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Finish configuring OIDC in Infisical">
|
||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||

|
||||
|
||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||

|
||||
|
||||
Once you've done that, press **Update** to complete the required configuration.
|
||||
|
||||
</Step>
|
||||
<Step title="Enable OIDC SSO in Infisical">
|
||||
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Enforce OIDC SSO in Infisical">
|
||||
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||
by logging into the organization via Keycloak.
|
||||
|
||||
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
|
||||
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||
|
||||
<Warning>
|
||||
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
|
||||
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
|
||||
work:
|
||||
<div class="height:1px;"/>
|
||||
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
|
||||
can be a random 32-byte base64 string generated with `openssl rand -base64
|
||||
32`.
|
||||
<div class="height:1px;"/>
|
||||
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
||||
</Note>
|
After Width: | Height: | Size: 341 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 318 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 438 KiB |
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 468 KiB |
After Width: | Height: | Size: 578 KiB |
After Width: | Height: | Size: 1.2 MiB |
@@ -38,8 +38,8 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
|
||||
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
|
||||
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
|
||||
|
@@ -40,8 +40,8 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
||||
|
||||
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
|
||||
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in GCP secret manager if secrets conflict.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
|
||||
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
|
||||
|
@@ -52,6 +52,7 @@ Refer to the table below for a list of subjects and the actions they support.
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
||||
| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -63,21 +64,23 @@ Refer to the table below for a list of subjects and the actions they support.
|
||||
`read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `read` |
|
||||
| Subject | Actions |
|
||||
| --------------------- | ------------------------------------------------ |
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `project-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` |
|
||||
| `kms` | `read` |
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -90,7 +93,6 @@ Permission inversion allows you to explicitly deny actions instead of allowing t
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
- cmek
|
||||
|
||||
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
|
||||
|
||||
|
@@ -249,7 +249,13 @@
|
||||
"documentation/platform/sso/keycloak-saml",
|
||||
"documentation/platform/sso/google-saml",
|
||||
"documentation/platform/sso/auth0-saml",
|
||||
"documentation/platform/sso/keycloak-oidc",
|
||||
{
|
||||
"group": "Keycloak OIDC",
|
||||
"pages": [
|
||||
"documentation/platform/sso/keycloak-oidc/overview",
|
||||
"documentation/platform/sso/keycloak-oidc/group-membership-mapping"
|
||||
]
|
||||
},
|
||||
"documentation/platform/sso/auth0-oidc",
|
||||
"documentation/platform/sso/general-oidc"
|
||||
]
|
||||
|
@@ -42,7 +42,7 @@ namespace Example
|
||||
ProjectId = "PROJECT_ID",
|
||||
Environment = "dev",
|
||||
};
|
||||
var secret = infisical.GetSecret(getSecretOptions);
|
||||
var secret = infisicalClient.GetSecret(getSecretOptions);
|
||||
|
||||
|
||||
Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}");
|
||||
|
@@ -87,7 +87,7 @@ const Content = ({ secretSync, onComplete }: ContentProps) => {
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Specify how Infisical should resolve the initial sync to {destinationName}. The
|
||||
Specify how Infisical should resolve importing secrets from {destinationName}. The
|
||||
following options are available:
|
||||
</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
|
@@ -27,11 +27,11 @@ export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||
}),
|
||||
[SecretSyncInitialSyncBehavior.ImportPrioritizeSource]: (destinationName: string) => ({
|
||||
name: "Import Destination Secrets - Prioritize Infisical Values",
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values present in Infisical over ${destinationName}.`
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values from Infisical over ${destinationName} when keys conflict.`
|
||||
}),
|
||||
[SecretSyncInitialSyncBehavior.ImportPrioritizeDestination]: (destinationName: string) => ({
|
||||
name: `Import Destination Secrets - Prioritize ${destinationName} Values`,
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values present in ${destinationName} over Infisical.`
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination prior to syncing, prioritizing values from ${destinationName} over Infisical when keys conflict.`
|
||||
})
|
||||
};
|
||||
|
||||
@@ -41,10 +41,10 @@ export const SECRET_SYNC_IMPORT_BEHAVIOR_MAP: Record<
|
||||
> = {
|
||||
[SecretSyncImportBehavior.PrioritizeSource]: (destinationName: string) => ({
|
||||
name: "Prioritize Infisical Values",
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values present in Infisical over ${destinationName}.`
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from Infisical over ${destinationName} when keys conflict.`
|
||||
}),
|
||||
[SecretSyncImportBehavior.PrioritizeDestination]: (destinationName: string) => ({
|
||||
name: `Prioritize ${destinationName} Values`,
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values present in ${destinationName} over Infisical.`
|
||||
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from ${destinationName} over Infisical when keys conflict.`
|
||||
})
|
||||
};
|
||||
|
@@ -114,7 +114,11 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.DELETE_SECRET_SYNC]: "Delete Secret Sync",
|
||||
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
|
||||
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
|
||||
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets"
|
||||
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets",
|
||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
|
||||
"OIDC group membership mapping assigned user to groups",
|
||||
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
|
||||
"OIDC group membership mapping removed user from groups"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@@ -127,5 +127,7 @@ export enum EventType {
|
||||
DELETE_SECRET_SYNC = "delete-secret-sync",
|
||||
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
||||
}
|
||||
|
@@ -20,7 +20,8 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
}: {
|
||||
allowedEmailDomains?: string;
|
||||
issuer?: string;
|
||||
@@ -34,6 +35,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
isActive?: boolean;
|
||||
configurationType?: string;
|
||||
orgSlug: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", {
|
||||
issuer,
|
||||
@@ -47,7 +49,8 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientId,
|
||||
orgSlug,
|
||||
clientSecret,
|
||||
isActive
|
||||
isActive,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -74,7 +77,8 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
}: {
|
||||
issuer?: string;
|
||||
configurationType: string;
|
||||
@@ -88,6 +92,7 @@ export const useCreateOIDCConfig = () => {
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/sso/oidc/config", {
|
||||
issuer,
|
||||
@@ -101,7 +106,8 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug
|
||||
orgSlug,
|
||||
manageGroupMemberships
|
||||
});
|
||||
|
||||
return data;
|
||||
|
@@ -5,7 +5,9 @@ import { apiRequest } from "@app/config/request";
|
||||
import { OIDCConfigData } from "./types";
|
||||
|
||||
export const oidcConfigKeys = {
|
||||
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const
|
||||
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
|
||||
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
|
||||
["oidc-manage-group-memberships", orgId] as const
|
||||
};
|
||||
|
||||
export const useGetOIDCConfig = (orgSlug: string) => {
|
||||
@@ -25,3 +27,16 @@ export const useGetOIDCConfig = (orgSlug: string) => {
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useOidcManageGroupMembershipsEnabled = (orgId: string) => {
|
||||
return useQuery({
|
||||
queryKey: oidcConfigKeys.getOIDCManageGroupMembershipsEnabled(orgId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ isEnabled: boolean }>(
|
||||
`/api/v1/sso/oidc/manage-group-memberships?orgId=${orgId}`
|
||||
);
|
||||
|
||||
return data.isEnabled;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -12,4 +12,5 @@ export type OIDCConfigData = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships: boolean;
|
||||
};
|
||||
|
@@ -40,6 +40,12 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.PLATFORM:
|
||||
return (
|
||||
<Td>
|
||||
<p>Platform</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.UNKNOWN_USER:
|
||||
return (
|
||||
<Td>
|
||||
|
@@ -2,8 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddGroupMembersModal } from "../AddGroupMemberModal";
|
||||
@@ -20,6 +22,11 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
||||
"removeMemberFromGroup"
|
||||
] as const);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup();
|
||||
const handleRemoveUserFromGroup = async (username: string) => {
|
||||
try {
|
||||
@@ -47,19 +54,35 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Group Members</h3>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<IconButton
|
||||
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<GroupMembersTable
|
||||
|
@@ -21,11 +21,12 @@ import {
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useListGroupUsers } from "@app/hooks/api";
|
||||
import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { EFilterReturnedUsers } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -58,6 +59,11 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
toggleOrderDirection
|
||||
} = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 });
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
const { data: groupMemberships, isPending } = useListGroupUsers({
|
||||
id: groupId,
|
||||
groupSlug,
|
||||
@@ -173,19 +179,30 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
|
||||
{!groupMemberships?.users.length && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupMembers", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add members
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
|
@@ -3,7 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
|
||||
import { TGroupUser } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -19,6 +20,11 @@ export const GroupMembershipRow = ({
|
||||
user: { firstName, lastName, username, joinedGroupAt, email, id },
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data: isOidcManageGroupMembershipsEnabled = false } =
|
||||
useOidcManageGroupMembershipsEnabled(currentOrg.id);
|
||||
|
||||
return (
|
||||
<Tr className="items-center" key={`group-user-${id}`}>
|
||||
<Td>
|
||||
@@ -36,15 +42,21 @@ export const GroupMembershipRow = ({
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Remove user from group">
|
||||
<Tooltip
|
||||
content={
|
||||
isOidcManageGroupMembershipsEnabled
|
||||
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
|
||||
: "Remove user from group"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
|
||||
ariaLabel="Remove user from group"
|
||||
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserMinus} className="cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faUserMinus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import { faInfoCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, Switch } from "@app/components/v2";
|
||||
import { Button, Switch, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -79,6 +82,29 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOIDCGroupManagement = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
if (!subscription?.oidcSSO) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgSlug: currentOrg?.slug,
|
||||
manageGroupMemberships: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} OIDC group membership mapping`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const addOidcButtonClick = async () => {
|
||||
if (subscription?.oidcSSO && currentOrg) {
|
||||
handlePopUpOpen("addOIDC");
|
||||
@@ -148,6 +174,65 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
Enforce members to authenticate via OIDC to access this organization
|
||||
</p>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="text-md flex items-center text-mineshaft-100">
|
||||
<span>OIDC Group Membership Mapping</span>
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When this feature is enabled, Infisical will automatically sync group
|
||||
memberships between the OIDC provider and Infisical. Users will be added to
|
||||
Infisical groups that match their OIDC group names, and removed from any
|
||||
Infisical groups not present in their groups claim. When enabled, manual
|
||||
management of Infisical group memberships will be disabled.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
To use this feature you must include group claims in the OIDC token.
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-300"
|
||||
href="https://infisical.com/docs/documentation/platform/sso/overview"
|
||||
>
|
||||
See your OIDC provider docs for details.
|
||||
</a>
|
||||
<p className="mt-4 text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" icon={faWarning} />
|
||||
Group membership changes in the OIDC provider only sync with Infisical when a
|
||||
user logs in via OIDC. For example, if you remove a user from a group in the
|
||||
OIDC provider, this change will not be reflected in Infisical until their next
|
||||
OIDC login. To ensure this behavior, Infisical recommends enabling Enforce OIDC
|
||||
SSO.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
size="sm"
|
||||
className="ml-1 mt-0.5 inline-block text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-org-auth"
|
||||
isChecked={data?.manageGroupMemberships ?? false}
|
||||
onCheckedChange={(value) => handleOIDCGroupManagement(value)}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
Infisical will manage user group memberships based on the OIDC provider
|
||||
</p>
|
||||
</div>
|
||||
<OIDCModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
|
@@ -80,6 +80,19 @@ const ConditionSchema = z
|
||||
return true;
|
||||
},
|
||||
{ message: "Duplicate operator found for a condition" }
|
||||
)
|
||||
.refine(
|
||||
(val) =>
|
||||
val
|
||||
.filter(
|
||||
(el) => el.lhs === "secretPath" && el.operator !== PermissionConditionOperators.$GLOB
|
||||
)
|
||||
.every((el) =>
|
||||
el.operator === PermissionConditionOperators.$IN
|
||||
? el.rhs.split(",").every((i) => i.trim().startsWith("/"))
|
||||
: el.rhs.trim().startsWith("/")
|
||||
),
|
||||
{ message: "Invalid Secret Path. Must start with '/'" }
|
||||
);
|
||||
|
||||
export const projectRoleFormSchema = z.object({
|
||||
|
@@ -36,6 +36,10 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
||||
name: `permissions.secrets.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.secrets?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.secrets?.[position]?.conditions?.root?.message;
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
@@ -147,10 +151,10 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors?.permissions?.secrets?.[position]?.conditions?.message && (
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
|
@@ -12,11 +12,10 @@ export const FrameworkIntegrationTab = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
|
||||
<div className="mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
|
||||
</div>
|
||||
<div className="mx-2 mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
<div className="mt-4 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{sortedFrameworks.map((framework) => (
|
||||
<a
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
|
@@ -5,13 +5,12 @@ export const InfrastructureIntegrationTab = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
|
||||
<div className="mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<p className="text-base text-gray-400">
|
||||
Click on of the integration to read the documentation.
|
||||
Click on an integration to read the documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<a
|
||||
key={`framework-integration-${integration.slug}`}
|
||||
|
@@ -189,17 +189,19 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="Copy secret name">
|
||||
<IconButton
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="relative">
|
||||
<Tooltip content="Copy secret name">
|
||||
<IconButton
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-100"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecNameCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
@@ -209,44 +211,48 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content={errors.key ? errors.key.message : "Save"}>
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
type="submit"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
isDisabled={isSubmitting || Boolean(errors.key)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
size="lg"
|
||||
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Cancel">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="relative">
|
||||
<Tooltip content={errors.key ? errors.key.message : "Save"}>
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
type="submit"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
isDisabled={isSubmitting || Boolean(errors.key)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
size="lg"
|
||||
className={twMerge("text-primary", errors.key && "text-mineshaft-400")}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Tooltip content="Cancel">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
@@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.8.7
|
||||
version: v0.8.9
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.8.7"
|
||||
appVersion: "v0.8.9"
|
||||
|
@@ -1,3 +1,4 @@
|
||||
{{- if .Values.installCRDs }}
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
@@ -465,4 +466,5 @@ status:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
storedVersions: []
|
||||
{{- end }}
|
@@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.8.7
|
||||
tag: v0.8.8
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
@@ -11,7 +11,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) error {
|
||||
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) error {
|
||||
if infisicalSecret.Status.Conditions == nil {
|
||||
infisicalSecret.Status.Conditions = []metav1.Condition{}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
|
||||
Type: "secrets.infisical.com/ReadyToSyncSecrets",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "OK",
|
||||
Message: "Infisical controller has started syncing your secrets",
|
||||
Message: fmt.Sprintf("Infisical controller has started syncing your secrets. Last reconcile synced %d secrets", secretsCount),
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -151,11 +151,10 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
api.API_CA_CERTIFICATE = ""
|
||||
}
|
||||
|
||||
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
|
||||
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
|
||||
secretsCount, err := r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
|
||||
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, secretsCount, err)
|
||||
|
||||
if err != nil {
|
||||
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
@@ -172,7 +171,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
|
||||
logger.Info(fmt.Sprintf("Successfully synced %d secrets. Operator will requeue after [%v]", secretsCount, requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
@@ -182,6 +181,10 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
if e.ObjectOld.GetGeneration() == e.ObjectNew.GetGeneration() {
|
||||
return false // Skip reconciliation for status-only changes
|
||||
}
|
||||
|
||||
if infisicalSecretResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
|
@@ -337,7 +337,7 @@ func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1al
|
||||
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) error {
|
||||
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) (int, error) {
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalSecret)
|
||||
infisicalClient := resourceVariables.InfisicalClient
|
||||
@@ -351,7 +351,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to authenticate [err=%s]", err)
|
||||
return 0, fmt.Errorf("unable to authenticate [err=%s]", err)
|
||||
}
|
||||
|
||||
r.updateResourceVariables(infisicalSecret, util.ResourceVariables{
|
||||
@@ -361,6 +361,8 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
})
|
||||
}
|
||||
|
||||
secretsCount := 0
|
||||
|
||||
for _, managedSecretReference := range managedKubeSecretReferences {
|
||||
// Look for managed secret by name and namespace
|
||||
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
@@ -369,7 +371,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
})
|
||||
|
||||
if err != nil && !k8Errors.IsNotFound(err) {
|
||||
return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
|
||||
return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
|
||||
}
|
||||
|
||||
// Get exiting Etag if exists
|
||||
@@ -384,12 +386,12 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
|
||||
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
|
||||
@@ -397,7 +399,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
} else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
|
||||
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
|
||||
@@ -406,7 +408,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
|
||||
@@ -415,30 +417,27 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
|
||||
|
||||
} else {
|
||||
return errors.New("no authentication method provided. Please configure a authentication method then try again")
|
||||
return 0, errors.New("no authentication method provided. Please configure a authentication method then try again")
|
||||
}
|
||||
|
||||
if !updateDetails.Modified {
|
||||
logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed")
|
||||
continue
|
||||
}
|
||||
secretsCount = len(plainTextSecretsFromApi)
|
||||
|
||||
if managedKubeSecret == nil {
|
||||
if err := r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, managedSecretReference, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
|
||||
return fmt.Errorf("failed to create managed secret [err=%s]", err)
|
||||
return 0, fmt.Errorf("failed to create managed secret [err=%s]", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
|
||||
return fmt.Errorf("failed to update managed secret [err=%s]", err)
|
||||
return 0, fmt.Errorf("failed to update managed secret [err=%s]", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return secretsCount, nil
|
||||
}
|
||||
|