Fix merge conflicts

This commit is contained in:
Tuan Dang
2024-03-12 16:20:17 -07:00
81 changed files with 3330 additions and 915 deletions
+16
View File
@@ -32,6 +32,9 @@ import {
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate,
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
@@ -83,6 +86,9 @@ import {
TProjects,
TProjectsInsert,
TProjectsUpdate,
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
@@ -221,6 +227,11 @@ declare module "knex/types/tables" {
TProjectEnvironmentsUpdate
>;
[TableName.ProjectBot]: Knex.CompositeTableType<TProjectBots, TProjectBotsInsert, TProjectBotsUpdate>;
[TableName.ProjectUserMembershipRole]: Knex.CompositeTableType<
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
@@ -272,6 +283,11 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.IdentityProjectMembershipRole]: Knex.CompositeTableType<
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
@@ -0,0 +1,50 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const doesTableExist = await knex.schema.hasTable(TableName.ProjectUserMembershipRole);
if (!doesTableExist) {
await knex.schema.createTable(TableName.ProjectUserMembershipRole, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable();
t.uuid("projectMembershipId").notNullable();
t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
// until role is changed/removed the role should not deleted
t.uuid("customRoleId");
t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.ProjectUserMembershipRole);
const projectMemberships = await knex(TableName.ProjectMembership).select(
"id",
"role",
"createdAt",
"updatedAt",
knex.ref("roleId").withSchema(TableName.ProjectMembership).as("customRoleId")
);
if (projectMemberships.length)
await knex.batchInsert(
TableName.ProjectUserMembershipRole,
projectMemberships.map((data) => ({ ...data, projectMembershipId: data.id }))
);
// will be dropped later
// await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
// t.dropColumn("roleId");
// t.dropColumn("role");
// });
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ProjectUserMembershipRole);
await dropOnUpdateTrigger(knex, TableName.ProjectUserMembershipRole);
}
@@ -0,0 +1,52 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const doesTableExist = await knex.schema.hasTable(TableName.IdentityProjectMembershipRole);
if (!doesTableExist) {
await knex.schema.createTable(TableName.IdentityProjectMembershipRole, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable();
t.uuid("projectMembershipId").notNullable();
t.foreign("projectMembershipId")
.references("id")
.inTable(TableName.IdentityProjectMembership)
.onDelete("CASCADE");
// until role is changed/removed the role should not deleted
t.uuid("customRoleId");
t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
t.boolean("isTemporary").notNullable().defaultTo(false);
t.string("temporaryMode");
t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
t.datetime("temporaryAccessStartTime");
t.datetime("temporaryAccessEndTime");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.IdentityProjectMembershipRole);
const identityMemberships = await knex(TableName.IdentityProjectMembership).select(
"id",
"role",
"createdAt",
"updatedAt",
knex.ref("roleId").withSchema(TableName.IdentityProjectMembership).as("customRoleId")
);
if (identityMemberships.length)
await knex.batchInsert(
TableName.IdentityProjectMembershipRole,
identityMemberships.map((data) => ({ ...data, projectMembershipId: data.id }))
);
// await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
// t.dropColumn("roleId");
// t.dropColumn("role");
// });
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityProjectMembershipRole);
await dropOnUpdateTrigger(knex, TableName.IdentityProjectMembershipRole);
}
@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityProjectMembershipRoleSchema = z.object({
id: z.string().uuid(),
role: z.string(),
projectMembershipId: z.string().uuid(),
customRoleId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityProjectMembershipRole = z.infer<typeof IdentityProjectMembershipRoleSchema>;
export type TIdentityProjectMembershipRoleInsert = Omit<
z.input<typeof IdentityProjectMembershipRoleSchema>,
TImmutableDBKeys
>;
export type TIdentityProjectMembershipRoleUpdate = Partial<
Omit<z.input<typeof IdentityProjectMembershipRoleSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -8,6 +8,7 @@ export * from "./git-app-org";
export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-org-memberships";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";
export * from "./identity-ua-client-secrets";
export * from "./identity-universal-auths";
@@ -25,6 +26,7 @@ export * from "./project-environments";
export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-user-membership-roles";
export * from "./projects";
export * from "./saml-configs";
export * from "./scim-tokens";
+2
View File
@@ -20,6 +20,7 @@ export enum TableName {
Environment = "project_environments",
ProjectMembership = "project_memberships",
ProjectRoles = "project_roles",
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
Secret = "secrets",
SecretBlindIndex = "secret_blind_indexes",
@@ -41,6 +42,7 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
ScimToken = "scim_tokens",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectUserMembershipRolesSchema = z.object({
id: z.string().uuid(),
role: z.string(),
projectMembershipId: z.string().uuid(),
customRoleId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectUserMembershipRoles = z.infer<typeof ProjectUserMembershipRolesSchema>;
export type TProjectUserMembershipRolesInsert = Omit<
z.input<typeof ProjectUserMembershipRolesSchema>,
TImmutableDBKeys
>;
export type TProjectUserMembershipRolesUpdate = Partial<
Omit<z.input<typeof ProjectUserMembershipRolesSchema>, TImmutableDBKeys>
>;
+11 -5
View File
@@ -4,7 +4,7 @@ import { Knex } from "knex";
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@@ -30,10 +30,16 @@ export async function seed(knex: Knex): Promise<void> {
})
.returning("*");
await knex(TableName.ProjectMembership).insert({
projectId: project.id,
role: OrgMembershipRole.Admin,
userId: seedData1.id
const projectMembership = await knex(TableName.ProjectMembership)
.insert({
projectId: project.id,
userId: seedData1.id,
role: ProjectMembershipRole.Admin
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
+10 -3
View File
@@ -75,9 +75,16 @@ export async function seed(knex: Knex): Promise<void> {
}
]);
await knex(TableName.IdentityProjectMembership).insert({
identityId: seedData1.machineIdentity.id,
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
.insert({
identityId: seedData1.machineIdentity.id,
projectId: seedData1.project.id,
role: ProjectMembershipRole.Admin
})
.returning("*");
await knex(TableName.IdentityProjectMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectId: seedData1.project.id
projectMembershipId: identityProjectMembership[0].id
});
}
+24 -3
View File
@@ -1,6 +1,7 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -13,7 +14,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
organizationId: z.string().trim()
}),
body: z.object({
slug: z.string().trim(),
slug: z
.string()
.min(1)
.trim()
.refine(
(val) => Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
}),
name: z.string().trim(),
description: z.string().trim().optional(),
permissions: z.any().array()
@@ -45,7 +56,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim()
}),
body: z.object({
slug: z.string().trim().optional(),
slug: z
.string()
.trim()
.optional()
.refine(
(val) => typeof val === "undefined" || Object.keys(OrgMembershipRole).includes(val),
"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(),
description: z.string().trim().optional(),
permissions: z.any().array()
@@ -1,7 +1,9 @@
import { z } from "zod";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { selectAllTableCols } from "@app/lib/knex";
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
@@ -43,21 +45,72 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const membership = await db(TableName.ProjectMembership)
.leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`)
const docs = await db(TableName.ProjectMembership)
.join(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectMembership))
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select(
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project)
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions")
.first();
.select("permissions");
return membership;
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
parentMapper: ({
orgId,
orgAuthEnforced,
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
oldRoleField
}) => ({
orgId,
orgAuthEnforced,
userId,
role: oldRoleField,
id: membershipId,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
ProjectUserMembershipRolesSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
}
]
});
// when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });
}
@@ -65,18 +118,62 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
try {
const membership = await db(TableName.IdentityProjectMembership)
const docs = await db(TableName.IdentityProjectMembership)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembership}.roleId`,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.where("identityId", identityId)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.IdentityProjectMembership))
.select("permissions")
.first();
return membership;
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions");
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({
id: membershipId,
identityId,
projectId,
role: oldRoleField,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
// just a prefilled value
orgAuthEnforced: false,
orgId: ""
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
IdentityProjectMembershipRoleSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
}
]
});
// when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
}
@@ -18,6 +18,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { TBuildProjectPermissionDTO } from "./permission-types";
import {
buildServiceTokenProjectPermission,
projectAdminPermissions,
@@ -64,31 +65,35 @@ export const permissionServiceFactory = ({
}
};
const buildProjectPermission = (role: string, permission?: unknown) => {
switch (role) {
case ProjectMembershipRole.Admin:
return projectAdminPermissions;
case ProjectMembershipRole.Member:
return projectMemberPermissions;
case ProjectMembershipRole.Viewer:
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.Custom:
return createMongoAbility<ProjectPermissionSet>(
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permission as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
),
{
conditionsMatcher
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles
.map(({ role, permissions }) => {
switch (role) {
case ProjectMembershipRole.Admin:
return projectAdminPermissions;
case ProjectMembershipRole.Member:
return projectMemberPermissions;
case ProjectMembershipRole.Viewer:
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.Custom: {
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
);
}
);
default:
throw new BadRequestError({
name: "ProjectRoleInvalid",
message: "Project role not found"
});
}
default:
throw new BadRequestError({
name: "ProjectRoleInvalid",
message: "Project role not found"
});
}
})
.reduce((curr, prev) => prev.concat(curr), []);
return createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
};
/*
@@ -145,33 +150,56 @@ export const permissionServiceFactory = ({
};
// user permission for a project in an organization
const getUserProjectPermission = async (userId: string, projectId: string, userOrgId?: string) => {
const membership = await permissionDAL.getProjectPermission(userId, projectId);
if (!membership) throw new UnauthorizedError({ name: "User not in project" });
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
const getUserProjectPermission = async (
userId: string,
projectId: string,
userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" });
if (
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
) {
throw new BadRequestError({ name: "Custom permission not found" });
}
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) {
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
}
return {
permission: buildProjectPermission(membership.role, membership.permissions),
membership
permission: buildProjectPermission(userProjectPermission.roles),
membership: userProjectPermission,
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
};
};
const getIdentityProjectPermission = async (identityId: string, projectId: string) => {
const membership = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!membership) throw new UnauthorizedError({ name: "Identity not in project" });
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
const getIdentityProjectPermission = async (
identityId: string,
projectId: string
): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" });
if (
identityProjectPermission.roles.some(
({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions
)
) {
throw new BadRequestError({ name: "Custom permission not found" });
}
return {
permission: buildProjectPermission(membership.role, membership.permissions),
membership
permission: buildProjectPermission(identityProjectPermission.roles),
membership: identityProjectPermission,
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
};
};
@@ -191,14 +219,19 @@ export const permissionServiceFactory = ({
};
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
? { permission: MongoAbility<ProjectPermissionSet, MongoQuery>; membership: undefined }
? {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined;
hasRole: (arg: string) => boolean;
} // service token doesn't have both membership and roles
: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
orgAuthEnforced: boolean;
orgAuthEnforced: boolean | null | undefined;
orgId: string;
permissions?: unknown;
roles: Array<{ role: string }>;
};
hasRole: (role: string) => boolean;
};
const getProjectPermission = async <T extends ActorType>(
@@ -228,11 +261,13 @@ export const permissionServiceFactory = ({
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
return {
permission: buildProjectPermission(ProjectMembershipRole.Custom, projectRole.permissions),
permission: buildProjectPermission([
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
]),
role: projectRole
};
}
return { permission: buildProjectPermission(role, []) };
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
};
return {
@@ -0,0 +1,4 @@
export type TBuildProjectPermissionDTO = {
permissions?: unknown;
role: string;
}[];
@@ -56,8 +56,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
@@ -135,13 +135,13 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
return build({ conditionsMatcher });
return rules;
};
export const projectAdminPermissions = buildAdminPermission();
export const projectAdminPermissions = buildAdminPermissionRules();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
@@ -196,13 +196,13 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
return rules;
};
export const projectMemberPermissions = buildMemberPermission();
export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
@@ -220,14 +220,14 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
return rules;
};
export const projectViewerPermission = buildViewerPermission();
export const projectViewerPermission = buildViewerPermissionRules();
const buildNoAccessProjectPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return rules;
};
export const buildServiceTokenProjectPermission = (
@@ -129,14 +129,14 @@ export const secretApprovalRequestServiceFactory = ({
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { policy } = secretApprovalRequest;
const { membership } = await permissionService.getProjectPermission(
const { membership, hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalRequest.projectId,
actorOrgId
);
if (
membership.role !== ProjectMembershipRole.Admin &&
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
) {
@@ -156,14 +156,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership } = await permissionService.getProjectPermission(
const { membership, hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
actorOrgId
);
if (
membership.role !== ProjectMembershipRole.Admin &&
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
) {
@@ -198,14 +198,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy } = secretApprovalRequest;
const { membership } = await permissionService.getProjectPermission(
const { membership, hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
actorOrgId
);
if (
membership.role !== ProjectMembershipRole.Admin &&
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
) {
@@ -236,9 +236,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
const { policy, folderId, projectId } = secretApprovalRequest;
const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId, actorOrgId);
const { membership, hasRole } = await permissionService.getProjectPermission(
ActorType.USER,
actorId,
projectId,
actorOrgId
);
if (
membership.role !== ProjectMembershipRole.Admin &&
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
!policy.approvers.find((approverId) => approverId === membership.id)
) {
@@ -1,3 +1,10 @@
import {
CreateAccessKeyCommand,
DeleteAccessKeyCommand,
GetAccessKeyLastUsedCommand,
IAMClient
} from "@aws-sdk/client-iam";
import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import {
@@ -18,7 +25,12 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { TSecretRotationDALFactory } from "../secret-rotation-dal";
import { rotationTemplates } from "../templates";
import { TDbProviderClients, TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types";
import {
TAwsProviderSystems,
TDbProviderClients,
TProviderFunctionTypes,
TSecretRotationProviderTemplate
} from "../templates/types";
import {
getDbSetQuery,
secretRotationDbFn,
@@ -127,7 +139,10 @@ export const secretRotationQueueFactory = ({
internal: {}
};
// when its a database we keep cycling the variables accordingly
/* Rotation Function For Database
* A database like sql cannot have multiple password for a user
* thus we ask users to create two users with required permission and then we keep cycling between these two db users
*/
if (provider.template.type === TProviderFunctionTypes.DB) {
const lastCred = variables.creds.at(-1);
if (lastCred && variables.creds.length === 1) {
@@ -170,6 +185,65 @@ export const secretRotationQueueFactory = ({
if (variables.creds.length === 2) variables.creds.pop();
}
/*
* Rotation Function For AWS Services
* Due to complexity in AWS Authorization hashing signature process we keep it as seperate entity instead of http template mode
* We first delete old key before creating a new one because aws iam has a quota limit of 2 keys
* */
if (provider.template.type === TProviderFunctionTypes.AWS) {
if (provider.template.client === TAwsProviderSystems.IAM) {
const client = new IAMClient({
region: newCredential.inputs.manager_user_aws_region as string,
credentials: {
accessKeyId: newCredential.inputs.manager_user_access_key as string,
secretAccessKey: newCredential.inputs.manager_user_secret_key as string
}
});
const iamUserName = newCredential.inputs.iam_username as string;
if (variables.creds.length === 2) {
const deleteCycleCredential = variables.creds.pop();
if (deleteCycleCredential) {
const deletedIamAccessKey = await client.send(
new DeleteAccessKeyCommand({
UserName: iamUserName,
AccessKeyId: deleteCycleCredential.outputs.iam_user_access_key as string
})
);
if (
!deletedIamAccessKey?.$metadata?.httpStatusCode ||
deletedIamAccessKey?.$metadata?.httpStatusCode > 300
) {
throw new DisableRotationErrors({
message: "Failed to delete aws iam access key. Check managed iam user policy"
});
}
}
}
const newIamAccessKey = await client.send(new CreateAccessKeyCommand({ UserName: iamUserName }));
if (!newIamAccessKey.AccessKey)
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
// test
const testAccessKey = await client.send(
new GetAccessKeyLastUsedCommand({ AccessKeyId: newIamAccessKey.AccessKey.AccessKeyId })
);
if (testAccessKey?.UserName !== iamUserName)
throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" });
newCredential.outputs.iam_user_access_key = newIamAccessKey.AccessKey.AccessKeyId;
newCredential.outputs.iam_user_secret_key = newIamAccessKey.AccessKey.SecretAccessKey;
}
}
/* Rotation function of HTTP infisical template
* This is a generic http based template system for rotation
* we use this for sendgrid and for custom secret rotation
* This will ensure user provided rotation is easier to make
* */
if (provider.template.type === TProviderFunctionTypes.HTTP) {
if (provider.template.functions.set?.pre) {
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
@@ -185,6 +259,9 @@ export const secretRotationQueueFactory = ({
}
}
}
// insert the new variables to start
// encrypt the data - save it
variables.creds.unshift({
outputs: newCredential.outputs,
internal: newCredential.internal
@@ -200,6 +277,7 @@ export const secretRotationQueueFactory = ({
key
)
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
@@ -0,0 +1,21 @@
import { TAwsProviderSystems, TProviderFunctionTypes } from "./types";
export const AWS_IAM_TEMPLATE = {
type: TProviderFunctionTypes.AWS as const,
client: TAwsProviderSystems.IAM,
inputs: {
type: "object" as const,
properties: {
manager_user_access_key: { type: "string" as const },
manager_user_secret_key: { type: "string" as const },
manager_user_aws_region: { type: "string" as const },
iam_username: { type: "string" as const }
},
required: ["manager_user_access_key", "manager_user_secret_key", "manager_user_aws_region", "iam_username"],
additionalProperties: false
},
outputs: {
iam_user_access_key: { type: "string" },
iam_user_secret_key: { type: "string" }
}
};
@@ -1,3 +1,4 @@
import { AWS_IAM_TEMPLATE } from "./aws-iam";
import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid";
@@ -24,5 +25,12 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
image: "mysql.png",
description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE
},
{
name: "aws-iam",
title: "AWS IAM",
image: "aws-iam.svg",
description: "Rotate AWS IAM User credentials",
template: AWS_IAM_TEMPLATE
}
];
@@ -1,6 +1,7 @@
export enum TProviderFunctionTypes {
HTTP = "http",
DB = "database"
DB = "database",
AWS = "aws"
}
export enum TDbProviderClients {
@@ -10,6 +11,10 @@ export enum TDbProviderClients {
MySql = "mysql"
}
export enum TAwsProviderSystems {
IAM = "iam"
}
export enum TAssignOp {
Direct = "direct",
JmesPath = "jmesopath"
@@ -42,7 +47,7 @@ export type TSecretRotationProviderTemplate = {
title: string;
image?: string;
description?: string;
template: THttpProviderTemplate | TDbProviderTemplate;
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
};
export type THttpProviderTemplate = {
@@ -70,3 +75,14 @@ export type TDbProviderTemplate = {
};
outputs: Record<string, unknown>;
};
export type TAwsProviderTemplate = {
type: TProviderFunctionTypes.AWS;
client: TAwsProviderSystems;
inputs: {
type: "object";
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
required?: string[];
};
outputs: Record<string, unknown>;
};
+14 -3
View File
@@ -53,6 +53,7 @@ import { identityServiceFactory } from "@app/services/identity/identity-service"
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
@@ -78,6 +79,7 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"
import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { secretDALFactory } from "@app/services/secret/secret-dal";
@@ -141,6 +143,7 @@ export const registerRoutes = async (
const projectDAL = projectDALFactory(db);
const projectMembershipDAL = projectMembershipDALFactory(db);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
const projectRoleDAL = projectRoleDALFactory(db);
const projectEnvDAL = projectEnvDALFactory(db);
const projectKeyDAL = projectKeyDALFactory(db);
@@ -164,6 +167,7 @@ export const registerRoutes = async (
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
const identityOrgMembershipDAL = identityOrgDALFactory(db);
const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
@@ -321,6 +325,7 @@ export const registerRoutes = async (
const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDAL,
projectUserMembershipRoleDAL,
projectDAL,
permissionService,
projectBotDAL,
@@ -352,7 +357,8 @@ export const registerRoutes = async (
projectBotDAL,
projectMembershipDAL,
secretApprovalRequestDAL,
secretApprovalSecretDAL: sarSecretDAL
secretApprovalSecretDAL: sarSecretDAL,
projectUserMembershipRoleDAL
});
const projectService = projectServiceFactory({
@@ -369,8 +375,11 @@ export const registerRoutes = async (
orgService,
projectMembershipDAL,
folderDAL,
licenseService
licenseService,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL
});
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDAL,
@@ -521,7 +530,9 @@ export const registerRoutes = async (
permissionService,
projectDAL,
identityProjectDAL,
identityOrgMembershipDAL
identityOrgMembershipDAL,
identityProjectMembershipRoleDAL,
projectRoleDAL
});
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
@@ -1,15 +1,17 @@
import ms from "ms";
import { z } from "zod";
import {
OrgMembershipsSchema,
ProjectMembershipRole,
ProjectMembershipsSchema,
ProjectUserMembershipRolesSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -28,16 +30,31 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
memberships: ProjectMembershipsSchema.omit({ role: true })
.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
@@ -86,10 +103,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
members: req.body.members.map((member) => ({
...member,
projectRole: ProjectMembershipRole.Member
}))
members: req.body.members
});
await server.services.auditLog.createAuditLog({
@@ -124,39 +138,56 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
membershipId: z.string().trim()
}),
body: z.object({
role: z.string().trim()
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.min(1)
.refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required")
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.updateProjectMembership({
const roles = await server.services.projectMembership.updateProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId,
role: req.body.role
roles: req.body.roles
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: membership.userId,
newRole: req.body.role,
oldRole: membership.role,
email: ""
}
}
});
return { membership };
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: req.params.workspaceId,
// event: {
// type: EventType.UPDATE_USER_WORKSPACE_ROLE,
// metadata: {
// userId: membership.userId,
// newRole: req.body.role,
// oldRole: membership.role,
// email: ""
// }
// }
// });
return { roles };
}
});
+26 -11
View File
@@ -60,17 +60,32 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
users: ProjectMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
username: true,
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
users: ProjectMembershipsSchema.omit({ role: true })
.merge(
z.object({
user: UsersSchema.pick({
username: true,
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
@@ -1,13 +1,15 @@
import ms from "ms";
import { z } from "zod";
import {
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
ProjectRolesSchema
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -57,24 +59,40 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
identityId: z.string().trim()
}),
body: z.object({
role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess)
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.min(1)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.updateProjectIdentity({
const roles = await server.services.identityProject.updateProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
role: req.body.role
roles: req.body.roles
});
return { identityMembership };
return { roles };
}
});
@@ -127,18 +145,29 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}),
response: {
200: z.object({
identityMemberships: IdentityProjectMembershipsSchema.merge(
z.object({
customRole: ProjectRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional(),
identityMemberships: z
.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
})
).array()
.array()
})
}
},
@@ -3,7 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, sqlNestRelationships } from "@app/lib/knex";
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
@@ -15,52 +15,81 @@ export const identityProjectDALFactory = (db: TDbClient) => {
const docs = await (tx || db)(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembership}.roleId`,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(selectAllTableCols(TableName.IdentityProjectMembership))
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.ProjectRoles))
.select(db.ref("name").as("crName").withSchema(TableName.ProjectRoles))
.select(db.ref("slug").as("crSlug").withSchema(TableName.ProjectRoles))
.select(db.ref("description").as("crDescription").withSchema(TableName.ProjectRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles))
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
return docs.map(
({
crId,
crDescription,
crSlug,
crPermission,
crName,
identityId,
identityName,
identityAuthMethod,
...el
}) => ({
...el,
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole)
);
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt }) => ({
id,
identityId,
createdAt,
updatedAt,
identity: {
id: identityId,
name: identityName,
authMethod: identityAuthMethod
},
customRole: el.roleId
? {
id: crId,
name: crName,
slug: crSlug,
permissions: crPermission,
description: crDescription
}
: undefined
})
);
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
} catch (error) {
throw new DatabaseError({ error, name: "FindByProjectId" });
}
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProjectMembershipRoleDALFactory = ReturnType<typeof identityProjectMembershipRoleDALFactory>;
export const identityProjectMembershipRoleDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.IdentityProjectMembershipRole);
return orm;
};
@@ -1,15 +1,20 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ProjectMembershipRole, TProjectRoles } from "@app/db/schemas";
import { ProjectMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TIdentityProjectDALFactory } from "./identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-membership-role-dal";
import {
TCreateProjectIdentityDTO,
TDeleteProjectIdentityDTO,
@@ -19,7 +24,12 @@ import {
type TIdentityProjectServiceFactoryDep = {
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<
TIdentityProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
>;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
};
@@ -30,7 +40,9 @@ export const identityProjectServiceFactory = ({
identityProjectDAL,
permissionService,
identityOrgMembershipDAL,
projectDAL
identityProjectMembershipRoleDAL,
projectDAL,
projectRoleDAL
}: TIdentityProjectServiceFactoryDep) => {
const createProjectIdentity = async ({
identityId,
@@ -70,11 +82,26 @@ export const identityProjectServiceFactory = ({
});
const isCustomRole = Boolean(customRole);
const projectIdentity = await identityProjectDAL.create({
identityId,
projectId: project.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role,
roleId: customRole?.id
const projectIdentity = await identityProjectDAL.transaction(async (tx) => {
const identityProjectMembership = await identityProjectDAL.create(
{
identityId,
projectId: project.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
await identityProjectMembershipRoleDAL.create(
{
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role,
customRoleId: customRole?.id
},
tx
);
return identityProjectMembership;
});
return projectIdentity;
};
@@ -82,7 +109,7 @@ export const identityProjectServiceFactory = ({
const updateProjectIdentity = async ({
projectId,
identityId,
role,
roles,
actor,
actorId,
actorOrgId
@@ -106,28 +133,51 @@ export const identityProjectServiceFactory = ({
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
let customRole: TProjectRoles | undefined;
if (role) {
const { permission: rolePermission, role: customOrgRole } = await permissionService.getProjectPermissionByRole(
role,
projectIdentity.projectId
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
if (isCustomRole) customRole = customOrgRole;
}
const [updatedProjectIdentity] = await identityProjectDAL.update(
{ projectId, identityId: projectIdentity.identityId },
{
role: customRole ? ProjectMembershipRole.Custom : role,
roleId: customRole ? customRole.id : null
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
return updatedProjectIdentity;
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: projectIdentity.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: projectIdentity.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
});
return updatedRoles;
};
const deleteProjectIdentity = async ({
@@ -1,12 +1,26 @@
import { TProjectPermission } from "@app/lib/types";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
export type TCreateProjectIdentityDTO = {
identityId: string;
role: string;
} & TProjectPermission;
export type TUpdateProjectIdentityDTO = {
role: string;
roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
identityId: string;
} & TProjectPermission;
+2 -2
View File
@@ -58,7 +58,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
{ id: roleId, orgId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole;
};
@@ -66,7 +66,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return deletedRole;
};
@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
@@ -11,32 +11,86 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
// special query
const findAllProjectMembers = async (projectId: string) => {
try {
const members = await db(TableName.ProjectMembership)
.where({ projectId })
const docs = await db(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.join(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(
db.ref("id").withSchema(TableName.ProjectMembership),
db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole)
)
.where({ isGhost: false });
return members.map(({ username, email, firstName, lastName, publicKey, isGhost, ...data }) => ({
...data,
user: { username, email, firstName, lastName, id: data.userId, publicKey, isGhost }
}));
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });
}
@@ -1,14 +1,13 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import {
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships,
TUsers
TProjectMemberships
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -29,22 +28,24 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import {
ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO,
TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO,
TGetProjectMembershipDTO,
TInviteUserToProjectDTO,
TUpdateProjectMembershipDTO
} from "./project-membership-types";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: TSmtpService;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
@@ -56,6 +57,7 @@ export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembersh
export const projectMembershipServiceFactory = ({
permissionService,
projectMembershipDAL,
projectUserMembershipRoleDAL,
smtpService,
projectRoleDAL,
projectBotDAL,
@@ -72,82 +74,6 @@ export const projectMembershipServiceFactory = ({
return projectMembershipDAL.findAllProjectMembers(projectId);
};
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const invitees: TUsers[] = [];
const project = await projectDAL.findById(projectId);
const users = await userDAL.find({
$in: { email: emails }
});
await projectDAL.transaction(async (tx) => {
for (const invitee of users) {
if (!invitee.isAccepted)
throw new BadRequestError({
message: "Failed to validate invitee",
name: "Invite user to project"
});
const inviteeMembership = await projectMembershipDAL.findOne(
{
userId: invitee.id,
projectId
},
tx
);
if (inviteeMembership) {
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
}
const inviteeMembershipOrg = await orgDAL.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
});
if (!inviteeMembershipOrg) {
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
});
}
await projectMembershipDAL.create(
{
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
},
tx
);
invitees.push(invitee);
}
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: invitees.filter((i) => i.email).map((i) => i.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
});
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
return { invitees, latestKey };
};
const addUsersToProject = async ({
projectId,
actorId,
@@ -176,17 +102,16 @@ export const projectMembershipServiceFactory = ({
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
await projectMembershipDAL.transaction(async (tx) => {
await projectMembershipDAL.insertMany(
orgMembers.map(({ userId, id: membershipId }) => {
const role =
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
return {
projectId,
userId: userId as string,
role
};
}),
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string,
role: ProjectMembershipRole.Member
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
tx
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
@@ -296,7 +221,7 @@ export const projectMembershipServiceFactory = ({
const members: TProjectMemberships[] = [];
await projectMembershipDAL.transaction(async (tx) => {
const result = await projectMembershipDAL.insertMany(
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id,
@@ -304,8 +229,12 @@ export const projectMembershipServiceFactory = ({
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
tx
);
members.push(...result);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
@@ -346,43 +275,71 @@ export const projectMembershipServiceFactory = ({
actorOrgId,
projectId,
membershipId,
role
roles
}: TUpdateProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.isGhost) {
if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) {
throw new BadRequestError({
message: "Unauthorized member update",
name: "Update project membership"
});
}
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) {
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
if (!customRole) throw new BadRequestError({ name: "Update project membership", message: "Role not found" });
const project = await projectDAL.findById(customRole.projectId);
const plan = await licenseService.getPlan(project.orgId);
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) {
const plan = await licenseService.getPlan(actorOrgId as string);
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const [membership] = await projectMembershipDAL.update(
{ id: membershipId, projectId },
{
role: ProjectMembershipRole.Custom,
roleId: customRole.id
}
);
return membership;
}
const [membership] = await projectMembershipDAL.update({ id: membershipId, projectId }, { role, roleId: null });
return membership;
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const santiziedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: membershipId,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: membershipId,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
});
return updatedRoles;
};
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
@@ -481,7 +438,6 @@ export const projectMembershipServiceFactory = ({
return {
getProjectMemberships,
inviteUserToProject,
updateProjectMembership,
addUsersToProjectNonE2EE,
deleteProjectMemberships,
@@ -1,7 +1,9 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export enum ProjectUserMembershipTemporaryMode {
Relative = "relative"
}
export type TInviteUserToProjectDTO = {
emails: string[];
@@ -9,7 +11,19 @@ export type TInviteUserToProjectDTO = {
export type TUpdateProjectMembershipDTO = {
membershipId: string;
role: string;
roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
} & TProjectPermission;
export type TDeleteProjectMembershipOldDTO = {
@@ -27,7 +41,6 @@ export type TAddUsersToWorkspaceDTO = {
orgMembershipId: string;
workspaceEncryptedKey: string;
workspaceEncryptedNonce: string;
projectRole: ProjectMembershipRole;
}[];
} & TProjectPermission;
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectUserMembershipRoleDALFactory = ReturnType<typeof projectUserMembershipRoleDALFactory>;
export const projectUserMembershipRoleDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectUserMembershipRole);
return orm;
};
@@ -76,7 +76,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return deletedRole;
};
@@ -92,7 +92,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
name: "Admin",
slug: ProjectMembershipRole.Admin,
description: "Complete administration access over the project",
permissions: packRules(projectAdminPermissions.rules),
permissions: packRules(projectAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
@@ -102,7 +102,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
name: "Developer",
slug: ProjectMembershipRole.Member,
description: "Non-administrative role in an project",
permissions: packRules(projectMemberPermissions.rules),
permissions: packRules(projectMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
@@ -112,7 +112,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
description: "Non-administrative role in an project",
permissions: packRules(projectViewerPermission.rules),
permissions: packRules(projectViewerPermission),
createdAt: new Date(),
updatedAt: new Date()
},
@@ -122,7 +122,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
name: "No Access",
slug: "no-access",
description: "No access to any resources in the project",
permissions: packRules(projectNoAccessPermissions.rules),
permissions: packRules(projectNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
@@ -38,6 +38,7 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
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 { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@@ -58,9 +59,9 @@ type TProjectQueueFactoryDep = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
integrationAuthDAL: TIntegrationAuthDALFactory;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
@@ -81,7 +82,8 @@ export const projectQueueFactory = ({
orgDAL,
projectDAL,
orgService,
projectMembershipDAL
projectMembershipDAL,
projectUserMembershipRoleDAL
}: TProjectQueueFactoryDep) => {
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
@@ -227,7 +229,7 @@ export const projectQueueFactory = ({
);
// Create a membership for the ghost user
await projectMembershipDAL.create(
const projectMembership = await projectMembershipDAL.create(
{
projectId: project.id,
userId: ghostUser.user.id,
@@ -235,6 +237,10 @@ export const projectQueueFactory = ({
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
// If a bot already exists, delete it
if (existingBot) {
@@ -17,11 +17,13 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
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 { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
@@ -50,9 +52,11 @@ type TProjectServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
@@ -75,7 +79,9 @@ export const projectServiceFactory = ({
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
licenseService
licenseService,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -114,14 +120,18 @@ export const projectServiceFactory = ({
tx
);
// set ghost user as admin of project
await projectMembershipDAL.create(
const projectMembership = await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
role: ProjectMembershipRole.Admin,
projectId: project.id
projectId: project.id,
role: ProjectMembershipRole.Admin
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
// generate the blind index for project
await secretBlindIndexDAL.create(
@@ -213,7 +223,7 @@ export const projectServiceFactory = ({
});
// Create a membership for the user
await projectMembershipDAL.create(
const userProjectMembership = await projectMembershipDAL.create(
{
projectId: project.id,
userId: user.id,
@@ -221,6 +231,10 @@ export const projectServiceFactory = ({
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole },
tx
);
// Create a project key for the user
await projectKeyDAL.create(
@@ -266,7 +280,7 @@ export const projectServiceFactory = ({
});
const isCustomRole = Boolean(customRole);
await identityProjectDAL.create(
const identityProjectMembership = await identityProjectDAL.create(
{
identityId: actorId,
projectId: project.id,
@@ -275,6 +289,15 @@ export const projectServiceFactory = ({
},
tx
);
await identityProjectMembershipRoleDAL.create(
{
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
customRoleId: customRole?.id
},
tx
);
}
return {
@@ -350,11 +373,11 @@ export const projectServiceFactory = ({
};
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
const { permission, hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
if (membership?.role !== ProjectMembershipRole.Admin) {
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new ForbiddenRequestError({
message: "User must be admin"
});
@@ -37,8 +37,8 @@ export const secretBlindIndexServiceFactory = ({
};
const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => {
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
if (membership?.role !== ProjectMembershipRole.Admin) {
const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new UnauthorizedError({ message: "User must be admin" });
}
@@ -53,8 +53,8 @@ export const secretBlindIndexServiceFactory = ({
actorOrgId,
secretsToUpdate
}: TUpdateProjectSecretNameDTO) => {
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
if (membership?.role !== ProjectMembershipRole.Admin) {
const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new UnauthorizedError({ message: "User must be admin" });
}