Add rbac permissioning support for ssh hosts, render access tree for secrets projects only

This commit is contained in:
Tuan Dang
2025-04-08 14:56:05 -07:00
parent bf5e8d8c8b
commit 728c3f56a7
8 changed files with 366 additions and 61 deletions
+4 -4
View File
@@ -123,8 +123,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
.describe(SSH_HOSTS.CREATE.hostCertTtl), .describe(SSH_HOSTS.CREATE.hostCertTtl),
loginMappings: z loginMappings: z
.object({ .object({
loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation
allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation
}) })
.array() .array()
.default([]) .default([])
@@ -207,8 +207,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
.describe(SSH_HOSTS.UPDATE.hostCertTtl), .describe(SSH_HOSTS.UPDATE.hostCertTtl),
loginMappings: z loginMappings: z
.object({ .object({
loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser), loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser),
allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals) allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals)
}) })
.array() .array()
.optional() .optional()
@@ -67,6 +67,15 @@ export enum ProjectPermissionGroupActions {
GrantPrivileges = "grant-privileges" GrantPrivileges = "grant-privileges"
} }
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueUserCert = "issue-user-cert",
IssueHostCert = "issue-host-cert"
}
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -147,6 +156,10 @@ export type IdentityManagementSubjectFields = {
identityId: string; identityId: string;
}; };
export type SshHostSubjectFields = {
hostname: string;
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -196,7 +209,10 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshHosts] | [
ProjectPermissionSshHostActions,
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
@@ -295,6 +311,21 @@ const IdentityManagementConditionSchema = z
}) })
.partial(); .partial();
const SshHostConditionSchema = z
.object({
hostname: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -420,12 +451,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@@ -549,6 +574,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe(
"Describe what action an entity can take."
),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
conditions: SshHostConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -579,8 +614,7 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.PkiCollections, ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates, ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates, ProjectPermissionSub.SshCertificateTemplates
ProjectPermissionSub.SshHosts
].forEach((el) => { ].forEach((el) => {
can( can(
[ [
@@ -593,6 +627,18 @@ const buildAdminPermissionRules = () => {
); );
}); });
can(
[
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueUserCert,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
);
can( can(
[ [
ProjectPermissionMemberActions.Create, ProjectPermissionMemberActions.Create,
@@ -840,7 +886,9 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshHosts);
can([ProjectPermissionSshHostActions.IssueUserCert], ProjectPermissionSub.SshHosts);
can( can(
[ [
ProjectPermissionCmekActions.Create, ProjectPermissionCmekActions.Create,
@@ -901,7 +949,6 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
return rules; return rules;
@@ -1,8 +1,8 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { ActionProjectType, ProjectType } from "@app/db/schemas";
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 { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
@@ -63,11 +63,6 @@ type TSshCertificateAuthorityServiceFactoryDep = {
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>; export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
/**
* Checklist:
* - check permissions across various roles to make sure it makes sense for the SSH hosts
*/
export const sshHostServiceFactory = ({ export const sshHostServiceFactory = ({
userDAL, userDAL,
projectDAL, projectDAL,
@@ -82,7 +77,8 @@ export const sshHostServiceFactory = ({
kmsService kmsService
}: TSshCertificateAuthorityServiceFactoryDep) => { }: TSshCertificateAuthorityServiceFactoryDep) => {
/** /**
* Return list of all SSH hosts that a user has access to across all SSH projects in the organization * Return list of all SSH hosts that a user can issue user SSH certificates for
* (i.e. is able to access / connect to) across all SSH projects in the organization
*/ */
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => { const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
const sshProjects = await projectDAL.find({ const sshProjects = await projectDAL.find({
@@ -90,11 +86,15 @@ export const sshHostServiceFactory = ({
type: ProjectType.SSH type: ProjectType.SSH
}); });
const projectIdsWithAccess: string[] = []; const principals = await convertActorToPrincipals({
actor,
actorId,
userDAL
});
const allAllowedHosts = [];
for await (const project of sshProjects) { for await (const project of sshProjects) {
let hasAccess = false;
try { try {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -105,26 +105,22 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
// TODO: consider glob-based permission items const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals);
hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
} catch {
hasAccess = false;
}
if (hasAccess) { const allowedHosts = projectHosts.filter((host) =>
projectIdsWithAccess.push(project.id); permission.can(
ProjectPermissionSshHostActions.IssueUserCert,
subject(ProjectPermissionSub.SshHosts, { hostname: host.hostname })
)
);
allAllowedHosts.push(...allowedHosts);
} catch {
// intentionally ignore projects where user lacks access
} }
} }
const principals = await convertActorToPrincipals({ return allAllowedHosts;
actor,
actorId,
userDAL
});
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, principals);
return hosts;
}; };
const createSshHost = async ({ const createSshHost = async ({
@@ -149,7 +145,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Create,
subject(ProjectPermissionSub.SshHosts, {
hostname
})
);
const resolveSshCaId = async ({ const resolveSshCaId = async ({
requestedId, requestedId,
@@ -257,7 +258,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Edit,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
const updatedHost = await sshHostDAL.transaction(async (tx) => { const updatedHost = await sshHostDAL.transaction(async (tx) => {
await sshHostDAL.updateById( await sshHostDAL.updateById(
@@ -308,7 +314,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Delete,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
await sshHostDAL.transaction(async (tx) => { await sshHostDAL.transaction(async (tx) => {
await sshHostLoginMappingDAL.delete({ sshHostId }, tx); await sshHostLoginMappingDAL.delete({ sshHostId }, tx);
@@ -335,7 +346,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Read,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
return host; return host;
}; };
@@ -370,9 +386,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.IssueUserCert,
// TODO: update permissions subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
const keyId = `${actor}-${actorId}`; const keyId = `${actor}-${actorId}`;
@@ -488,8 +507,12 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); ForbiddenError.from(permission).throwUnlessCan(
// TODO: update permissions ProjectPermissionSshHostActions.IssueHostCert,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId }); const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
@@ -141,6 +141,7 @@ export const projectRoleServiceFactory = ({
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.") allowedExpressions: (val) => val.includes("identity.")
}); });
const updatedRole = await projectRoleDAL.updateById(projectRole.id, { const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
...data, ...data,
permissions: data.permissions ? data.permissions : undefined permissions: data.permissions ? data.permissions : undefined
@@ -75,6 +75,15 @@ export enum ProjectPermissionGroupActions {
GrantPrivileges = "grant-privileges" GrantPrivileges = "grant-privileges"
} }
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueUserCert = "issue-user-cert",
IssueHostCert = "issue-host-cert"
}
export enum PermissionConditionOperators { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all", $ALL = "$all",
@@ -225,7 +234,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.SshHosts] | [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
@@ -18,6 +18,7 @@ import {
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSshHostActions,
TPermissionCondition, TPermissionCondition,
TPermissionConditionOperators TPermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
@@ -98,6 +99,15 @@ const GroupPolicyActionSchema = z.object({
[ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional() [ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional()
}); });
const SshHostPolicyActionSchema = z.object({
[ProjectPermissionSshHostActions.Read]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
[ProjectPermissionSshHostActions.IssueUserCert]: z.boolean().optional(),
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({ const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
create: z.boolean().optional() create: z.boolean().optional()
@@ -205,7 +215,12 @@ export const projectRoleFormSchema = z.object({
), ),
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshHosts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshHosts]: SshHostPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
@@ -227,7 +242,8 @@ type TConditionalFields =
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets | ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity; | ProjectPermissionSub.Identity
| ProjectPermissionSub.SshHosts;
export const isConditionalSubjects = ( export const isConditionalSubjects = (
subject: ProjectPermissionSub subject: ProjectPermissionSub
@@ -236,7 +252,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.DynamicSecrets || subject === ProjectPermissionSub.DynamicSecrets ||
subject === ProjectPermissionSub.SecretImports || subject === ProjectPermissionSub.SecretImports ||
subject === ProjectPermissionSub.SecretFolders || subject === ProjectPermissionSub.SecretFolders ||
subject === ProjectPermissionSub.Identity; subject === ProjectPermissionSub.Identity ||
subject === ProjectPermissionSub.SshHosts;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = []; const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -293,7 +310,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.Tags, ProjectPermissionSub.Tags,
ProjectPermissionSub.SecretRotation, ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Kms, ProjectPermissionSub.Kms,
ProjectPermissionSub.SshHosts,
ProjectPermissionSub.SshCertificateTemplates, ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates ProjectPermissionSub.SshCertificates
@@ -541,7 +557,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canRemoveSecrets) if (canRemoveSecrets)
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true; formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
} }
if (subject === ProjectPermissionSub.SshHosts) {
const canRead = action.includes(ProjectPermissionSshHostActions.Read);
const canEdit = action.includes(ProjectPermissionSshHostActions.Edit);
const canDelete = action.includes(ProjectPermissionSshHostActions.Delete);
const canCreate = action.includes(ProjectPermissionSshHostActions.Create);
const canIssueUserCert = action.includes(ProjectPermissionSshHostActions.IssueUserCert);
const canIssueHostCert = action.includes(ProjectPermissionSshHostActions.IssueHostCert);
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
if (canRead) formVal[subject]![0][ProjectPermissionSshHostActions.Read] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionSshHostActions.Edit] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionSshHostActions.Delete] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionSshHostActions.Create] = true;
if (canIssueUserCert)
formVal[subject]![0][ProjectPermissionSshHostActions.IssueUserCert] = true;
if (canIssueHostCert)
formVal[subject]![0][ProjectPermissionSshHostActions.IssueHostCert] = true;
formVal[subject]![0].conditions = conditions
? convertCaslConditionToFormOperator(conditions)
: [];
formVal[subject]![0].inverted = inverted;
}
}); });
return formVal; return formVal;
}; };
@@ -868,10 +910,12 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.SshHosts]: { [ProjectPermissionSub.SshHosts]: {
title: "SSH Hosts", title: "SSH Hosts",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: ProjectPermissionSshHostActions.Read },
{ label: "Create", value: "create" }, { label: "Create", value: ProjectPermissionSshHostActions.Create },
{ label: "Modify", value: "edit" }, { label: "Modify", value: ProjectPermissionSshHostActions.Edit },
{ label: "Remove", value: "delete" } { label: "Remove", value: ProjectPermissionSshHostActions.Delete },
{ label: "Connect", value: ProjectPermissionSshHostActions.IssueUserCert },
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
] ]
}, },
[ProjectPermissionSub.PkiCollections]: { [ProjectPermissionSub.PkiCollections]: {
@@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { evaluatePermissionsAbility } from "@app/helpers/permissions";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -33,6 +34,7 @@ import {
TFormSchema TFormSchema
} from "./ProjectRoleModifySection.utils"; } from "./ProjectRoleModifySection.utils";
import { SecretPermissionConditions } from "./SecretPermissionConditions"; import { SecretPermissionConditions } from "./SecretPermissionConditions";
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
type Props = { type Props = {
roleSlug: string; roleSlug: string;
@@ -51,6 +53,10 @@ export const renderConditionalComponents = (
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />; return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
} }
if (subject === ProjectPermissionSub.SshHosts) {
return <SshHostPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />; return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
} }
@@ -134,7 +140,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
return ( return (
<div className="w-full"> <div className="w-full">
<AccessTree permissions={formattedPermissions} /> {currentWorkspace.type === ProjectType.SecretManager && (
<AccessTree permissions={formattedPermissions} />
)}
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
@@ -0,0 +1,173 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.SshHosts;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
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>
<p className="text-sm text-mineshaft-400">
Conditions determine when a policy will be applied (always if no conditions are present).
</p>
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="hostname">Hostname</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "hostname",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};