mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Feat: Specify organization role and projects when inviting users to org
This commit is contained in:
@@ -688,6 +688,7 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
orgService,
|
||||
projectMembershipDAL,
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
certificateAuthorityDAL,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -19,7 +19,8 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
inviteeEmails: z.array(z.string().trim().email()),
|
||||
organizationId: z.string().trim(),
|
||||
projectIds: z.array(z.string().trim()).optional(),
|
||||
organizationRoleSlug: z.string().trim().optional()
|
||||
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -44,6 +45,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
userId: req.permission.id,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
projectIds: req.body.projectIds,
|
||||
projectRoleSlug: req.body.projectRoleSlug,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import {
|
||||
IntegrationsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
includeRoles: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaces: projectWithEnv.array()
|
||||
workspaces: projectWithEnv
|
||||
.extend({
|
||||
roles: ProjectRolesSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const workspaces = await server.services.project.getProjects(req.permission.id);
|
||||
const workspaces = await server.services.project.getProjects({
|
||||
includeRoles: req.query.includeRoles,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { workspaces };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
|
||||
export enum TokenType {
|
||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||
@@ -54,9 +56,14 @@ export enum TokenMetadataType {
|
||||
InviteToProjects = "projects-invite"
|
||||
}
|
||||
|
||||
// TODO: Split the payloads into separate types for better type inference (not needed until we add more types of metadata)
|
||||
export type TTokenInviteToProjectsMetadataPayload = {
|
||||
projectIds: string[];
|
||||
projectRoleSlug: ProjectMembershipRole;
|
||||
userId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTokenMetadata = {
|
||||
type: TokenMetadataType.InviteToProjects;
|
||||
projectIds: string[];
|
||||
userId: string;
|
||||
payload: TTokenInviteToProjectsMetadataPayload;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@@ -17,7 +17,7 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
@@ -373,13 +373,17 @@ export const authSignupServiceFactory = ({
|
||||
if (metadata) {
|
||||
const metadataObj = jwt.verify(metadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
||||
|
||||
if (user.id !== metadataObj.userId) {
|
||||
if (
|
||||
metadataObj.payload.userId !== user.id ||
|
||||
metadataObj.payload.orgId !== orgMembership.orgId ||
|
||||
metadataObj.type !== TokenMetadataType.InviteToProjects
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Malformed or invalid metadata token"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const projectId of metadataObj.projectIds) {
|
||||
for await (const projectId of metadataObj.payload.projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
@@ -394,10 +398,13 @@ export const authSignupServiceFactory = ({
|
||||
emails: [user.email!],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
tx
|
||||
{
|
||||
tx,
|
||||
throwOnProjectNotFound: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, TableName, TUsers } from "@app/db/schemas";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -25,8 +32,9 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenMetadataType, TokenType } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { verifyProjectVersion } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
@@ -435,6 +443,7 @@ export const orgServiceFactory = ({
|
||||
userId,
|
||||
inviteeEmails,
|
||||
organizationRoleSlug,
|
||||
projectRoleSlug,
|
||||
projectIds,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
@@ -467,6 +476,22 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (projectIds?.length) {
|
||||
const projects = await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
// if its not v3, throw an error
|
||||
if (!verifyProjectVersion(projects, ProjectVersion.V3)) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||
const users: Pick<
|
||||
TUsers & { orgId: string },
|
||||
@@ -474,6 +499,7 @@ export const orgServiceFactory = ({
|
||||
>[] = [];
|
||||
for await (const inviteeEmail of inviteeEmails) {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
@@ -504,7 +530,22 @@ export const orgServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (projectIds) {
|
||||
if (projectIds?.length) {
|
||||
if (
|
||||
organizationRoleSlug === OrgMembershipRole.Custom ||
|
||||
projectRoleSlug === ProjectMembershipRole.Custom
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Custom roles are not supported for inviting users to projects and organizations"
|
||||
});
|
||||
}
|
||||
|
||||
if (!projectRoleSlug) {
|
||||
throw new BadRequestError({
|
||||
message: "Selecting a project role is required to invite users to projects"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const projectId of projectIds) {
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
@@ -528,10 +569,12 @@ export const orgServiceFactory = ({
|
||||
emails: [inviteeEmail],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
projectMembershipRole: projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
tx
|
||||
{
|
||||
tx
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -561,7 +604,7 @@ export const orgServiceFactory = ({
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: organizationRoleSlug || OrgMembershipRole.Member,
|
||||
role: organizationRoleSlug,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
@@ -588,13 +631,17 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
|
||||
let inviteMetadata: string = "";
|
||||
if (projectIds && projectIds.length > 0) {
|
||||
if (projectIds && projectIds?.length > 0) {
|
||||
inviteMetadata = jwt.sign(
|
||||
{
|
||||
type: TokenMetadataType.InviteToProjects,
|
||||
projectIds,
|
||||
userId: invitee.id
|
||||
},
|
||||
payload: {
|
||||
projectIds,
|
||||
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
}
|
||||
} satisfies TTokenMetadata,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -30,8 +31,9 @@ export type TInviteUserToOrgDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug?: string;
|
||||
organizationRoleSlug: OrgMembershipRole;
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: ProjectMembershipRole;
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
|
||||
@@ -35,6 +35,11 @@ type AddMembersToNonE2EEProjectDTO = {
|
||||
sendEmails?: boolean;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectOptions = {
|
||||
tx?: Knex;
|
||||
throwOnProjectNotFound?: boolean;
|
||||
};
|
||||
|
||||
export const addMembersToProject = ({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
@@ -45,15 +50,22 @@ export const addMembersToProject = ({
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TAddMembersToProjectArg) => {
|
||||
// Can create multiple memberships for a singular project, based on user email / username
|
||||
const addMembersToNonE2EEProject = async (
|
||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||
transaction?: Knex
|
||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||
) => {
|
||||
const processTransaction = async (tx: Knex) => {
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
if (!project) {
|
||||
if (options.throwOnProjectNotFound) {
|
||||
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
||||
project.orgId,
|
||||
@@ -61,12 +73,6 @@ export const addMembersToProject = ({
|
||||
tx
|
||||
);
|
||||
|
||||
// const allorgMembers = await orgDAL.findAllOrgMembers(project.orgId,);
|
||||
|
||||
console.log("usernameandena", usernamesAndEmails);
|
||||
console.log("orgMembers", orgMembers);
|
||||
// console.log("allorgMembers", allorgMembers);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
@@ -131,7 +137,7 @@ export const addMembersToProject = ({
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -172,8 +178,8 @@ export const addMembersToProject = ({
|
||||
return members;
|
||||
};
|
||||
|
||||
if (transaction) {
|
||||
return processTransaction(transaction);
|
||||
if (options.tx) {
|
||||
return processTransaction(options.tx);
|
||||
}
|
||||
return projectMembershipDAL.transaction(processTransaction);
|
||||
};
|
||||
|
||||
52
backend/src/services/project-role/project-role-fns.ts
Normal file
52
backend/src/services/project-role/project-role-fns.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
projectViewerPermission
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
|
||||
export 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));
|
||||
};
|
||||
@@ -5,13 +5,9 @@ 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
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
@@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||
import { getPredefinedRoles } from "./project-role-fns";
|
||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||
|
||||
type TProjectRoleServiceFactoryDep = {
|
||||
@@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||
};
|
||||
|
||||
export const verifyProjectVersion = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
|
||||
for (const project of projects) {
|
||||
if (project.version !== version) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getProjectKmsCertificateKeyId = async ({
|
||||
projectId,
|
||||
projectDAL,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
@@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
@@ -44,6 +47,7 @@ import {
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
TListProjectCertsDTO,
|
||||
TListProjectsDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
@@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
| "updateProjectSecretManagerKmsKey"
|
||||
@@ -112,6 +117,7 @@ export const projectServiceFactory = ({
|
||||
projectEnvDAL,
|
||||
licenseService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
@@ -389,8 +395,37 @@ export const projectServiceFactory = ({
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
const getProjects = async (actorId: string) => {
|
||||
const getProjects = async ({ actorId, includeRoles, actor, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||
|
||||
if (includeRoles) {
|
||||
const customRoles = await projectRoleDAL.find({
|
||||
$in: {
|
||||
projectId: workspaces.map((workspace) => workspace.id)
|
||||
}
|
||||
});
|
||||
|
||||
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
|
||||
|
||||
const workspacesWithRoles = await Promise.all(
|
||||
workspaces.map(async (workspace) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
workspace.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
return {
|
||||
...workspace,
|
||||
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return workspacesWithRoles;
|
||||
}
|
||||
return workspaces;
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
userPrivateKey: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -32,7 +32,8 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
.map(async (email) => {
|
||||
mutateAsync({
|
||||
inviteeEmails: [email],
|
||||
organizationId: String(localStorage.getItem("orgData.id"))
|
||||
organizationId: String(localStorage.getItem("orgData.id")),
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const roleQueryKeys = {
|
||||
["user-project-permissions", { workspaceId }] as const
|
||||
};
|
||||
|
||||
const getProjectRoles = async (projectId: string) => {
|
||||
export const getProjectRoles = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||
`/api/v1/workspace/${projectId}/roles`
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ export const useAddUsersToOrg = () => {
|
||||
type Response = {
|
||||
data: {
|
||||
message: string;
|
||||
completeInviteLinks: {
|
||||
completeInviteLinks?: {
|
||||
email: string;
|
||||
link: string;
|
||||
}[];
|
||||
|
||||
@@ -151,7 +151,8 @@ export type DeletOrgMembershipDTO = {
|
||||
export type AddUserToOrgDTO = {
|
||||
inviteeEmails: string[];
|
||||
projectIds?: string[];
|
||||
organizationRoleSlug?: string;
|
||||
projectRoleSlug?: string;
|
||||
organizationRoleSlug: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
|
||||
params: {
|
||||
includeRoles
|
||||
}
|
||||
});
|
||||
return data.workspaces;
|
||||
};
|
||||
|
||||
@@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserWorkspaces = () =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
|
||||
|
||||
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TProjectRole } from "../roles/types";
|
||||
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2,
|
||||
@@ -22,6 +24,8 @@ export type Workspace = {
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
|
||||
roles?: TProjectRole[];
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -25,18 +26,21 @@ import {
|
||||
useGetOrgRoles,
|
||||
useGetUserWorkspaces
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { OrgInviteLink } from "./OrgInviteLink";
|
||||
|
||||
const DEFAULT_ORG_MEMBER_ROLE_SLUG = "member";
|
||||
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
|
||||
|
||||
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
emails: z.string().min(1).trim().toLowerCase(),
|
||||
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
|
||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_MEMBER_ROLE_SLUG)
|
||||
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
|
||||
});
|
||||
|
||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||
@@ -71,7 +75,12 @@ export const AddOrgMemberModal = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
|
||||
|
||||
const onAddMembers = async ({ emails, organizationRoleSlug, projectIds }: TAddMemberForm) => {
|
||||
const onAddMembers = async ({
|
||||
emails,
|
||||
organizationRoleSlug,
|
||||
projectIds,
|
||||
projectRoleSlug
|
||||
}: TAddMemberForm) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
@@ -98,7 +107,8 @@ export const AddOrgMemberModal = ({
|
||||
organizationId: currentOrg?.id,
|
||||
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
||||
organizationRoleSlug,
|
||||
projectIds
|
||||
projectIds,
|
||||
projectRoleSlug
|
||||
});
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||
@@ -128,7 +138,12 @@ export const AddOrgMemberModal = ({
|
||||
};
|
||||
|
||||
const projectIds = watch("projectIds", []);
|
||||
const { data: projects } = useGetUserWorkspaces();
|
||||
const { data: projects } = useGetUserWorkspaces(true);
|
||||
|
||||
const filteredProjects = useMemo(
|
||||
() => projects?.filter((project) => project.version === ProjectVersion.V3),
|
||||
[projects]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -179,7 +194,7 @@ export const AddOrgMemberModal = ({
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={DEFAULT_ORG_MEMBER_ROLE_SLUG}
|
||||
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||
{...field}
|
||||
onValueChange={(val) => field.onChange(val)}
|
||||
>
|
||||
@@ -195,83 +210,117 @@ export const AddOrgMemberModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="-space-y-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Add user to projects"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{projects && projects.length > 0 ? (
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{projectIds.length === 1
|
||||
? projects.find((project) => project.id === projectIds[0])?.name
|
||||
: `${projectIds.length} projects selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
No projects found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80"
|
||||
>
|
||||
{projects && projects.length > 0 ? (
|
||||
projects.map((project) => {
|
||||
const isSelected = projectIds.includes(String(project.id));
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Add user to projects"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{filteredProjects && filteredProjects.length > 0 ? (
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{projectIds.length === 1
|
||||
? filteredProjects.find((project) => project.id === projectIds[0])
|
||||
?.name
|
||||
: `${projectIds.length} projects selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
No projects found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80"
|
||||
>
|
||||
{filteredProjects && filteredProjects.length > 0 ? (
|
||||
filteredProjects.map((project) => {
|
||||
const isSelected = projectIds.includes(String(project.id));
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => projects.length > 0 && event.preventDefault()}
|
||||
onClick={() => {
|
||||
if (projectIds.includes(String(project.id))) {
|
||||
field.onChange(
|
||||
projectIds.filter(
|
||||
(projectId: string) => projectId !== String(project.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...projectIds, String(project.id)]);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
filteredProjects.length > 0 && event.preventDefault()
|
||||
}
|
||||
}}
|
||||
key={`project-id-${project.id}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{project.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<p className="ml-0.5 text-xs text-mineshaft-400">
|
||||
Members will automatically be assigned the <a className="font-bold">Developer</a>{" "}
|
||||
role for the projects you select.
|
||||
</p>
|
||||
onClick={() => {
|
||||
if (projectIds.includes(String(project.id))) {
|
||||
field.onChange(
|
||||
projectIds.filter(
|
||||
(projectId: string) => projectId !== String(project.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...projectIds, String(project.id)]);
|
||||
}
|
||||
}}
|
||||
key={`project-id-${project.id}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{project.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-fit justify-end">
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectRoleSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="Select which role to assign to the users in the selected projects."
|
||||
label="Role"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<div>
|
||||
<Select
|
||||
isDisabled={projectIds.length === 0}
|
||||
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||
{...field}
|
||||
onValueChange={(val) => field.onChange(val)}
|
||||
>
|
||||
{Object.entries(ProjectMembershipRole).map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_, slug]) =>
|
||||
slug !== "custom" && (
|
||||
<SelectItem key={slug} value={slug}>
|
||||
<span className="capitalize">{slug.replace("-", " ")}</span>
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
|
||||
@@ -108,7 +108,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
try {
|
||||
const { data } = await addUsersMutateAsync({
|
||||
organizationId: orgId,
|
||||
inviteeEmails: [email]
|
||||
inviteeEmails: [email],
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks || null);
|
||||
|
||||
@@ -50,7 +50,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
try {
|
||||
const { data } = await inviteUsers({
|
||||
organizationId: orgId,
|
||||
inviteeEmails: [email]
|
||||
inviteeEmails: [email],
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
|
||||
// setCompleteInviteLink(data?.completeInviteLink || "");
|
||||
|
||||
Reference in New Issue
Block a user