Compare commits

..

35 Commits

Author SHA1 Message Date
github-actions
06dca77be2 chore: renamed new migration files to latest timestamp (gh-action) 2024-05-29 06:07:52 +00:00
Maidul Islam
b79ed28bb8 Merge pull request #1895 from Infisical/maidul-12djiqd 2024-05-29 02:07:26 -04:00
Maidul Islam
6055661515 add secret version secrets index 2024-05-29 00:54:29 -04:00
Daniel Hougaard
f3eda1fd13 Merge pull request #1893 from Infisical/daniel/fix-query-invalidation-bug
Fix: Select organization query invalidation
2024-05-29 03:08:41 +02:00
Daniel Hougaard
60178a6ba6 Update queries.tsx 2024-05-29 02:59:53 +02:00
Daniel Hougaard
3e6d43e4df Update queries.tsx 2024-05-29 02:59:29 +02:00
Maidul Islam
be68ecc25d update api fields 2024-05-28 17:31:09 -04:00
Maidul Islam
b2ad7cc7c0 small rephrase 2024-05-28 17:20:22 -04:00
Maidul Islam
6c6c436cc6 Merge pull request #1874 from akhilmhdh/feat/tf-role-sp-changes
Updates api endpoints for project role and identity specfic privilege
2024-05-28 16:59:44 -04:00
Sheen Capadngan
01ea41611b Merge pull request #1890 from Infisical/misc/minor-mfa-lock-prompt-adjustments
misc: minor ui adjustments
2024-05-29 02:06:05 +08:00
Sheen Capadngan
dc7bf9674a misc: minor ui adjustments 2024-05-29 02:00:49 +08:00
Sheen Capadngan
b6814b67b0 Merge pull request #1885 from Infisical/fix/gitlab-integration-creation-with-groups
fix: resolved gitlab integration creation issue with groups selection
2024-05-29 01:08:45 +08:00
Sheen Capadngan
5234a89612 Merge pull request #1888 from Infisical/create-pull-request/patch-1716910746
GH Action: rename new migration file timestamp
2024-05-29 01:01:35 +08:00
Sheen Capadngan
45bb2f0fcc Merge pull request #1889 from Infisical/fix/added-lock-prompt-to-sso-signin
fix: added account locked prompt to sso signin flow
2024-05-29 01:00:48 +08:00
Sheen Capadngan
4c7e218d0d misc: removed unnecessary set state 2024-05-29 00:58:29 +08:00
Sheen Capadngan
0371a57548 fix: added account locked prompt to sso signin flow 2024-05-29 00:43:14 +08:00
github-actions
7d0eb9a0fd chore: renamed new migration files to latest timestamp (gh-action) 2024-05-28 15:39:05 +00:00
Sheen Capadngan
44b14756b1 Merge pull request #1861 from Infisical/feat/secure-mfa-endpoints-with-improved-rate-limiting
feat: secure mfa endpoints with improved rate limiting and account locking
2024-05-28 23:38:35 +08:00
Sheen Capadngan
51f4047207 Merge pull request #1883 from Infisical/misc/improve-rbac-integration-creation-and-update-1
misc: improved integration rbac control
2024-05-28 19:49:27 +08:00
Maidul Islam
4567e505ec pause project delete 2024-05-28 05:40:32 -04:00
Sheen Capadngan
0fc4fb8858 misc: added backend validation for secrets read during integration create/update 2024-05-28 12:13:35 +08:00
Sheen Capadngan
fe4cc950d3 misc: updated temporary lock error message 2024-05-28 01:41:16 +08:00
Sheen Capadngan
a0f678a295 misc: moved to using router push instead of reload 2024-05-27 22:59:24 +08:00
Sheen Capadngan
3639a7fc18 misc: migrated to native DAL method 2024-05-27 11:01:22 +08:00
Sheen Capadngan
59c8dc3cda Merge branch 'main' into feat/secure-mfa-endpoints-with-improved-rate-limiting 2024-05-27 10:55:06 +08:00
=
7a955e3fae docs: api docs for identity specific privilege 2024-05-26 22:49:36 +05:30
=
ee5130f56c feat: privilege api better permission inputs and required changes in ui for role and privilege 2024-05-26 22:49:35 +05:30
=
719f3beab0 feat: api changes for update role api for identity based use 2024-05-26 22:49:35 +05:30
Sheen Capadngan
b9a6f94eea misc: moved user lock reset after backup success 2024-05-24 23:56:24 +08:00
Sheen Capadngan
c0daa11aeb misc: addressed PR comments 2024-05-24 23:45:16 +08:00
Sheen Capadngan
9b2b6d61be Merge branch 'main' into feat/secure-mfa-endpoints-with-improved-rate-limiting 2024-05-24 22:18:05 +08:00
Sheen Capadngan
a0d9331e67 misc: removed comment 2024-05-23 00:21:19 +08:00
Sheen Capadngan
8ec8b1ce2f feat: add custom rate limiting for mfa 2024-05-22 23:51:58 +08:00
Sheen Capadngan
e3dae9d498 feat: integration user lock flow to frontend 2024-05-22 22:51:20 +08:00
Sheen Capadngan
41d72d5dc6 feat: added user-locking on mfa failure 2024-05-22 21:55:56 +08:00
49 changed files with 1101 additions and 510 deletions

View File

@@ -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");
}
});
}

View File

@@ -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");
});
}
}

View File

@@ -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>;

View File

@@ -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 };

View File

@@ -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, {

View File

@@ -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 };
}
});

View File

@@ -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."
}
};

View File

@@ -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,

View File

@@ -523,7 +523,8 @@ export const registerRoutes = async (
permissionService,
projectRoleDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL
identityProjectMembershipRoleDAL,
projectDAL
});
const snapshotService = secretSnapshotServiceFactory({

View File

@@ -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,

View File

@@ -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`);
}
});
};

View File

@@ -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({

View File

@@ -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();

View File

@@ -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 = {

View File

@@ -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.`
});
}
}
};

View File

@@ -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,

View File

@@ -174,6 +174,12 @@ export const authPaswordServiceFactory = ({
salt,
verifier
});
await userDAL.updateById(userId, {
isLocked: false,
temporaryLockDateEnd: null,
consecutiveFailedMfaAttempts: 0
});
};
/*

View File

@@ -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" });

View File

@@ -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 };
};

View File

@@ -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">;

View File

@@ -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",

View 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>

View File

@@ -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
};
};

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/workspace/{projectSlug}/roles"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/workspace/{projectSlug}/roles/{roleId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get By Slug"
openapi: "GET /api/v1/workspace/{projectSlug}/roles/slug/{slug}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/workspace/{projectSlug}/roles"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/workspace/{projectSlug}/roles/{roleId}"
---

View File

@@ -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": [

View File

@@ -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
]);
}
});
};

View File

@@ -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 = {

View File

@@ -8,6 +8,7 @@ export {
} from "./mutation";
export {
useGetOrgRoles,
useGetProjectRoleBySlug,
useGetProjectRoles,
useGetUserOrgPermissions,
useGetUserProjectPermissions

View File

@@ -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));
}
});
};

View File

@@ -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) => {

View File

@@ -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;
};

View File

@@ -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);
}
};

View File

@@ -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.",

View File

@@ -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">

View File

@@ -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);
}
};

View File

@@ -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>
);
}
};

View File

@@ -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();

View File

@@ -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();

View File

@@ -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}

View File

@@ -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
});

View File

@@ -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}

View File

@@ -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>
);
},

View File

@@ -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")}

View File

@@ -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)}>

View File

@@ -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 });
}
});
}