mirror of
https://github.com/Infisical/infisical.git
synced 2025-09-04 07:35:30 +00:00
Compare commits
35 Commits
fix/gitlab
...
create-pul
Author | SHA1 | Date | |
---|---|---|---|
|
06dca77be2 | ||
|
b79ed28bb8 | ||
|
6055661515 | ||
|
f3eda1fd13 | ||
|
60178a6ba6 | ||
|
3e6d43e4df | ||
|
be68ecc25d | ||
|
b2ad7cc7c0 | ||
|
6c6c436cc6 | ||
|
01ea41611b | ||
|
dc7bf9674a | ||
|
b6814b67b0 | ||
|
5234a89612 | ||
|
45bb2f0fcc | ||
|
4c7e218d0d | ||
|
0371a57548 | ||
|
7d0eb9a0fd | ||
|
44b14756b1 | ||
|
51f4047207 | ||
|
4567e505ec | ||
|
0fc4fb8858 | ||
|
fe4cc950d3 | ||
|
a0f678a295 | ||
|
3639a7fc18 | ||
|
59c8dc3cda | ||
|
7a955e3fae | ||
|
ee5130f56c | ||
|
719f3beab0 | ||
|
b9a6f94eea | ||
|
c0daa11aeb | ||
|
9b2b6d61be | ||
|
a0d9331e67 | ||
|
8ec8b1ce2f | ||
|
e3dae9d498 | ||
|
41d72d5dc6 |
@@ -0,0 +1,43 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
if (!hasConsecutiveFailedMfaAttempts) {
|
||||
t.integer("consecutiveFailedMfaAttempts").defaultTo(0);
|
||||
}
|
||||
|
||||
if (!hasIsLocked) {
|
||||
t.boolean("isLocked").defaultTo(false);
|
||||
}
|
||||
|
||||
if (!hasTemporaryLockDateEnd) {
|
||||
t.dateTime("temporaryLockDateEnd").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
if (hasConsecutiveFailedMfaAttempts) {
|
||||
t.dropColumn("consecutiveFailedMfaAttempts");
|
||||
}
|
||||
|
||||
if (hasIsLocked) {
|
||||
t.dropColumn("isLocked");
|
||||
}
|
||||
|
||||
if (hasTemporaryLockDateEnd) {
|
||||
t.dropColumn("temporaryLockDateEnd");
|
||||
}
|
||||
});
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesSecretVersionIdExist) t.index("secretVersionId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
|
||||
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||
if (doesSecretVersionIdExist) t.dropIndex("secretVersionId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -22,7 +22,10 @@ export const UsersSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
isGhost: z.boolean().default(false),
|
||||
username: z.string(),
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||
consecutiveFailedMfaAttempts: z.number().optional(),
|
||||
isLocked: z.boolean().optional(),
|
||||
temporaryLockDateEnd: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
@@ -5,10 +5,15 @@ import { z } from "zod";
|
||||
|
||||
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 { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
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";
|
||||
|
||||
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.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: {
|
||||
200: z.object({
|
||||
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
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({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: false,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: JSON.stringify(packRules(permission))
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.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
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
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({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
...req.body,
|
||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||
isTemporary: true,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: JSON.stringify(packRules(permission))
|
||||
});
|
||||
return { privilege };
|
||||
}
|
||||
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
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),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
|
||||
temporaryRange: z
|
||||
.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),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
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({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
projectSlug: req.body.projectSlug,
|
||||
data: {
|
||||
...updatedInfo,
|
||||
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
|
||||
permissions: permission ? JSON.stringify(packRules(permission)) : undefined
|
||||
}
|
||||
});
|
||||
return { privilege };
|
||||
|
@@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1)
|
||||
.trim()
|
||||
.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"
|
||||
)
|
||||
.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 { 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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectId/roles",
|
||||
url: "/:projectSlug/roles",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.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: {
|
||||
200: z.object({
|
||||
role: ProjectRolesSchema
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.createRole(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
projectSlug: req.params.projectSlug,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
roleId: z.string().trim()
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().trim().optional(),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.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: {
|
||||
200: z.object({
|
||||
role: ProjectRolesSchema
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.updateRole(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.params.roleId,
|
||||
req.body,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
projectSlug: req.params.projectSlug,
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
roleId: z.string().trim()
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: ProjectRolesSchema
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.deleteRole(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.params.roleId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
const role = await server.services.projectRole.deleteRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
projectSlug: req.params.projectSlug,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
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: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
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: {
|
||||
200: z.object({
|
||||
data: z.object({
|
||||
roles: ProjectRolesSchema.omit({ permissions: true })
|
||||
.merge(z.object({ permissions: z.unknown() }))
|
||||
.array()
|
||||
})
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.projectRole.listRoles(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { roles } };
|
||||
const role = await server.services.projectRole.getRoleBySlug({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
projectSlug: req.params.projectSlug,
|
||||
roleSlug: req.params.slug
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -519,7 +519,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity 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
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
@@ -533,6 +534,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
- { "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.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
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.",
|
||||
slug: "The 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
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
@@ -558,6 +561,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||
\`\`\`
|
||||
`,
|
||||
privilegePermission: "The permission object for the privilege.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
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."
|
||||
}
|
||||
};
|
||||
|
||||
export const PROJECT_ROLE = {
|
||||
CREATE: {
|
||||
projectSlug: "Slug of the project to create the role for.",
|
||||
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: "Slug of the project to update the role for.",
|
||||
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: "Slug of the project to delete this role for.",
|
||||
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 the roles of."
|
||||
}
|
||||
};
|
||||
|
@@ -52,6 +52,14 @@ export const inviteUserRateLimit: RateLimitOptions = {
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const mfaRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: 20,
|
||||
keyGenerator: (req) => {
|
||||
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
||||
}
|
||||
};
|
||||
|
||||
export const creationLimit: RateLimitOptions = {
|
||||
// identity, project, org
|
||||
timeWindow: 60 * 1000,
|
||||
|
@@ -523,7 +523,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
projectRoleDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL
|
||||
identityProjectMembershipRoleDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const snapshotService = secretSnapshotServiceFactory({
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
DynamicSecretsSchema,
|
||||
IdentityProjectAdditionalPrivilegeSchema,
|
||||
IntegrationAuthsSchema,
|
||||
ProjectRolesSchema,
|
||||
SecretApprovalPoliciesSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
|
||||
.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({
|
||||
permissions: UnpackedPermissionSchema.array()
|
||||
});
|
||||
|
||||
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
|
||||
permissions: UnpackedPermissionSchema.array()
|
||||
});
|
||||
|
||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
|
@@ -1,11 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { authRateLimit, readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
@@ -25,4 +29,29 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
return { user };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:userId/unlock",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
token: z.string().trim()
|
||||
}),
|
||||
params: z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
await server.services.user.unlockUser(req.params.userId, req.query.token);
|
||||
} catch (err) {
|
||||
logger.error(`User unlock failed for ${req.params.userId}`);
|
||||
logger.error(err);
|
||||
}
|
||||
return res.redirect(`${appCfg.SITE_URL}/login`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -34,7 +34,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/mfa/send",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: mfaRateLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
@@ -53,7 +53,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/mfa/verify",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: mfaRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@@ -13,8 +13,9 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
|
||||
|
||||
type TAuthTokenServiceFactoryDep = {
|
||||
tokenDAL: TTokenDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||
};
|
||||
|
||||
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||
|
||||
export const getTokenConfig = (tokenType: TokenType) => {
|
||||
@@ -53,6 +54,11 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||
return { token, expiresAt };
|
||||
}
|
||||
case TokenType.TOKEN_USER_UNLOCK: {
|
||||
const token = crypto.randomBytes(16).toString("hex");
|
||||
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||
return { token, expiresAt };
|
||||
}
|
||||
default: {
|
||||
const token = crypto.randomBytes(16).toString("hex");
|
||||
const expiresAt = new Date();
|
||||
|
@@ -3,7 +3,8 @@ export enum TokenType {
|
||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||
TOKEN_EMAIL_MFA = "emailMfa",
|
||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||
TOKEN_USER_UNLOCK = "userUnlock"
|
||||
}
|
||||
|
||||
export type TCreateTokenForUserDTO = {
|
||||
|
@@ -44,3 +44,27 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||
};
|
||||
|
||||
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
||||
if (isLocked) {
|
||||
throw new UnauthorizedError({
|
||||
name: "User Locked",
|
||||
message:
|
||||
"User is locked due to multiple failed login attempts. An email has been sent to you in order to unlock your account. You can also reset your password to unlock your account."
|
||||
});
|
||||
}
|
||||
|
||||
if (temporaryLockDateEnd) {
|
||||
const timeDiff = new Date().getTime() - temporaryLockDateEnd.getTime();
|
||||
if (timeDiff < 0) {
|
||||
const secondsDiff = (-1 * timeDiff) / 1000;
|
||||
const timeDisplay =
|
||||
secondsDiff > 60 ? `${Math.ceil(secondsDiff / 60)} minutes` : `${Math.ceil(secondsDiff)} seconds`;
|
||||
|
||||
throw new UnauthorizedError({
|
||||
name: "User Locked",
|
||||
message: `User is temporary locked due to multiple failed login attempts. Try again after ${timeDisplay}. You can also reset your password now to proceed.`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
|
||||
@@ -13,7 +13,7 @@ import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { validateProviderAuthToken } from "./auth-fns";
|
||||
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
||||
import {
|
||||
TLoginClientProofDTO,
|
||||
TLoginGenServerPublicKeyDTO,
|
||||
@@ -212,6 +212,9 @@ export const authLoginServiceFactory = ({
|
||||
});
|
||||
// send multi factor auth token if they it enabled
|
||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||
const user = await userDAL.findById(userEnc.userId);
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
|
||||
const mfaToken = jwt.sign(
|
||||
{
|
||||
authMethod,
|
||||
@@ -300,28 +303,111 @@ export const authLoginServiceFactory = ({
|
||||
const resendMfaToken = async (userId: string) => {
|
||||
const user = await userDAL.findById(userId);
|
||||
if (!user || !user.email) return;
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
await sendUserMfaCode({
|
||||
userId: user.id,
|
||||
email: user.email
|
||||
});
|
||||
};
|
||||
|
||||
const processFailedMfaAttempt = async (userId: string) => {
|
||||
try {
|
||||
const updatedUser = await userDAL.transaction(async (tx) => {
|
||||
const PROGRESSIVE_DELAY_INTERVAL = 3;
|
||||
const user = await userDAL.updateById(userId, { $incr: { consecutiveFailedMfaAttempts: 1 } }, tx);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const progressiveDelaysInMins = [5, 30, 60];
|
||||
|
||||
// lock user when failed attempt exceeds threshold
|
||||
if (
|
||||
user.consecutiveFailedMfaAttempts &&
|
||||
user.consecutiveFailedMfaAttempts >= PROGRESSIVE_DELAY_INTERVAL * (progressiveDelaysInMins.length + 1)
|
||||
) {
|
||||
return userDAL.updateById(
|
||||
userId,
|
||||
{
|
||||
isLocked: true,
|
||||
temporaryLockDateEnd: null
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
// delay user only when failed MFA attempts is a multiple of configured delay interval
|
||||
if (user.consecutiveFailedMfaAttempts && user.consecutiveFailedMfaAttempts % PROGRESSIVE_DELAY_INTERVAL === 0) {
|
||||
const delayIndex = user.consecutiveFailedMfaAttempts / PROGRESSIVE_DELAY_INTERVAL - 1;
|
||||
return userDAL.updateById(
|
||||
userId,
|
||||
{
|
||||
temporaryLockDateEnd: new Date(new Date().getTime() + progressiveDelaysInMins[delayIndex] * 60 * 1000)
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Process failed MFA Attempt" });
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Multi factor authentication verification of code
|
||||
* Third step of login in which user completes with mfa
|
||||
* */
|
||||
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const user = await userDAL.findById(userId);
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
|
||||
try {
|
||||
await tokenService.validateTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_MFA,
|
||||
userId,
|
||||
code: mfaToken
|
||||
});
|
||||
} catch (err) {
|
||||
const updatedUser = await processFailedMfaAttempt(userId);
|
||||
if (updatedUser.isLocked) {
|
||||
if (updatedUser.email) {
|
||||
const unlockToken = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_USER_UNLOCK,
|
||||
userId: updatedUser.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.UnlockAccount,
|
||||
subjectLine: "Unlock your Infisical account",
|
||||
recipients: [updatedUser.email],
|
||||
substitutions: {
|
||||
token: unlockToken,
|
||||
callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||
|
||||
// reset lock states
|
||||
await userDAL.updateById(userId, {
|
||||
consecutiveFailedMfaAttempts: 0,
|
||||
temporaryLockDateEnd: null
|
||||
});
|
||||
|
||||
const token = await generateUserTokens({
|
||||
user: {
|
||||
...userEnc,
|
||||
|
@@ -174,6 +174,12 @@ export const authPaswordServiceFactory = ({
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
await userDAL.updateById(userId, {
|
||||
isLocked: false,
|
||||
temporaryLockDateEnd: null,
|
||||
consecutiveFailedMfaAttempts: 0
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -66,6 +66,11 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
@@ -123,6 +128,11 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||
|
||||
|
@@ -1,25 +1,30 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
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 {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub,
|
||||
projectViewerPermission
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
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 { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||
|
||||
type TProjectRoleServiceFactoryDep = {
|
||||
projectRoleDAL: TProjectRoleDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
||||
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
||||
@@ -27,20 +32,68 @@ type TProjectRoleServiceFactoryDep = {
|
||||
|
||||
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: "Full administrative access over a 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: "Limited read/write role in a 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: "Only read role in a 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 = ({
|
||||
projectRoleDAL,
|
||||
permissionService,
|
||||
identityProjectMembershipRoleDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
projectUserMembershipRoleDAL,
|
||||
projectDAL
|
||||
}: TProjectRoleServiceFactoryDep) => {
|
||||
const createRole = async (
|
||||
actor: ActorType,
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
data: Omit<TProjectRolesInsert, "projectId">,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const createRole = async ({ projectSlug, data, actor, actorId, actorAuthMethod, actorOrgId }: TCreateRoleDTO) => {
|
||||
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,
|
||||
@@ -53,21 +106,54 @@ export const projectRoleServiceFactory = ({
|
||||
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
||||
const role = await projectRoleDAL.create({
|
||||
...data,
|
||||
projectId,
|
||||
permissions: JSON.stringify(data.permissions)
|
||||
projectId
|
||||
});
|
||||
return role;
|
||||
return { ...role, permissions: unpackPermissions(role.permissions) };
|
||||
};
|
||||
|
||||
const updateRole = async (
|
||||
actor: ActorType,
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
roleId: string,
|
||||
data: Omit<TOrgRolesUpdate, "orgId">,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const getRoleBySlug = async ({
|
||||
actor,
|
||||
actorId,
|
||||
projectSlug,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
roleSlug
|
||||
}: 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(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -81,22 +167,16 @@ export const projectRoleServiceFactory = ({
|
||||
if (existingRole && existingRole.id !== roleId)
|
||||
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||
}
|
||||
const [updatedRole] = await projectRoleDAL.update(
|
||||
{ id: roleId, projectId },
|
||||
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
|
||||
);
|
||||
const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data);
|
||||
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||
return updatedRole;
|
||||
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
|
||||
};
|
||||
|
||||
const deleteRole = async (
|
||||
actor: ActorType,
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
roleId: string,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, projectSlug, roleId }: TDeleteRoleDTO) => {
|
||||
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,
|
||||
@@ -125,16 +205,14 @@ export const projectRoleServiceFactory = ({
|
||||
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
||||
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
|
||||
|
||||
return deletedRole;
|
||||
return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
|
||||
};
|
||||
|
||||
const listRoles = async (
|
||||
actor: ActorType,
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
const listRoles = async ({ projectSlug, actorOrgId, actorAuthMethod, actorId, actor }: TListRolesDTO) => {
|
||||
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,
|
||||
@@ -144,52 +222,7 @@ export const projectRoleServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
const customRoles = await projectRoleDAL.find({ projectId });
|
||||
const roles = [
|
||||
{
|
||||
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
|
||||
}))
|
||||
];
|
||||
const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])];
|
||||
|
||||
return roles;
|
||||
};
|
||||
@@ -209,5 +242,5 @@ export const projectRoleServiceFactory = ({
|
||||
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">;
|
||||
|
@@ -21,6 +21,7 @@ export enum SmtpTemplates {
|
||||
EmailVerification = "emailVerification.handlebars",
|
||||
SecretReminder = "secretReminder.handlebars",
|
||||
EmailMfa = "emailMfa.handlebars",
|
||||
UnlockAccount = "unlockAccount.handlebars",
|
||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||
NewDeviceJoin = "newDevice.handlebars",
|
||||
|
16
backend/src/services/smtp/templates/unlockAccount.handlebars
Normal file
16
backend/src/services/smtp/templates/unlockAccount.handlebars
Normal file
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Your Infisical account has been locked</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Unlock your Infisical account</h2>
|
||||
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
|
||||
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
|
||||
<p>If these attempts were not made by you, reset your password immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -207,6 +207,19 @@ export const userServiceFactory = ({
|
||||
return userAction;
|
||||
};
|
||||
|
||||
const unlockUser = async (userId: string, token: string) => {
|
||||
await tokenService.validateTokenForUser({
|
||||
userId,
|
||||
code: token,
|
||||
type: TokenType.TOKEN_USER_UNLOCK
|
||||
});
|
||||
|
||||
await userDAL.update(
|
||||
{ id: userId },
|
||||
{ consecutiveFailedMfaAttempts: 0, isLocked: false, temporaryLockDateEnd: null }
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
sendEmailVerificationCode,
|
||||
verifyEmailVerificationCode,
|
||||
@@ -216,6 +229,7 @@ export const userServiceFactory = ({
|
||||
deleteMe,
|
||||
getMe,
|
||||
createUserAction,
|
||||
getUserAction
|
||||
getUserAction,
|
||||
unlockUser
|
||||
};
|
||||
};
|
||||
|
4
docs/api-reference/endpoints/project-roles/create.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/workspace/{projectSlug}/roles"
|
||||
---
|
4
docs/api-reference/endpoints/project-roles/delete.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/delete.mdx
Normal file
@@ -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}"
|
||||
---
|
4
docs/api-reference/endpoints/project-roles/list.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/workspace/{projectSlug}/roles"
|
||||
---
|
4
docs/api-reference/endpoints/project-roles/update.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/update.mdx
Normal file
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"pages": [
|
||||
|
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { setAuthToken } from "@app/reactQuery";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import {
|
||||
ChangePasswordDTO,
|
||||
CompleteAccountDTO,
|
||||
@@ -78,7 +79,10 @@ export const useSelectOrganization = () => {
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
||||
queryClient.invalidateQueries([
|
||||
organizationKeys.getUserOrganizations,
|
||||
workspaceKeys.getAllUserWorkspace
|
||||
]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -28,6 +28,15 @@ export type TIdentityProjectPrivilege = {
|
||||
}
|
||||
);
|
||||
|
||||
export type TProjectSpecificPrivilegePermission = {
|
||||
conditions: {
|
||||
environment: string;
|
||||
secretPath?: { $glob: string };
|
||||
};
|
||||
actions: string[];
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export type TCreateIdentityProjectPrivilegeDTO = {
|
||||
identityId: string;
|
||||
projectSlug: string;
|
||||
@@ -36,14 +45,16 @@ export type TCreateIdentityProjectPrivilegeDTO = {
|
||||
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
|
||||
temporaryRange?: string;
|
||||
temporaryAccessStartTime?: string;
|
||||
permissions: TProjectPermission[];
|
||||
privilegePermission: TProjectSpecificPrivilegePermission;
|
||||
};
|
||||
|
||||
export type TUpdateIdentityProjectPrivlegeDTO = {
|
||||
projectSlug: string;
|
||||
identityId: string;
|
||||
privilegeSlug: string;
|
||||
privilegeDetails: Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>;
|
||||
privilegeDetails: Partial<
|
||||
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
|
||||
>;
|
||||
};
|
||||
|
||||
export type TDeleteIdentityProjectPrivilegeDTO = {
|
||||
|
@@ -8,6 +8,7 @@ export {
|
||||
} from "./mutation";
|
||||
export {
|
||||
useGetOrgRoles,
|
||||
useGetProjectRoleBySlug,
|
||||
useGetProjectRoles,
|
||||
useGetUserOrgPermissions,
|
||||
useGetUserProjectPermissions
|
||||
|
@@ -17,13 +17,10 @@ export const useCreateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) =>
|
||||
apiRequest.post(`/api/v1/workspace/${projectId}/roles`, {
|
||||
...dto,
|
||||
permissions: permissions.length ? packRules(permissions) : []
|
||||
}),
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
|
||||
apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -32,13 +29,10 @@ export const useUpdateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) =>
|
||||
apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, {
|
||||
...dto,
|
||||
permissions: permissions?.length ? packRules(permissions) : []
|
||||
}),
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
|
||||
apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -47,12 +41,10 @@ export const useDeleteProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) =>
|
||||
apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, {
|
||||
data: { projectId }
|
||||
}),
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
|
||||
apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -14,7 +14,6 @@ import {
|
||||
TGetUserProjectPermissionDTO,
|
||||
TOrgRole,
|
||||
TPermission,
|
||||
TProjectPermission,
|
||||
TProjectRole
|
||||
} from "./types";
|
||||
|
||||
@@ -37,7 +36,9 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||
|
||||
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,
|
||||
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
||||
["user-permissions", { orgId }] as const,
|
||||
@@ -46,20 +47,29 @@ export const roleQueryKeys = {
|
||||
};
|
||||
|
||||
const getProjectRoles = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<{
|
||||
data: { roles: Array<Omit<TProjectRole, "permissions"> & { permissions: unknown }> };
|
||||
}>(`/api/v1/workspace/${projectId}/roles`);
|
||||
return data.data.roles.map(({ permissions, ...el }) => ({
|
||||
...el,
|
||||
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
|
||||
}));
|
||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||
`/api/v1/workspace/${projectId}/roles`
|
||||
);
|
||||
return data.roles;
|
||||
};
|
||||
|
||||
export const useGetProjectRoles = (projectId: string) =>
|
||||
export const useGetProjectRoles = (projectSlug: string) =>
|
||||
useQuery({
|
||||
queryKey: roleQueryKeys.getProjectRoles(projectId),
|
||||
queryFn: () => getProjectRoles(projectId),
|
||||
enabled: Boolean(projectId)
|
||||
queryKey: roleQueryKeys.getProjectRoles(projectSlug),
|
||||
queryFn: () => getProjectRoles(projectSlug),
|
||||
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) => {
|
||||
|
@@ -71,7 +71,7 @@ export type TDeleteOrgRoleDTO = {
|
||||
};
|
||||
|
||||
export type TCreateProjectRoleDTO = {
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
@@ -79,11 +79,11 @@ export type TCreateProjectRoleDTO = {
|
||||
};
|
||||
|
||||
export type TUpdateProjectRoleDTO = {
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
id: string;
|
||||
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
||||
|
||||
export type TDeleteProjectRoleDTO = {
|
||||
projectId: string;
|
||||
projectSlug: string;
|
||||
id: string;
|
||||
};
|
||||
|
@@ -169,12 +169,12 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
mappingBehavior: selectedMappingBehavior
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
setTargetSecretNameErrorText("");
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
@@ -105,8 +105,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
if (err.response.data.error === "User Locked") {
|
||||
createNotification({
|
||||
title: err.response.data.error,
|
||||
text: err.response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginError(true);
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||
|
@@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
import Error from "@app/components/basic/Error"; // which to notification
|
||||
import Error from "@app/components/basic/Error";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
|
||||
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||
@@ -46,20 +46,7 @@ type Props = {
|
||||
callbackPort?: string | null;
|
||||
};
|
||||
|
||||
interface VerifyMfaTokenError {
|
||||
response: {
|
||||
data: {
|
||||
context: {
|
||||
code: string;
|
||||
triesLeft: number;
|
||||
};
|
||||
};
|
||||
status: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||
@@ -178,20 +165,31 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as VerifyMfaTokenError;
|
||||
} catch (err: any) {
|
||||
if (err.response.data.error === "User Locked") {
|
||||
createNotification({
|
||||
title: err.response.data.error,
|
||||
text: err.response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: "Failed to log in",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
if (error?.response?.status === 500) {
|
||||
window.location.reload();
|
||||
} else if (error?.response?.data?.context?.triesLeft) {
|
||||
setTriesLeft(error?.response?.data?.context?.triesLeft);
|
||||
if (error.response.data.context.triesLeft === 0) {
|
||||
window.location.reload();
|
||||
if (triesLeft) {
|
||||
setTriesLeft((left) => {
|
||||
if (triesLeft === 1) {
|
||||
router.push("/");
|
||||
}
|
||||
return (left as number) - 1;
|
||||
});
|
||||
} else {
|
||||
setTriesLeft(2);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -236,7 +234,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
{typeof triesLeft === "number" && (
|
||||
<Error text={`${t("mfa.step2-code-error")} ${triesLeft}`} />
|
||||
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||
)}
|
||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||
<div className="text-l w-full py-1 text-lg">
|
||||
|
@@ -31,7 +31,6 @@ export const PasswordStep = ({
|
||||
setPassword,
|
||||
setStep
|
||||
}: Props) => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -146,13 +145,23 @@ export const PasswordStep = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
setIsLoading(false);
|
||||
console.error(err);
|
||||
|
||||
if (err.response.data.error === "User Locked") {
|
||||
createNotification({
|
||||
title: err.response.data.error,
|
||||
text: err.response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||
type: "error"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -5,19 +5,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem} from "@app/components/v2";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddGroupToWorkspace,
|
||||
useGetOrganizationGroups,
|
||||
useGetProjectRoles,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspaceGroups
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -33,20 +27,17 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const GroupModal = ({
|
||||
popUp,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: groups } = useGetOrganizationGroups(orgId);
|
||||
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
|
||||
|
||||
const { data: roles } = useGetProjectRoles(workspaceId);
|
||||
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||
|
||||
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
|
||||
|
||||
@@ -84,14 +75,13 @@ export const GroupModal = ({
|
||||
text: "Successfully added group to project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to add group to project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -179,4 +169,4 @@ export const GroupModal = ({
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -201,11 +201,7 @@ export type TMemberRolesProp = {
|
||||
|
||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||
|
||||
export const GroupRoles = ({
|
||||
roles = [],
|
||||
disableEdit = false,
|
||||
groupSlug
|
||||
}: TMemberRolesProp) => {
|
||||
export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
|
||||
const [searchRoles, setSearchRoles] = useState("");
|
||||
@@ -220,9 +216,9 @@ export const GroupRoles = ({
|
||||
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 updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
|
||||
|
@@ -30,17 +30,17 @@ type Props = {
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
||||
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
||||
|
||||
const { data: roles } = useGetProjectRoles(workspaceId);
|
||||
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||
|
||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||
|
||||
|
@@ -65,7 +65,8 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
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 isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -338,7 +339,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
"opacity-0 cursor-default",
|
||||
"cursor-default opacity-0",
|
||||
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||
)}
|
||||
isDisabled={!roleForm.formState.isDirty}
|
||||
|
@@ -131,20 +131,17 @@ const SpecificPrivilegeSecretForm = ({
|
||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
||||
{ 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({
|
||||
privilegeDetails: {
|
||||
...data.temporaryAccess,
|
||||
permissions: actions
|
||||
.filter(({ allowed }) => allowed)
|
||||
.map(({ action }) => ({
|
||||
action,
|
||||
privilegePermission: {
|
||||
actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
conditions
|
||||
}))
|
||||
conditions: {
|
||||
environment: data.environmentSlug,
|
||||
...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
|
||||
}
|
||||
}
|
||||
},
|
||||
privilegeSlug: privilege.slug,
|
||||
identityId,
|
||||
@@ -474,15 +471,13 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
|
||||
if (createIdentityPrivilege.isLoading) return;
|
||||
try {
|
||||
await createIdentityPrivilege.mutateAsync({
|
||||
permissions: [
|
||||
{
|
||||
action: ProjectPermissionActions.Read,
|
||||
privilegePermission: {
|
||||
actions: [ProjectPermissionActions.Read],
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
conditions: {
|
||||
environment: currentWorkspace?.environments?.[0].slug
|
||||
environment: currentWorkspace?.environments?.[0].slug as string
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
identityId,
|
||||
projectSlug
|
||||
});
|
||||
|
@@ -65,7 +65,8 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
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 isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -335,7 +336,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
"opacity-0 cursor-default",
|
||||
"cursor-default opacity-0",
|
||||
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||
)}
|
||||
isDisabled={!roleForm.formState.isDirty}
|
||||
|
@@ -3,7 +3,6 @@ import { motion } from "framer-motion";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { ProjectRoleList } from "./components/ProjectRoleList";
|
||||
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
|
||||
@@ -21,7 +20,7 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleModifySection
|
||||
role={popUp.editRole.data as TProjectRole}
|
||||
roleSlug={popUp.editRole.data as string}
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
/>
|
||||
</motion.div>
|
||||
@@ -33,7 +32,7 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
|
||||
<ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
|
@@ -24,7 +24,7 @@ import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
type Props = {
|
||||
onSelectRole: (role?: TProjectRole) => void;
|
||||
onSelectRole: (slug?: string) => void;
|
||||
};
|
||||
|
||||
export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
@@ -32,10 +32,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
console.log(roles);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||
|
||||
const { mutateAsync: deleteRole } = useDeleteProjectRole();
|
||||
|
||||
@@ -43,7 +42,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||
try {
|
||||
await deleteRole({
|
||||
projectId: workspaceId,
|
||||
projectSlug,
|
||||
id
|
||||
});
|
||||
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||
@@ -109,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit"
|
||||
onClick={() => onSelectRole(role)}
|
||||
onClick={() => onSelectRole(role.slug)}
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
@@ -146,8 +145,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRole.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteRole?.data as TProjectRole)?.name || " "
|
||||
title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
|
||||
} role?`}
|
||||
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
|
||||
onClose={() => handlePopUpClose("deleteRole")}
|
||||
|
@@ -19,9 +19,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
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 { useCreateProjectRole, useUpdateProjectRole } from "@app/hooks/api";
|
||||
import {
|
||||
useCreateProjectRole,
|
||||
useGetProjectRoleBySlug,
|
||||
useUpdateProjectRole
|
||||
} from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
|
||||
@@ -117,17 +121,20 @@ const SINGLE_PERMISSION_LIST = [
|
||||
] as const;
|
||||
|
||||
type Props = {
|
||||
role?: TProjectRole;
|
||||
roleSlug?: string;
|
||||
onGoBack: VoidFunction;
|
||||
};
|
||||
|
||||
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(role?.slug || "");
|
||||
const isNewRole = !role?.slug;
|
||||
|
||||
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
|
||||
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
|
||||
const isNewRole = !roleSlug;
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
|
||||
currentWorkspace?.slug || "",
|
||||
roleSlug as string
|
||||
);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -137,19 +144,21 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
getValues,
|
||||
control
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
||||
values: roleDetails
|
||||
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
|
||||
: ({} as TProjectRole),
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync: createRole } = useCreateProjectRole();
|
||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||
|
||||
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||
if (!role?.id) return;
|
||||
if (!roleDetails?.id) return;
|
||||
|
||||
try {
|
||||
await updateRole({
|
||||
id: role?.id,
|
||||
projectId: workspaceId,
|
||||
id: roleDetails?.id as string,
|
||||
projectSlug,
|
||||
...el,
|
||||
permissions: formRolePermission2API(el.permissions)
|
||||
});
|
||||
@@ -169,7 +178,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
||||
|
||||
try {
|
||||
await createRole({
|
||||
projectId: workspaceId,
|
||||
projectSlug,
|
||||
...el,
|
||||
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 (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
|
@@ -95,10 +95,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const formVal: Record<string, any> = {};
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
const {
|
||||
subject: [subject],
|
||||
action
|
||||
} = permission;
|
||||
const { subject: caslSub, action } = permission;
|
||||
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
|
||||
if (!formVal?.[subject]) formVal[subject] = {};
|
||||
|
||||
if (subject === "secrets") {
|
||||
@@ -123,7 +121,7 @@ const multiEnvForm2Api = (
|
||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||
// if any of them is set in all push it without any condition
|
||||
PERMISSION_ACTIONS.forEach((action) => {
|
||||
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
|
||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
||||
});
|
||||
|
||||
if (!isFullAccess) {
|
||||
@@ -144,7 +142,7 @@ const multiEnvForm2Api = (
|
||||
if (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 {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: [rule], action });
|
||||
permissions.push({ subject: rule, action });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user