mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 14:28:20 +00:00
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:
@@ -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 };
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
---
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {
|
|||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
useGetOrgRoles,
|
useGetOrgRoles,
|
||||||
|
useGetProjectRoleBySlug,
|
||||||
useGetProjectRoles,
|
useGetProjectRoles,
|
||||||
useGetUserOrgPermissions,
|
useGetUserOrgPermissions,
|
||||||
useGetUserProjectPermissions
|
useGetUserProjectPermissions
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+137
-147
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+12
-16
@@ -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 }) => (
|
||||||
|
|||||||
+2
-2
@@ -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();
|
||||||
|
|
||||||
|
|||||||
+16
-15
@@ -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}
|
||||||
|
|||||||
+14
-19
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-15
@@ -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}
|
||||||
|
|||||||
+2
-3
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-9
@@ -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}
|
||||||
|
|||||||
+30
-13
@@ -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)}>
|
||||||
|
|||||||
+5
-7
@@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user