mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add rbac permissioning support for ssh hosts, render access tree for secrets projects only
This commit is contained in:
@@ -123,8 +123,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(SSH_HOSTS.CREATE.hostCertTtl),
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation
|
||||
allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation
|
||||
loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation
|
||||
allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
@@ -207,8 +207,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(SSH_HOSTS.UPDATE.hostCertTtl),
|
||||
loginMappings: z
|
||||
.object({
|
||||
loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser),
|
||||
allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals)
|
||||
loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser),
|
||||
allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals)
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
|
||||
@@ -67,6 +67,15 @@ export enum ProjectPermissionGroupActions {
|
||||
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 {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -147,6 +156,10 @@ export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type SshHostSubjectFields = {
|
||||
hostname: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -196,7 +209,10 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHosts]
|
||||
| [
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
@@ -295,6 +311,21 @@ const IdentityManagementConditionSchema = z
|
||||
})
|
||||
.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 = [
|
||||
z.object({
|
||||
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."
|
||||
)
|
||||
}),
|
||||
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({
|
||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||
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."
|
||||
).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
|
||||
]);
|
||||
|
||||
@@ -579,8 +614,7 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHosts
|
||||
ProjectPermissionSub.SshCertificateTemplates
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
@@ -593,6 +627,18 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
ProjectPermissionSshHostActions.Delete,
|
||||
ProjectPermissionSshHostActions.IssueUserCert,
|
||||
ProjectPermissionSshHostActions.IssueHostCert
|
||||
],
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
@@ -840,7 +886,9 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshHosts);
|
||||
|
||||
can([ProjectPermissionSshHostActions.IssueUserCert], ProjectPermissionSub.SshHosts);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
@@ -901,7 +949,6 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
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 { 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 { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-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>;
|
||||
|
||||
/**
|
||||
* Checklist:
|
||||
* - check permissions across various roles to make sure it makes sense for the SSH hosts
|
||||
*/
|
||||
|
||||
export const sshHostServiceFactory = ({
|
||||
userDAL,
|
||||
projectDAL,
|
||||
@@ -82,7 +77,8 @@ export const sshHostServiceFactory = ({
|
||||
kmsService
|
||||
}: 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 sshProjects = await projectDAL.find({
|
||||
@@ -90,11 +86,15 @@ export const sshHostServiceFactory = ({
|
||||
type: ProjectType.SSH
|
||||
});
|
||||
|
||||
const projectIdsWithAccess: string[] = [];
|
||||
const principals = await convertActorToPrincipals({
|
||||
actor,
|
||||
actorId,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const allAllowedHosts = [];
|
||||
|
||||
for await (const project of sshProjects) {
|
||||
let hasAccess = false;
|
||||
|
||||
try {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -105,26 +105,22 @@ export const sshHostServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
// TODO: consider glob-based permission items
|
||||
hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
|
||||
} catch {
|
||||
hasAccess = false;
|
||||
}
|
||||
const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals);
|
||||
|
||||
if (hasAccess) {
|
||||
projectIdsWithAccess.push(project.id);
|
||||
const allowedHosts = projectHosts.filter((host) =>
|
||||
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({
|
||||
actor,
|
||||
actorId,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, principals);
|
||||
|
||||
return hosts;
|
||||
return allAllowedHosts;
|
||||
};
|
||||
|
||||
const createSshHost = async ({
|
||||
@@ -149,7 +145,12 @@ export const sshHostServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname
|
||||
})
|
||||
);
|
||||
|
||||
const resolveSshCaId = async ({
|
||||
requestedId,
|
||||
@@ -257,7 +258,12 @@ export const sshHostServiceFactory = ({
|
||||
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) => {
|
||||
await sshHostDAL.updateById(
|
||||
@@ -308,7 +314,12 @@ export const sshHostServiceFactory = ({
|
||||
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 sshHostLoginMappingDAL.delete({ sshHostId }, tx);
|
||||
@@ -335,7 +346,12 @@ export const sshHostServiceFactory = ({
|
||||
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;
|
||||
};
|
||||
@@ -370,9 +386,12 @@ export const sshHostServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
|
||||
|
||||
// TODO: update permissions
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.IssueUserCert,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
const keyId = `${actor}-${actorId}`;
|
||||
|
||||
@@ -488,8 +507,12 @@ export const sshHostServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
|
||||
// TODO: update permissions
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.IssueHostCert,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export const projectRoleServiceFactory = ({
|
||||
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
|
||||
...data,
|
||||
permissions: data.permissions ? data.permissions : undefined
|
||||
|
||||
@@ -75,6 +75,15 @@ export enum ProjectPermissionGroupActions {
|
||||
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 {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -225,7 +234,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHosts]
|
||||
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
TPermissionCondition,
|
||||
TPermissionConditionOperators
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
@@ -98,6 +99,15 @@ const GroupPolicyActionSchema = z.object({
|
||||
[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({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -205,7 +215,12 @@ export const projectRoleFormSchema = z.object({
|
||||
),
|
||||
[ProjectPermissionSub.SshCertificates]: 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.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||
@@ -227,7 +242,8 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.Identity;
|
||||
| ProjectPermissionSub.Identity
|
||||
| ProjectPermissionSub.SshHosts;
|
||||
|
||||
export const isConditionalSubjects = (
|
||||
subject: ProjectPermissionSub
|
||||
@@ -236,7 +252,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.DynamicSecrets ||
|
||||
subject === ProjectPermissionSub.SecretImports ||
|
||||
subject === ProjectPermissionSub.SecretFolders ||
|
||||
subject === ProjectPermissionSub.Identity;
|
||||
subject === ProjectPermissionSub.Identity ||
|
||||
subject === ProjectPermissionSub.SshHosts;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -293,7 +310,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Kms,
|
||||
ProjectPermissionSub.SshHosts,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates
|
||||
@@ -541,7 +557,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canRemoveSecrets)
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -868,10 +910,12 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.SshHosts]: {
|
||||
title: "SSH Hosts",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read", value: ProjectPermissionSshHostActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionSshHostActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete },
|
||||
{ label: "Connect", value: ProjectPermissionSshHostActions.IssueUserCert },
|
||||
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiCollections]: {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
import { SecretPermissionConditions } from "./SecretPermissionConditions";
|
||||
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
|
||||
|
||||
type Props = {
|
||||
roleSlug: string;
|
||||
@@ -51,6 +53,10 @@ export const renderConditionalComponents = (
|
||||
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SshHosts) {
|
||||
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
@@ -134,7 +140,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<AccessTree permissions={formattedPermissions} />
|
||||
{currentWorkspace.type === ProjectType.SecretManager && (
|
||||
<AccessTree permissions={formattedPermissions} />
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user