mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 10:26:35 +00:00
Merge pull request #2950 from Infisical/feat/secret-access-list
feat: secret access list
This commit is contained in:
@@ -22,6 +22,7 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou
|
|||||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||||
|
import { registerSecretRouter } from "./secret-router";
|
||||||
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||||
import { registerSnapshotRouter } from "./snapshot-router";
|
import { registerSnapshotRouter } from "./snapshot-router";
|
||||||
@@ -92,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { RAW_SECRETS } from "@app/lib/api-docs";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const AccessListEntrySchema = z
|
||||||
|
.object({
|
||||||
|
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
|
||||||
|
id: z.string(),
|
||||||
|
membershipId: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
|
||||||
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:secretName/access-list",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get list of users, machine identities, and groups with access to a secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId),
|
||||||
|
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
groups: AccessListEntrySchema,
|
||||||
|
identities: AccessListEntrySchema,
|
||||||
|
users: AccessListEntrySchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const { secretPath, environment, workspaceId: projectId } = req.query;
|
||||||
|
|
||||||
|
return server.services.secret.getSecretAccessList({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secretName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
rbac: false,
|
rbac: false,
|
||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
|
secretAccessInsights: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
auditLogStreams: false,
|
auditLogStreams: false,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export type TFeatureSet = {
|
|||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
hsm: false;
|
hsm: false;
|
||||||
oidcSSO: false;
|
oidcSSO: false;
|
||||||
|
secretAccessInsights: false;
|
||||||
scim: false;
|
scim: false;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
groups: false;
|
groups: false;
|
||||||
|
|||||||
@@ -125,6 +125,404 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectGroupPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.GroupProjectMembership)
|
||||||
|
.join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`groupCustomRoles.id`
|
||||||
|
)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||||
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("groupProjectMembershipRoleTemporaryAccessEndTime")
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupPermissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "groupId",
|
||||||
|
parentMapper: ({ groupId, groupName, membershipId }) => ({
|
||||||
|
groupId,
|
||||||
|
username: groupName,
|
||||||
|
id: membershipId
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "groupProjectMembershipRoleId",
|
||||||
|
label: "groupRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
groupProjectMembershipRoleId,
|
||||||
|
groupProjectMembershipRole,
|
||||||
|
groupProjectMembershipRolePermission,
|
||||||
|
groupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
groupProjectMembershipRoleIsTemporary,
|
||||||
|
groupProjectMembershipRoleTemporaryMode,
|
||||||
|
groupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
groupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
groupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: groupProjectMembershipRoleId,
|
||||||
|
role: groupProjectMembershipRole,
|
||||||
|
customRoleSlug: groupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: groupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: groupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: groupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: groupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupPermissions
|
||||||
|
.map((groupPermission) => {
|
||||||
|
if (!groupPermission) return undefined;
|
||||||
|
|
||||||
|
const activeGroupRoles =
|
||||||
|
groupPermission?.groupRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...groupPermission,
|
||||||
|
roles: activeGroupRoles
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectGroupPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectUserPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.Users)
|
||||||
|
.where("isGhost", "=", false)
|
||||||
|
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||||
|
void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]));
|
||||||
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectRoles>(
|
||||||
|
{ groupCustomRoles: TableName.ProjectRoles },
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`groupCustomRoles.id`
|
||||||
|
)
|
||||||
|
.join(TableName.ProjectMembership, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectUserMembershipRole,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
|
||||||
|
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
|
||||||
|
})
|
||||||
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
|
// groups specific
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("customRoleId")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleCustomRoleId"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.GroupProjectMembershipRole)
|
||||||
|
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
// user specific
|
||||||
|
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserMembershipRole)
|
||||||
|
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
|
||||||
|
db
|
||||||
|
.ref("permissions")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryMode"),
|
||||||
|
db
|
||||||
|
.ref("isTemporary")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryRange"),
|
||||||
|
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
|
// general
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||||
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
|
);
|
||||||
|
|
||||||
|
const userPermissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "userId",
|
||||||
|
parentMapper: ({
|
||||||
|
orgId,
|
||||||
|
username,
|
||||||
|
orgAuthEnforced,
|
||||||
|
membershipId,
|
||||||
|
groupMembershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
groupMembershipCreatedAt,
|
||||||
|
groupMembershipUpdatedAt,
|
||||||
|
membershipUpdatedAt,
|
||||||
|
projectType,
|
||||||
|
userId
|
||||||
|
}) => ({
|
||||||
|
orgId,
|
||||||
|
orgAuthEnforced,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
username,
|
||||||
|
projectType,
|
||||||
|
id: membershipId || groupMembershipId,
|
||||||
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "userGroupProjectMembershipRoleId",
|
||||||
|
label: "userGroupRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userGroupProjectMembershipRoleId,
|
||||||
|
userGroupProjectMembershipRole,
|
||||||
|
userGroupProjectMembershipRolePermission,
|
||||||
|
userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
userGroupProjectMembershipRoleIsTemporary,
|
||||||
|
userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userGroupProjectMembershipRoleTemporaryRange
|
||||||
|
}) => ({
|
||||||
|
id: userGroupProjectMembershipRoleId,
|
||||||
|
role: userGroupProjectMembershipRole,
|
||||||
|
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userGroupProjectMembershipRolePermission,
|
||||||
|
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userGroupProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userProjectMembershipRoleId",
|
||||||
|
label: "projectMembershipRoles" as const,
|
||||||
|
mapper: ({
|
||||||
|
userProjectMembershipRoleId,
|
||||||
|
userProjectMembershipRole,
|
||||||
|
userProjectCustomRolePermission,
|
||||||
|
userProjectMembershipRoleIsTemporary,
|
||||||
|
userProjectMembershipRoleTemporaryMode,
|
||||||
|
userProjectMembershipRoleTemporaryRange,
|
||||||
|
userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
userProjectMembershipRoleCustomRoleSlug
|
||||||
|
}) => ({
|
||||||
|
id: userProjectMembershipRoleId,
|
||||||
|
role: userProjectMembershipRole,
|
||||||
|
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
|
||||||
|
permissions: userProjectCustomRolePermission,
|
||||||
|
temporaryRange: userProjectMembershipRoleTemporaryRange,
|
||||||
|
temporaryMode: userProjectMembershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: userProjectMembershipRoleIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userAdditionalPrivilegesId",
|
||||||
|
label: "additionalPrivileges" as const,
|
||||||
|
mapper: ({
|
||||||
|
userAdditionalPrivilegesId,
|
||||||
|
userAdditionalPrivilegesPermissions,
|
||||||
|
userAdditionalPrivilegesIsTemporary,
|
||||||
|
userAdditionalPrivilegesTemporaryMode,
|
||||||
|
userAdditionalPrivilegesTemporaryRange,
|
||||||
|
userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
|
userAdditionalPrivilegesTemporaryAccessStartTime
|
||||||
|
}) => ({
|
||||||
|
id: userAdditionalPrivilegesId,
|
||||||
|
permissions: userAdditionalPrivilegesPermissions,
|
||||||
|
temporaryRange: userAdditionalPrivilegesTemporaryRange,
|
||||||
|
temporaryMode: userAdditionalPrivilegesTemporaryMode,
|
||||||
|
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return userPermissions
|
||||||
|
.map((userPermission) => {
|
||||||
|
if (!userPermission) return undefined;
|
||||||
|
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined;
|
||||||
|
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles =
|
||||||
|
userPermission?.projectMembershipRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const activeGroupRoles =
|
||||||
|
userPermission?.userGroupRoles?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const activeAdditionalPrivileges =
|
||||||
|
userPermission?.additionalPrivileges?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...userPermission,
|
||||||
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
|
additionalPrivileges: activeAdditionalPrivileges
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectUserPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
|
||||||
@@ -414,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectIdentityPermissions = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const docs = await db
|
||||||
|
.replicaNode()(TableName.IdentityProjectMembership)
|
||||||
|
.join(
|
||||||
|
TableName.IdentityProjectMembershipRole,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityProjectAdditionalPrivilege,
|
||||||
|
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
|
||||||
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
// Join the Project table to later select orgId
|
||||||
|
TableName.Project,
|
||||||
|
`${TableName.IdentityProjectMembership}.projectId`,
|
||||||
|
`${TableName.Project}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("id").withSchema(TableName.Identity).as("identityId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
|
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
|
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
|
||||||
|
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
|
||||||
|
db
|
||||||
|
.ref("temporaryMode")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryMode"),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
|
||||||
|
db
|
||||||
|
.ref("temporaryRange")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
|
.as("identityApTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissions = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "identityId",
|
||||||
|
parentMapper: ({
|
||||||
|
membershipId,
|
||||||
|
membershipCreatedAt,
|
||||||
|
membershipUpdatedAt,
|
||||||
|
orgId,
|
||||||
|
identityName,
|
||||||
|
projectType,
|
||||||
|
identityId
|
||||||
|
}) => ({
|
||||||
|
id: membershipId,
|
||||||
|
identityId,
|
||||||
|
username: identityName,
|
||||||
|
projectId,
|
||||||
|
createdAt: membershipCreatedAt,
|
||||||
|
updatedAt: membershipUpdatedAt,
|
||||||
|
orgId,
|
||||||
|
projectType,
|
||||||
|
// just a prefilled value
|
||||||
|
orgAuthEnforced: false
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: (data) =>
|
||||||
|
IdentityProjectMembershipRoleSchema.extend({
|
||||||
|
permissions: z.unknown(),
|
||||||
|
customRoleSlug: z.string().optional().nullable()
|
||||||
|
}).parse(data)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "identityApId",
|
||||||
|
label: "additionalPrivileges" as const,
|
||||||
|
mapper: ({
|
||||||
|
identityApId,
|
||||||
|
identityApPermissions,
|
||||||
|
identityApIsTemporary,
|
||||||
|
identityApTemporaryMode,
|
||||||
|
identityApTemporaryRange,
|
||||||
|
identityApTemporaryAccessEndTime,
|
||||||
|
identityApTemporaryAccessStartTime
|
||||||
|
}) => ({
|
||||||
|
id: identityApId,
|
||||||
|
permissions: identityApPermissions,
|
||||||
|
temporaryRange: identityApTemporaryRange,
|
||||||
|
temporaryMode: identityApTemporaryMode,
|
||||||
|
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||||
|
isTemporary: identityApIsTemporary
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
.map((permission) => {
|
||||||
|
if (!permission) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when introducting cron mode change it here
|
||||||
|
const activeRoles = permission?.roles.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
|
||||||
|
})
|
||||||
|
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const docs = await db
|
const docs = await db
|
||||||
@@ -568,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
getOrgIdentityPermission,
|
getOrgIdentityPermission,
|
||||||
getProjectPermission,
|
getProjectPermission,
|
||||||
getProjectIdentityPermission
|
getProjectIdentityPermission,
|
||||||
|
getProjectUserPermissions,
|
||||||
|
getProjectIdentityPermissions,
|
||||||
|
getProjectGroupPermissions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -405,6 +405,123 @@ export const permissionServiceFactory = ({
|
|||||||
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectPermissions = async (projectId: string) => {
|
||||||
|
// fetch user permissions
|
||||||
|
const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
|
||||||
|
const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const additionalPrivileges =
|
||||||
|
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||||
|
role: ProjectMembershipRole.Custom,
|
||||||
|
permissions
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
|
objectify(
|
||||||
|
userProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
name: userProjectPermission.username,
|
||||||
|
membershipId: userProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch identity permissions
|
||||||
|
const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId);
|
||||||
|
const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const additionalPrivileges =
|
||||||
|
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
|
||||||
|
role: ProjectMembershipRole.Custom,
|
||||||
|
permissions
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||||
|
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||||
|
objectify(
|
||||||
|
identityProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
name: identityProjectPermission.username,
|
||||||
|
membershipId: identityProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch group permissions
|
||||||
|
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId);
|
||||||
|
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
|
||||||
|
const rolePermissions =
|
||||||
|
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission,
|
||||||
|
id: groupProjectPermission.groupId,
|
||||||
|
name: groupProjectPermission.username,
|
||||||
|
membershipId: groupProjectPermission.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userPermissions,
|
||||||
|
identityPermissions,
|
||||||
|
groupPermissions
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectPermission = async <T extends ActorType>(
|
const getProjectPermission = async <T extends ActorType>(
|
||||||
type: T,
|
type: T,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -455,6 +572,7 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermission,
|
getOrgPermission,
|
||||||
getUserProjectPermission,
|
getUserProjectPermission,
|
||||||
getProjectPermission,
|
getProjectPermission,
|
||||||
|
getProjectPermissions,
|
||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
|
|||||||
@@ -741,6 +741,12 @@ export const RAW_SECRETS = {
|
|||||||
workspaceId: "The ID of the project where the secret is located.",
|
workspaceId: "The ID of the project where the secret is located.",
|
||||||
environment: "The slug of the environment where the the secret is located.",
|
environment: "The slug of the environment where the the secret is located.",
|
||||||
secretPath: "The folder path where the secret is located."
|
secretPath: "The folder path where the secret is located."
|
||||||
|
},
|
||||||
|
GET_ACCESS_LIST: {
|
||||||
|
secretName: "The name of the secret to get the access list for.",
|
||||||
|
workspaceId: "The ID of the project where the secret is located.",
|
||||||
|
environment: "The slug of the environment where the the secret is located.",
|
||||||
|
secretPath: "The folder path where the secret is located."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1033,7 +1033,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
secretApprovalRequestService
|
secretApprovalRequestService,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
SecretsSchema,
|
SecretsSchema,
|
||||||
SecretType
|
SecretType
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
@@ -69,6 +70,7 @@ import {
|
|||||||
TDeleteSecretRawDTO,
|
TDeleteSecretRawDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetASecretRawDTO,
|
TGetASecretRawDTO,
|
||||||
|
TGetSecretAccessListDTO,
|
||||||
TGetSecretsDTO,
|
TGetSecretsDTO,
|
||||||
TGetSecretsRawDTO,
|
TGetSecretsRawDTO,
|
||||||
TGetSecretVersionsDTO,
|
TGetSecretVersionsDTO,
|
||||||
@@ -94,7 +96,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
secretV2BridgeService: TSecretV2BridgeServiceFactory;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissions">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
secretQueueService: Pick<
|
secretQueueService: Pick<
|
||||||
TSecretQueueFactory,
|
TSecretQueueFactory,
|
||||||
@@ -113,6 +115,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
TSecretApprovalRequestSecretDALFactory,
|
TSecretApprovalRequestSecretDALFactory,
|
||||||
"insertMany" | "insertApprovalSecretTags"
|
"insertMany" | "insertApprovalSecretTags"
|
||||||
>;
|
>;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
|
||||||
@@ -134,7 +137,8 @@ export const secretServiceFactory = ({
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
secretApprovalRequestService
|
secretApprovalRequestService,
|
||||||
|
licenseService
|
||||||
}: TSecretServiceFactoryDep) => {
|
}: TSecretServiceFactoryDep) => {
|
||||||
const getSecretReference = async (projectId: string) => {
|
const getSecretReference = async (projectId: string) => {
|
||||||
// if bot key missing means e2e still exist
|
// if bot key missing means e2e still exist
|
||||||
@@ -1153,6 +1157,71 @@ export const secretServiceFactory = ({
|
|||||||
return secretV2BridgeService.getSecretReferenceTree(dto);
|
return secretV2BridgeService.getSecretReferenceTree(dto);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecretAccessList = async (dto: TGetSecretAccessListDTO) => {
|
||||||
|
const { environment, secretPath, secretName, projectId } = dto;
|
||||||
|
const plan = await licenseService.getPlan(dto.actorOrgId);
|
||||||
|
if (!plan.secretAccessInsights) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to fetch secret access list due to plan restriction. Upgrade your plan."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = await secretV2BridgeService.getSecretByName({
|
||||||
|
actor: dto.actor,
|
||||||
|
actorId: dto.actorId,
|
||||||
|
actorOrgId: dto.actorOrgId,
|
||||||
|
actorAuthMethod: dto.actorAuthMethod,
|
||||||
|
projectId,
|
||||||
|
secretName,
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
type: "shared"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions(
|
||||||
|
dto.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
const attachAllowedActions = (
|
||||||
|
entityPermission:
|
||||||
|
| (typeof userPermissions)[number]
|
||||||
|
| (typeof identityPermissions)[number]
|
||||||
|
| (typeof groupPermissions)[number]
|
||||||
|
) => {
|
||||||
|
const allowedActions = [
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Edit
|
||||||
|
].filter((action) =>
|
||||||
|
entityPermission.permission.can(
|
||||||
|
action,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
secretTags: secret?.tags?.map((el) => el.slug)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...entityPermission,
|
||||||
|
allowedActions
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const usersWithAccess = userPermissions.map(attachAllowedActions).filter((user) => user.allowedActions.length > 0);
|
||||||
|
const identitiesWithAccess = identityPermissions
|
||||||
|
.map(attachAllowedActions)
|
||||||
|
.filter((identity) => identity.allowedActions.length > 0);
|
||||||
|
const groupsWithAccess = groupPermissions
|
||||||
|
.map(attachAllowedActions)
|
||||||
|
.filter((group) => group.allowedActions.length > 0);
|
||||||
|
|
||||||
|
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
|
||||||
|
};
|
||||||
|
|
||||||
const getSecretsRaw = async ({
|
const getSecretsRaw = async ({
|
||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
@@ -2946,6 +3015,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretsCountMultiEnv,
|
getSecretsCountMultiEnv,
|
||||||
getSecretsRawMultiEnv,
|
getSecretsRawMultiEnv,
|
||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsRawByFolderMappings
|
getSecretsRawByFolderMappings,
|
||||||
|
getSecretAccessList
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -190,6 +190,12 @@ export type TGetSecretsRawDTO = {
|
|||||||
keys?: string[];
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSecretAccessListDTO = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretName: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretRawDTO = {
|
export type TGetASecretRawDTO = {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useToggle } from "@app/hooks/useToggle";
|
|||||||
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
|
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
|
||||||
import {
|
import {
|
||||||
GetSecretVersionsDTO,
|
GetSecretVersionsDTO,
|
||||||
|
SecretAccessListEntry,
|
||||||
SecretType,
|
SecretType,
|
||||||
SecretV3Raw,
|
SecretV3Raw,
|
||||||
SecretV3RawResponse,
|
SecretV3RawResponse,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
TGetProjectSecretsAllEnvDTO,
|
TGetProjectSecretsAllEnvDTO,
|
||||||
TGetProjectSecretsDTO,
|
TGetProjectSecretsDTO,
|
||||||
TGetProjectSecretsKey,
|
TGetProjectSecretsKey,
|
||||||
|
TGetSecretAccessListDTO,
|
||||||
TGetSecretReferenceTreeDTO,
|
TGetSecretReferenceTreeDTO,
|
||||||
TSecretReferenceTraceNode
|
TSecretReferenceTraceNode
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -27,6 +29,13 @@ export const secretKeys = {
|
|||||||
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
|
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
|
||||||
[{ workspaceId, environment, secretPath }, "secrets"] as const,
|
[{ workspaceId, environment, secretPath }, "secrets"] as const,
|
||||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
|
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
|
||||||
|
getSecretAccessList: ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretKey
|
||||||
|
}: TGetSecretAccessListDTO) =>
|
||||||
|
["secret-access-list", { workspaceId, environment, secretPath, secretKey }] as const,
|
||||||
getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto]
|
getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,6 +241,27 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
|||||||
}, [])
|
}, [])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) =>
|
||||||
|
useQuery({
|
||||||
|
enabled: Boolean(dto.secretKey),
|
||||||
|
queryKey: secretKeys.getSecretAccessList(dto),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
groups: SecretAccessListEntry[];
|
||||||
|
identities: SecretAccessListEntry[];
|
||||||
|
users: SecretAccessListEntry[];
|
||||||
|
}>(`/api/v1/secrets/${dto.secretKey}/access-list`, {
|
||||||
|
params: {
|
||||||
|
workspaceId: dto.workspaceId,
|
||||||
|
environment: dto.environment,
|
||||||
|
secretPath: dto.secretPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const fetchSecretReferenceTree = async ({
|
const fetchSecretReferenceTree = async ({
|
||||||
secretPath,
|
secretPath,
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ProjectPermissionActions } from "@app/context";
|
||||||
|
|
||||||
import type { WsTag } from "../tags/types";
|
import type { WsTag } from "../tags/types";
|
||||||
|
|
||||||
export enum SecretType {
|
export enum SecretType {
|
||||||
@@ -125,6 +127,13 @@ export type GetSecretVersionsDTO = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetSecretAccessListDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateSecretsV3DTO = {
|
export type TCreateSecretsV3DTO = {
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
secretValue: string;
|
secretValue: string;
|
||||||
@@ -231,3 +240,10 @@ export type TSecretReferenceTraceNode = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
children: TSecretReferenceTraceNode[];
|
children: TSecretReferenceTraceNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SecretAccessListEntry = {
|
||||||
|
allowedActions: ProjectPermissionActions[];
|
||||||
|
id: string;
|
||||||
|
membershipId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type SubscriptionPlan = {
|
|||||||
workspacesUsed: number;
|
workspacesUsed: number;
|
||||||
environmentLimit: number;
|
environmentLimit: number;
|
||||||
samlSSO: boolean;
|
samlSSO: boolean;
|
||||||
|
secretAccessInsights: boolean;
|
||||||
hsm: boolean;
|
hsm: boolean;
|
||||||
oidcSSO: boolean;
|
oidcSSO: boolean;
|
||||||
scim: boolean;
|
scim: boolean;
|
||||||
|
|||||||
+140
-3
@@ -13,8 +13,10 @@ import {
|
|||||||
} 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";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
@@ -36,10 +38,17 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import {
|
||||||
import { useToggle } from "@app/hooks";
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetSecretVersion } from "@app/hooks/api";
|
import { useGetSecretVersion } from "@app/hooks/api";
|
||||||
|
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { CreateReminderForm } from "./CreateReminderForm";
|
import { CreateReminderForm } from "./CreateReminderForm";
|
||||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||||
@@ -87,7 +96,12 @@ export const SecretDetailSidebar = ({
|
|||||||
values: secret
|
values: secret
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
|
||||||
|
"secretAccessUpgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const tagFields = useFieldArray({
|
const tagFields = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -137,6 +151,13 @@ export const SecretDetailSidebar = ({
|
|||||||
secretId: secret?.id
|
secretId: secret?.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: secretAccessList, isPending } = useGetSecretAccessList({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretKey
|
||||||
|
});
|
||||||
|
|
||||||
const handleOverrideClick = () => {
|
const handleOverrideClick = () => {
|
||||||
if (isOverridden) {
|
if (isOverridden) {
|
||||||
// override need not be flagged delete if it was never saved in server
|
// override need not be flagged delete if it was never saved in server
|
||||||
@@ -191,6 +212,13 @@ export const SecretDetailSidebar = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.secretAccessUpgradePlan.isOpen}
|
||||||
|
onOpenChange={(isUpgradeModalOpen) =>
|
||||||
|
handlePopUpToggle("secretAccessUpgradePlan", isUpgradeModalOpen)
|
||||||
|
}
|
||||||
|
text="Secret access analysis is only available on Infisical's Pro plan and above."
|
||||||
|
/>
|
||||||
<Drawer
|
<Drawer
|
||||||
onOpenChange={(state) => {
|
onOpenChange={(state) => {
|
||||||
if (isOpen && isDirty) {
|
if (isOpen && isDirty) {
|
||||||
@@ -553,8 +581,117 @@ export const SecretDetailSidebar = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||||
|
<div className="mb-2">
|
||||||
|
Access List
|
||||||
|
<Tooltip
|
||||||
|
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{isPending && (
|
||||||
|
<Button className="w-full px-2 py-1" variant="outline_bg" isDisabled>
|
||||||
|
Analyze Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isPending && secretAccessList === undefined && (
|
||||||
|
<Button
|
||||||
|
className="w-full px-2 py-1"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("secretAccessUpgradePlan");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Analyze Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isPending && secretAccessList && (
|
||||||
|
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||||
|
{secretAccessList.users.length > 0 && (
|
||||||
|
<div className="pb-3">
|
||||||
|
<div className="mb-2 font-bold">Users</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{secretAccessList.users.map((user) => (
|
||||||
|
<div className="rounded-md bg-bunker-500 px-1">
|
||||||
|
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
|
||||||
|
}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
membershipId: user.membershipId
|
||||||
|
}}
|
||||||
|
className="text-secondary/80 text-sm hover:text-primary"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{secretAccessList.identities.length > 0 && (
|
||||||
|
<div className="pb-3">
|
||||||
|
<div className="mb-2 font-bold">Identities</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{secretAccessList.identities.map((identity) => (
|
||||||
|
<div className="rounded-md bg-bunker-500 px-1">
|
||||||
|
<Tooltip
|
||||||
|
content={identity.allowedActions.join(", ")}
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
`/${ProjectType.SecretManager}/$projectId/identities/$identityId` as const
|
||||||
|
}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
identityId: identity.id
|
||||||
|
}}
|
||||||
|
className="text-secondary/80 text-sm hover:text-primary"
|
||||||
|
>
|
||||||
|
{identity.name}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{secretAccessList.groups.length > 0 && (
|
||||||
|
<div className="pb-3">
|
||||||
|
<div className="mb-2 font-bold">Groups</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{secretAccessList.groups.map((group) => (
|
||||||
|
<div className="rounded-md bg-bunker-500 px-1">
|
||||||
|
<Tooltip
|
||||||
|
content={group.allowedActions.join(", ")}
|
||||||
|
className="z-[100]"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={"/organization/groups/$groupId" as const}
|
||||||
|
params={{
|
||||||
|
groupId: group.id
|
||||||
|
}}
|
||||||
|
className="text-secondary/80 text-sm hover:text-primary"
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="mb-2 flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
|||||||
Reference in New Issue
Block a user