Merge pull request #1874 from akhilmhdh/feat/tf-role-sp-changes

Updates api endpoints for project role and identity specfic privilege
This commit is contained in:
Maidul Islam
2024-05-28 16:59:44 -04:00
committed by GitHub
29 changed files with 766 additions and 464 deletions
@@ -5,10 +5,15 @@ import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; import {
ProjectPermissionSchema,
ProjectSpecificPrivilegePermissionSchema,
SanitizedIdentityPrivilegeSchema
} from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.create({ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false, isTemporary: false,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: JSON.stringify(packRules(permission))
}); });
return { privilege }; return { privilege };
} }
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
).optional(),
temporaryMode: z temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.create({ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true, isTemporary: true,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: JSON.stringify(packRules(permission))
}); });
return { privilege }; return { privilege };
} }
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
).optional(),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z temporaryRange: z
.string() .string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number") .refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z temporaryAccessStartTime: z
.string() .string()
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const updatedInfo = req.body.privilegeDetails; const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug, projectSlug: req.body.projectSlug,
data: { data: {
...updatedInfo, ...updatedInfo,
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined permissions: permission ? JSON.stringify(packRules(permission)) : undefined
} }
}); });
return { privilege }; return { privilege };
+1 -1
View File
@@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.min(1) .min(1)
.trim() .trim()
.refine( .refine(
(val) => !Object.keys(OrgMembershipRole).includes(val), (val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((v) => slugify(v) === v, { .refine((v) => slugify(v) === v, {
+150 -64
View File
@@ -1,146 +1,232 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:projectId/roles", url: "/:projectSlug/roles",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Create a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
projectId: z.string().trim() projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}), }),
body: z.object({ body: z.object({
slug: z.string().trim(), slug: z
name: z.string().trim(), .string()
description: z.string().trim().optional(), .toLowerCase()
permissions: z.any().array() .trim()
.min(1)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions)
}), }),
response: { response: {
200: z.object({ 200: z.object({
role: ProjectRolesSchema role: SanitizedRoleSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const role = await server.services.projectRole.createRole( const role = await server.services.projectRole.createRole({
req.permission.type, actorAuthMethod: req.permission.authMethod,
req.permission.id, actorId: req.permission.id,
req.params.projectId, actorOrgId: req.permission.orgId,
req.body, actor: req.permission.type,
req.permission.authMethod, projectSlug: req.params.projectSlug,
req.permission.orgId data: {
); ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
}
});
return { role }; return { role };
} }
}); });
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:projectId/roles/:roleId", url: "/:projectSlug/roles/:roleId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Update a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
projectId: z.string().trim(), projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
roleId: z.string().trim() roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}), }),
body: z.object({ body: z.object({
slug: z.string().trim().optional(), slug: z
name: z.string().trim().optional(), .string()
description: z.string().trim().optional(), .toLowerCase()
permissions: z.any().array() .trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine(
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
}), }),
response: { response: {
200: z.object({ 200: z.object({
role: ProjectRolesSchema role: SanitizedRoleSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const role = await server.services.projectRole.updateRole( const role = await server.services.projectRole.updateRole({
req.permission.type, actorAuthMethod: req.permission.authMethod,
req.permission.id, actorId: req.permission.id,
req.params.projectId, actorOrgId: req.permission.orgId,
req.params.roleId, actor: req.permission.type,
req.body, projectSlug: req.params.projectSlug,
req.permission.authMethod, roleId: req.params.roleId,
req.permission.orgId data: {
); ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
}
});
return { role }; return { role };
} }
}); });
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:projectId/roles/:roleId", url: "/:projectSlug/roles/:roleId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Delete a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
projectId: z.string().trim(), projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
roleId: z.string().trim() roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
role: ProjectRolesSchema role: SanitizedRoleSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const role = await server.services.projectRole.deleteRole( const role = await server.services.projectRole.deleteRole({
req.permission.type, actorAuthMethod: req.permission.authMethod,
req.permission.id, actorId: req.permission.id,
req.params.projectId, actorOrgId: req.permission.orgId,
req.params.roleId, actor: req.permission.type,
req.permission.authMethod, projectSlug: req.params.projectSlug,
req.permission.orgId roleId: req.params.roleId
); });
return { role }; return { role };
} }
}); });
server.route({ server.route({
method: "GET", method: "GET",
url: "/:projectId/roles", url: "/:projectSlug/roles",
config: {
rateLimit: readLimit
},
schema: {
description: "List project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const roles = await server.services.projectRole.listRoles({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
projectSlug: req.params.projectSlug
});
return { roles };
}
});
server.route({
method: "GET",
url: "/:projectSlug/roles/slug/:slug",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim() projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
data: z.object({ role: SanitizedRoleSchema
roles: ProjectRolesSchema.omit({ permissions: true })
.merge(z.object({ permissions: z.unknown() }))
.array()
})
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const roles = await server.services.projectRole.listRoles( const role = await server.services.projectRole.getRoleBySlug({
req.permission.type, actorAuthMethod: req.permission.authMethod,
req.permission.id, actorId: req.permission.id,
req.params.projectId, actorOrgId: req.permission.orgId,
req.permission.authMethod, actor: req.permission.type,
req.permission.orgId projectSlug: req.params.projectSlug,
); roleSlug: req.params.slug
return { data: { roles } }; });
return { role };
} }
}); });
+35 -2
View File
@@ -519,7 +519,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to create.", identityId: "The ID of the identity to create.",
slug: "The slug of the privilege to create.", slug: "The slug of the privilege to create.",
permissions: `The permission object for the privilege. permissions: `@deprecated - use privilegePermission
The permission object for the privilege.
- Read secrets - Read secrets
\`\`\` \`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]} { "permissions": [{"action": "read", "subject": "secrets"]}
@@ -533,6 +534,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] } - { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\` \`\`\`
`, `,
privilegePermission: "The permission object for the privilege.",
isPackPermission: "Whether the server should pack(compact) the permission object.", isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.", isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative", temporaryMode: "Type of temporary access given. Types: relative",
@@ -544,7 +546,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
identityId: "The ID of the identity to update.", identityId: "The ID of the identity to update.",
slug: "The slug of the privilege to update.", slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.", newSlug: "The new slug of the privilege to update.",
permissions: `The permission object for the privilege. permissions: `@deprecated - use privilegePermission
The permission object for the privilege.
- Read secrets - Read secrets
\`\`\` \`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]} { "permissions": [{"action": "read", "subject": "secrets"]}
@@ -558,6 +561,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] } - { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\` \`\`\`
`, `,
privilegePermission: "The permission object for the privilege.",
isTemporary: "Whether the privilege is temporary.", isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative", temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
@@ -715,3 +719,32 @@ export const AUDIT_LOG_STREAMS = {
id: "The ID of the audit log stream to get details." id: "The ID of the audit log stream to get details."
} }
}; };
export const PROJECT_ROLE = {
CREATE: {
projectSlug: "The slug of the project to create role.",
slug: "The slug of the role.",
name: "The name of the role.",
description: "The description for the role.",
permissions: "The permissions assigned to the role."
},
UPDATE: {
projectSlug: "The slug of the project to update role.",
roleId: "The ID of the role to update",
slug: "The slug of the role.",
name: "The name of the role.",
description: "The description for the role.",
permissions: "The permissions assigned to the role."
},
DELETE: {
projectSlug: "The slug of the project to delete role.",
roleId: "The ID of the role to update"
},
GET_ROLE_BY_SLUG: {
projectSlug: "The slug of the project.",
roleSlug: "The slug of the role to get details"
},
LIST: {
projectSlug: "The slug of the project to list roles."
}
};
+2 -1
View File
@@ -523,7 +523,8 @@ export const registerRoutes = async (
permissionService, permissionService,
projectRoleDAL, projectRoleDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL identityProjectMembershipRoleDAL,
projectDAL
}); });
const snapshotService = secretSnapshotServiceFactory({ const snapshotService = secretSnapshotServiceFactory({
@@ -4,6 +4,7 @@ import {
DynamicSecretsSchema, DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema, IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema, IntegrationAuthsSchema,
ProjectRolesSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
.optional() .optional()
}); });
export const ProjectSpecificPrivilegePermissionSchema = z.object({
actions: z
.nativeEnum(ProjectPermissionActions)
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read")
.array()
.min(1),
subject: z
.enum([ProjectPermissionSub.Secrets])
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
conditions: z
.object({
environment: z.string().describe("The environment slug this permission should allow."),
secretPath: z
.object({
$glob: z
.string()
.min(1)
.describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
})
.optional()
})
.describe("When specified, only matching conditions will be allowed to access given resource.")
});
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({ export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array() permissions: UnpackedPermissionSchema.array()
}); });
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, inputIV: true,
inputTag: true, inputTag: true,
@@ -1,25 +1,30 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ProjectMembershipRole, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
projectAdminPermissions, projectAdminPermissions,
projectMemberPermissions, projectMemberPermissions,
projectNoAccessPermissions, projectNoAccessPermissions,
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSet,
ProjectPermissionSub, ProjectPermissionSub,
projectViewerPermission projectViewerPermission
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod } from "../auth/auth-type";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal";
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
type TProjectRoleServiceFactoryDep = { type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory; projectRoleDAL: TProjectRoleDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
@@ -27,20 +32,68 @@ type TProjectRoleServiceFactoryDep = {
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>; export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
permissions: projectAdminPermissions,
description: "Complete administration access over the project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
permissions: projectMemberPermissions,
description: "Non-administrative role in an project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
permissions: projectViewerPermission,
description: "Non-administrative role in an project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
permissions: projectNoAccessPermissions,
description: "No access to any resources in the project",
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
};
export const projectRoleServiceFactory = ({ export const projectRoleServiceFactory = ({
projectRoleDAL, projectRoleDAL,
permissionService, permissionService,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL projectUserMembershipRoleDAL,
projectDAL
}: TProjectRoleServiceFactoryDep) => { }: TProjectRoleServiceFactoryDep) => {
const createRole = async ( const createRole = async ({ projectSlug, data, actor, actorId, actorAuthMethod, actorOrgId }: TCreateRoleDTO) => {
actor: ActorType, const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
actorId: string, if (!project) throw new BadRequestError({ message: "Project not found" });
projectId: string, const projectId = project.id;
data: Omit<TProjectRolesInsert, "projectId">,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -53,21 +106,54 @@ export const projectRoleServiceFactory = ({
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await projectRoleDAL.create({ const role = await projectRoleDAL.create({
...data, ...data,
projectId, projectId
permissions: JSON.stringify(data.permissions)
}); });
return role; return { ...role, permissions: unpackPermissions(role.permissions) };
}; };
const updateRole = async ( const getRoleBySlug = async ({
actor: ActorType, actor,
actorId: string, actorId,
projectId: string, projectSlug,
roleId: string, actorAuthMethod,
data: Omit<TOrgRolesUpdate, "orgId">, actorOrgId,
actorAuthMethod: ActorAuthMethod, roleSlug
actorOrgId: string | undefined }: TGetRoleBySlugDTO) => {
) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0];
return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) };
}
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId });
if (!customRole) throw new BadRequestError({ message: "Role not found" });
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
};
const updateRole = async ({
roleId,
projectSlug,
actorOrgId,
actorAuthMethod,
actorId,
actor,
data
}: TUpdateRoleDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -81,22 +167,16 @@ export const projectRoleServiceFactory = ({
if (existingRole && existingRole.id !== roleId) if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" }); throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
} }
const [updatedRole] = await projectRoleDAL.update( const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data);
{ id: roleId, projectId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole; return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
}; };
const deleteRole = async ( const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, projectSlug, roleId }: TDeleteRoleDTO) => {
actor: ActorType, const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
actorId: string, if (!project) throw new BadRequestError({ message: "Project not found" });
projectId: string, const projectId = project.id;
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -125,16 +205,14 @@ export const projectRoleServiceFactory = ({
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" }); if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
return deletedRole; return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
}; };
const listRoles = async ( const listRoles = async ({ projectSlug, actorOrgId, actorAuthMethod, actorId, actor }: TListRolesDTO) => {
actor: ActorType, const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
actorId: string, if (!project) throw new BadRequestError({ message: "Project not found" });
projectId: string, const projectId = project.id;
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -144,52 +222,7 @@ export const projectRoleServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
const customRoles = await projectRoleDAL.find({ projectId }); const customRoles = await projectRoleDAL.find({ projectId });
const roles = [ const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])];
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
description: "Complete administration access over the project",
permissions: packRules(projectAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
description: "Non-administrative role in an project",
permissions: packRules(projectMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
description: "Non-administrative role in an project",
permissions: packRules(projectViewerPermission),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the project",
permissions: packRules(projectNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles; return roles;
}; };
@@ -209,5 +242,5 @@ export const projectRoleServiceFactory = ({
return { permissions: packRules(permission.rules), membership }; return { permissions: packRules(permission.rules), membership };
}; };
return { createRole, updateRole, deleteRole, listRoles, getUserPermission }; return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
}; };
@@ -0,0 +1,27 @@
import { TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TCreateRoleDTO = {
data: Omit<TProjectRolesInsert, "projectId">;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetRoleBySlugDTO = {
roleSlug: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateRoleDTO = {
roleId: string;
data: Omit<TOrgRolesUpdate, "orgId">;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteRoleDTO = {
roleId: string;
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TListRolesDTO = {
projectSlug: string;
} & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/workspace/{projectSlug}/roles"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/workspace/{projectSlug}/roles/{roleId}"
---
@@ -0,0 +1,4 @@
---
title: "Get By Slug"
openapi: "GET /api/v1/workspace/{projectSlug}/roles/slug/{slug}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/workspace/{projectSlug}/roles"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/workspace/{projectSlug}/roles/{roleId}"
---
+10
View File
@@ -476,6 +476,16 @@
"api-reference/endpoints/project-identities/delete-identity-membership" "api-reference/endpoints/project-identities/delete-identity-membership"
] ]
}, },
{
"group": "Project Roles",
"pages": [
"api-reference/endpoints/project-roles/create",
"api-reference/endpoints/project-roles/update",
"api-reference/endpoints/project-roles/delete",
"api-reference/endpoints/project-roles/get-by-slug",
"api-reference/endpoints/project-roles/list"
]
},
{ {
"group": "Environments", "group": "Environments",
"pages": [ "pages": [
@@ -12,21 +12,30 @@ export type TIdentityProjectPrivilege = {
updatedAt: Date; updatedAt: Date;
permissions?: TProjectPermission[]; permissions?: TProjectPermission[];
} & ( } & (
| { | {
isTemporary: true; isTemporary: true;
temporaryMode: string; temporaryMode: string;
temporaryRange: string; temporaryRange: string;
temporaryAccessStartTime: string; temporaryAccessStartTime: string;
temporaryAccessEndTime?: string; temporaryAccessEndTime?: string;
} }
| { | {
isTemporary: false; isTemporary: false;
temporaryMode?: null; temporaryMode?: null;
temporaryRange?: null; temporaryRange?: null;
temporaryAccessStartTime?: null; temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null; temporaryAccessEndTime?: null;
} }
); );
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TCreateIdentityProjectPrivilegeDTO = { export type TCreateIdentityProjectPrivilegeDTO = {
identityId: string; identityId: string;
@@ -36,14 +45,16 @@ export type TCreateIdentityProjectPrivilegeDTO = {
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode; temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
temporaryRange?: string; temporaryRange?: string;
temporaryAccessStartTime?: string; temporaryAccessStartTime?: string;
permissions: TProjectPermission[]; privilegePermission: TProjectSpecificPrivilegePermission;
}; };
export type TUpdateIdentityProjectPrivlegeDTO = { export type TUpdateIdentityProjectPrivlegeDTO = {
projectSlug: string; projectSlug: string;
identityId: string; identityId: string;
privilegeSlug: string; privilegeSlug: string;
privilegeDetails: Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>; privilegeDetails: Partial<
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
>;
}; };
export type TDeleteIdentityProjectPrivilegeDTO = { export type TDeleteIdentityProjectPrivilegeDTO = {
+1
View File
@@ -8,6 +8,7 @@ export {
} from "./mutation"; } from "./mutation";
export { export {
useGetOrgRoles, useGetOrgRoles,
useGetProjectRoleBySlug,
useGetProjectRoles, useGetProjectRoles,
useGetUserOrgPermissions, useGetUserOrgPermissions,
useGetUserProjectPermissions useGetUserProjectPermissions
+12 -20
View File
@@ -17,13 +17,10 @@ export const useCreateProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) => mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
apiRequest.post(`/api/v1/workspace/${projectId}/roles`, { apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
...dto, onSuccess: (_, { projectSlug }) => {
permissions: permissions.length ? packRules(permissions) : [] queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
@@ -32,13 +29,10 @@ export const useUpdateProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) => mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, { apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
...dto, onSuccess: (_, { projectSlug }) => {
permissions: permissions?.length ? packRules(permissions) : [] queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
@@ -47,12 +41,10 @@ export const useDeleteProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) => mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, { apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
data: { projectId } onSuccess: (_, { projectSlug }) => {
}), queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
+23 -13
View File
@@ -14,7 +14,6 @@ import {
TGetUserProjectPermissionDTO, TGetUserProjectPermissionDTO,
TOrgRole, TOrgRole,
TPermission, TPermission,
TProjectPermission,
TProjectRole TProjectRole
} from "./types"; } from "./types";
@@ -37,7 +36,9 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export const roleQueryKeys = { export const roleQueryKeys = {
getProjectRoles: (projectId: string) => ["roles", { projectId }] as const, getProjectRoles: (projectSlug: string) => ["roles", { projectSlug }] as const,
getProjectRoleBySlug: (projectSlug: string, roleSlug: string) =>
["roles", { projectSlug, roleSlug }] as const,
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const, getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const, ["user-permissions", { orgId }] as const,
@@ -46,20 +47,29 @@ export const roleQueryKeys = {
}; };
const getProjectRoles = async (projectId: string) => { const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
data: { roles: Array<Omit<TProjectRole, "permissions"> & { permissions: unknown }> }; `/api/v1/workspace/${projectId}/roles`
}>(`/api/v1/workspace/${projectId}/roles`); );
return data.data.roles.map(({ permissions, ...el }) => ({ return data.roles;
...el,
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
}));
}; };
export const useGetProjectRoles = (projectId: string) => export const useGetProjectRoles = (projectSlug: string) =>
useQuery({ useQuery({
queryKey: roleQueryKeys.getProjectRoles(projectId), queryKey: roleQueryKeys.getProjectRoles(projectSlug),
queryFn: () => getProjectRoles(projectId), queryFn: () => getProjectRoles(projectSlug),
enabled: Boolean(projectId) enabled: Boolean(projectSlug)
});
export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =>
useQuery({
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
queryFn: async () => {
const { data } = await apiRequest.get<{ role: TProjectRole }>(
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}`
);
return data.role;
},
enabled: Boolean(projectSlug && roleSlug)
}); });
const getOrgRoles = async (orgId: string) => { const getOrgRoles = async (orgId: string) => {
+3 -3
View File
@@ -71,7 +71,7 @@ export type TDeleteOrgRoleDTO = {
}; };
export type TCreateProjectRoleDTO = { export type TCreateProjectRoleDTO = {
projectId: string; projectSlug: string;
name: string; name: string;
description?: string; description?: string;
slug: string; slug: string;
@@ -79,11 +79,11 @@ export type TCreateProjectRoleDTO = {
}; };
export type TUpdateProjectRoleDTO = { export type TUpdateProjectRoleDTO = {
projectId: string; projectSlug: string;
id: string; id: string;
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>; } & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
export type TDeleteProjectRoleDTO = { export type TDeleteProjectRoleDTO = {
projectId: string; projectSlug: string;
id: string; id: string;
}; };
@@ -5,25 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
Button,
FormControl,
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
useAddGroupToWorkspace, useAddGroupToWorkspace,
useGetOrganizationGroups, useGetOrganizationGroups,
useGetProjectRoles, useGetProjectRoles,
useListWorkspaceGroups, useListWorkspaceGroups
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({ const schema = z.object({
slug: z.string(), slug: z.string(),
role: z.string() role: z.string()
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -33,150 +27,146 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
}; };
export const GroupModal = ({ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
popUp, const { currentOrg } = useOrganization();
handlePopUpToggle const { currentWorkspace } = useWorkspace();
}: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: groups } = useGetOrganizationGroups(orgId); const { data: groups } = useGetOrganizationGroups(orgId);
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || ""); const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { data: roles } = useGetProjectRoles(workspaceId); const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace(); const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
const filteredGroupMembershipOrgs = useMemo(() => { const filteredGroupMembershipOrgs = useMemo(() => {
const wsGroupIds = new Map(); const wsGroupIds = new Map();
groupMemberships?.forEach((groupMembership) => { groupMemberships?.forEach((groupMembership) => {
wsGroupIds.set(groupMembership.group.id, true); wsGroupIds.set(groupMembership.group.id, true);
}); });
return (groups || []).filter(({ id }) => !wsGroupIds.has(id)); return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
}, [groups, groupMemberships]); }, [groups, groupMemberships]);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema) resolver: zodResolver(schema)
});
const onFormSubmit = async ({ slug, role }: FormData) => {
try {
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug: slug,
role: role || undefined
}); });
const onFormSubmit = async ({ slug, role }: FormData) => { reset();
try { handlePopUpToggle("group", false);
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug: slug,
role: role || undefined
});
reset(); createNotification({
handlePopUpToggle("group", false); text: "Successfully added group to project",
type: "success"
createNotification({ });
text: "Successfully added group to project", } catch (err) {
type: "success" createNotification({
}); text: "Failed to add group to project",
type: "error"
} catch (err) { });
createNotification({
text: "Failed to add group to project",
type: "error"
});
}
} }
};
return ( return (
<Modal <Modal
isOpen={popUp?.group?.isOpen} isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen); handlePopUpToggle("group", isOpen);
reset(); reset();
}} }}
> >
<ModalContent title="Add Group to Project"> <ModalContent title="Add Group to Project">
{filteredGroupMembershipOrgs.length ? ( {filteredGroupMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
name="slug" name="slug"
defaultValue={filteredGroupMembershipOrgs?.[0]?.id} defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}> <FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
> >
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => ( {filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
<SelectItem value={slug} key={`org-group-${id}`}> <SelectItem value={slug} key={`org-group-${id}`}>
{name} {name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="role" name="role"
defaultValue="" defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Role" label="Role"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mt-4" className="mt-4"
> >
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
> >
{(roles || []).map(({ name, slug }) => ( {(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}> <SelectItem value={slug} key={`st-role-${slug}`}>
{name} {name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
)} )}
/> />
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"
type="submit" type="submit"
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={isSubmitting} isDisabled={isSubmitting}
> >
{popUp?.group?.data ? "Update" : "Create"} {popUp?.group?.data ? "Update" : "Create"}
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
Cancel Cancel
</Button> </Button>
</div> </div>
</form> </form>
) : ( ) : (
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<div className="text-sm"> <div className="text-sm">
All groups in your organization have already been added to this project. All groups in your organization have already been added to this project.
</div> </div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}> <Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Create a new group</Button> <Button variant="outline_bg">Create a new group</Button>
</Link> </Link>
</div> </div>
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
} };
@@ -201,11 +201,7 @@ export type TMemberRolesProp = {
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({ export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => {
roles = [],
disableEdit = false,
groupSlug
}: TMemberRolesProp) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
const [searchRoles, setSearchRoles] = useState(""); const [searchRoles, setSearchRoles] = useState("");
@@ -220,9 +216,9 @@ export const GroupRoles = ({
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
@@ -317,7 +313,7 @@ export const GroupRoles = ({
icon={faClock} icon={faClock}
className={twMerge( className={twMerge(
new Date() > new Date(temporaryAccessEndTime as string) && new Date() > new Date(temporaryAccessEndTime as string) &&
"text-red-600" "text-red-600"
)} )}
/> />
</Tooltip> </Tooltip>
@@ -390,14 +386,14 @@ export const GroupRoles = ({
defaultValue={ defaultValue={
userProjectRoleDetails?.isTemporary userProjectRoleDetails?.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryAccessStartTime: temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string, userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange: temporaryRange:
userProjectRoleDetails.temporaryRange as string, userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime: temporaryAccessEndTime:
userProjectRoleDetails.temporaryAccessEndTime userProjectRoleDetails.temporaryAccessEndTime
} }
: false : false
} }
render={({ field }) => ( render={({ field }) => (
@@ -30,17 +30,17 @@ type Props = {
}; };
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId); const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId); const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
const { data: roles } = useGetProjectRoles(workspaceId); const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
@@ -65,7 +65,8 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const isMemberEditDisabled = permission.cannot( const isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
slug: customRoleSlug || role, slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary temporaryAccess: dto.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryRange: dto.temporaryRange, temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime, temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime temporaryAccessStartTime: dto.temporaryAccessStartTime
} }
: { : {
isTemporary: dto.isTemporary isTemporary: dto.isTemporary
} }
})) }))
} }
}); });
@@ -191,9 +192,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired ? isExpired
? "Timed Access Expired" ? "Timed Access Expired"
: `Until ${format( : `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss" "yyyy-MM-dd HH:mm:ss"
)}` )}`
: "Non expiry access" : "Non expiry access"
} }
> >
@@ -212,9 +213,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired ? isExpired
? "Access Expired" ? "Access Expired"
: formatDistance( : formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date() new Date()
) )
: "Permanent"} : "Permanent"}
</Button> </Button>
</Tooltip> </Tooltip>
@@ -338,7 +339,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
type="submit" type="submit"
className={twMerge( className={twMerge(
"transition-all", "transition-all",
"opacity-0 cursor-default", "cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100" roleForm.formState.isDirty && "cursor-pointer opacity-100"
)} )}
isDisabled={!roleForm.formState.isDirty} isDisabled={!roleForm.formState.isDirty}
@@ -131,20 +131,17 @@ const SpecificPrivilegeSecretForm = ({
{ action: ProjectPermissionActions.Delete, allowed: data.delete }, { action: ProjectPermissionActions.Delete, allowed: data.delete },
{ action: ProjectPermissionActions.Edit, allowed: data.edit } { action: ProjectPermissionActions.Edit, allowed: data.edit }
]; ];
const conditions: Record<string, any> = { environment: data.environmentSlug };
if (data.secretPath) {
conditions.secretPath = { $glob: data.secretPath };
}
await updateIdentityPrivilege.mutateAsync({ await updateIdentityPrivilege.mutateAsync({
privilegeDetails: { privilegeDetails: {
...data.temporaryAccess, ...data.temporaryAccess,
permissions: actions privilegePermission: {
.filter(({ allowed }) => allowed) actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
.map(({ action }) => ({ subject: ProjectPermissionSub.Secrets,
action, conditions: {
subject: ProjectPermissionSub.Secrets, environment: data.environmentSlug,
conditions ...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
})) }
}
}, },
privilegeSlug: privilege.slug, privilegeSlug: privilege.slug,
identityId, identityId,
@@ -474,15 +471,13 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
if (createIdentityPrivilege.isLoading) return; if (createIdentityPrivilege.isLoading) return;
try { try {
await createIdentityPrivilege.mutateAsync({ await createIdentityPrivilege.mutateAsync({
permissions: [ privilegePermission: {
{ actions: [ProjectPermissionActions.Read],
action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets,
subject: ProjectPermissionSub.Secrets, conditions: {
conditions: { environment: currentWorkspace?.environments?.[0].slug as string
environment: currentWorkspace?.environments?.[0].slug
}
} }
], },
identityId, identityId,
projectSlug projectSlug
}); });
@@ -65,7 +65,8 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const isMemberEditDisabled = permission.cannot( const isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
slug: customRoleSlug || role, slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary temporaryAccess: dto.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryRange: dto.temporaryRange, temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime, temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime temporaryAccessStartTime: dto.temporaryAccessStartTime
} }
: { : {
isTemporary: dto.isTemporary isTemporary: dto.isTemporary
} }
})) }))
} }
}); });
@@ -191,9 +192,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired ? isExpired
? "Timed Access Expired" ? "Timed Access Expired"
: `Until ${format( : `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss" "yyyy-MM-dd HH:mm:ss"
)}` )}`
: "Non expiry access" : "Non expiry access"
} }
> >
@@ -212,9 +213,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired ? isExpired
? "Access Expired" ? "Access Expired"
: formatDistance( : formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date() new Date()
) )
: "Permanent"} : "Permanent"}
</Button> </Button>
</Tooltip> </Tooltip>
@@ -335,7 +336,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
type="submit" type="submit"
className={twMerge( className={twMerge(
"transition-all", "transition-all",
"opacity-0 cursor-default", "cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100" roleForm.formState.isDirty && "cursor-pointer opacity-100"
)} )}
isDisabled={!roleForm.formState.isDirty} isDisabled={!roleForm.formState.isDirty}
@@ -3,7 +3,6 @@ import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectRoleList } from "./components/ProjectRoleList"; import { ProjectRoleList } from "./components/ProjectRoleList";
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection"; import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
@@ -21,7 +20,7 @@ export const ProjectRoleListTab = withProjectPermission(
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<ProjectRoleModifySection <ProjectRoleModifySection
role={popUp.editRole.data as TProjectRole} roleSlug={popUp.editRole.data as string}
onGoBack={() => handlePopUpClose("editRole")} onGoBack={() => handlePopUpClose("editRole")}
/> />
</motion.div> </motion.div>
@@ -33,7 +32,7 @@ export const ProjectRoleListTab = withProjectPermission(
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }} exit={{ opacity: 0, translateX: -30 }}
> >
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} /> <ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
</motion.div> </motion.div>
); );
}, },
@@ -24,7 +24,7 @@ import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types"; import { TProjectRole } from "@app/hooks/api/roles/types";
type Props = { type Props = {
onSelectRole: (role?: TProjectRole) => void; onSelectRole: (slug?: string) => void;
}; };
export const ProjectRoleList = ({ onSelectRole }: Props) => { export const ProjectRoleList = ({ onSelectRole }: Props) => {
@@ -32,10 +32,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
console.log(roles);
const { mutateAsync: deleteRole } = useDeleteProjectRole(); const { mutateAsync: deleteRole } = useDeleteProjectRole();
@@ -43,7 +42,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { id } = popUp?.deleteRole?.data as TProjectRole; const { id } = popUp?.deleteRole?.data as TProjectRole;
try { try {
await deleteRole({ await deleteRole({
projectId: workspaceId, projectSlug,
id id
}); });
createNotification({ type: "success", text: "Successfully removed the role" }); createNotification({ type: "success", text: "Successfully removed the role" });
@@ -109,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
<IconButton <IconButton
isDisabled={!isAllowed} isDisabled={!isAllowed}
ariaLabel="edit" ariaLabel="edit"
onClick={() => onSelectRole(role)} onClick={() => onSelectRole(role.slug)}
variant="plain" variant="plain"
> >
<FontAwesomeIcon icon={faEdit} /> <FontAwesomeIcon icon={faEdit} />
@@ -146,9 +145,8 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
</div> </div>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteRole.isOpen} isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${ title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
(popUp?.deleteRole?.data as TProjectRole)?.name || " " } role?`}
} role?`}
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""} deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
onClose={() => handlePopUpClose("deleteRole")} onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete} onDeleteApproved={handleRoleDelete}
@@ -19,9 +19,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { useCreateProjectRole, useUpdateProjectRole } from "@app/hooks/api"; import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types"; import { TProjectRole } from "@app/hooks/api/roles/types";
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission"; import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
@@ -117,17 +121,20 @@ const SINGLE_PERMISSION_LIST = [
] as const; ] as const;
type Props = { type Props = {
role?: TProjectRole; roleSlug?: string;
onGoBack: VoidFunction; onGoBack: VoidFunction;
}; };
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => { export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(role?.slug || ""); const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
const isNewRole = !role?.slug; const isNewRole = !roleSlug;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
currentWorkspace?.slug || "",
roleSlug as string
);
const { const {
handleSubmit, handleSubmit,
@@ -137,19 +144,21 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
getValues, getValues,
control control
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, values: roleDetails
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
: ({} as TProjectRole),
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const { mutateAsync: createRole } = useCreateProjectRole(); const { mutateAsync: createRole } = useCreateProjectRole();
const { mutateAsync: updateRole } = useUpdateProjectRole(); const { mutateAsync: updateRole } = useUpdateProjectRole();
const handleRoleUpdate = async (el: TFormSchema) => { const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?.id) return; if (!roleDetails?.id) return;
try { try {
await updateRole({ await updateRole({
id: role?.id, id: roleDetails?.id as string,
projectId: workspaceId, projectSlug,
...el, ...el,
permissions: formRolePermission2API(el.permissions) permissions: formRolePermission2API(el.permissions)
}); });
@@ -169,7 +178,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
try { try {
await createRole({ await createRole({
projectId: workspaceId, projectSlug,
...el, ...el,
permissions: formRolePermission2API(el.permissions) permissions: formRolePermission2API(el.permissions)
}); });
@@ -181,6 +190,14 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
} }
}; };
if (!isNewRole && isRoleDetailsLoading) {
return (
<div className="flex w-full items-center justify-center p-8">
<Spinner />
</div>
);
}
return ( return (
<div> <div>
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -95,10 +95,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Record<string, any> = {}; const formVal: Record<string, any> = {};
permissions.forEach((permission) => { permissions.forEach((permission) => {
const { const { subject: caslSub, action } = permission;
subject: [subject], const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
action
} = permission;
if (!formVal?.[subject]) formVal[subject] = {}; if (!formVal?.[subject]) formVal[subject] = {};
if (subject === "secrets") { if (subject === "secrets") {
@@ -123,7 +121,7 @@ const multiEnvForm2Api = (
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]); const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition // if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => { PERMISSION_ACTIONS.forEach((action) => {
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] }); if (formVal?.all?.[action]) permissions.push({ action, subject });
}); });
if (!isFullAccess) { if (!isFullAccess) {
@@ -144,7 +142,7 @@ const multiEnvForm2Api = (
if (formVal[slug]?.secretPath) if (formVal[slug]?.secretPath)
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath }; conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
permissions.push({ action, subject: [subject], conditions }); permissions.push({ action, subject, conditions });
} }
}); });
}); });
@@ -161,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
} else { } else {
Object.entries(actions).forEach(([action, isAllowed]) => { Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) { if (isAllowed) {
permissions.push({ subject: [rule], action }); permissions.push({ subject: rule, action });
} }
}); });
} }