mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 14:26:25 +00:00
feat(groups): unique names
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// find any duplicate group names within organizations
|
||||||
|
const duplicates = await knex(TableName.Groups)
|
||||||
|
.select("orgId", "name")
|
||||||
|
.count("* as count")
|
||||||
|
.groupBy("orgId", "name")
|
||||||
|
.having(knex.raw("count(*) > 1"));
|
||||||
|
|
||||||
|
// for each set of duplicates, update all but one with a numbered suffix
|
||||||
|
for await (const duplicate of duplicates) {
|
||||||
|
const groups = await knex(TableName.Groups)
|
||||||
|
.select("id", "name")
|
||||||
|
.where({
|
||||||
|
orgId: duplicate.orgId,
|
||||||
|
name: duplicate.name
|
||||||
|
})
|
||||||
|
.orderBy("createdAt", "asc"); // keep original name for oldest group
|
||||||
|
|
||||||
|
// skip the first (oldest) group, rename others with numbered suffix
|
||||||
|
for (let i = 1; i < groups.length; i += 1) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.Groups)
|
||||||
|
.where("id", groups[i].id)
|
||||||
|
.update({
|
||||||
|
name: `${groups[i].name} (${i})`,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore TS doesn't know about Knex's timestamp types
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the unique constraint
|
||||||
|
await knex.schema.alterTable(TableName.Groups, (t) => {
|
||||||
|
t.unique(["orgId", "name"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// Remove the unique constraint
|
||||||
|
await knex.schema.alterTable(TableName.Groups, (t) => {
|
||||||
|
t.dropUnique(["orgId", "name"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -88,6 +88,13 @@ export const groupServiceFactory = ({
|
|||||||
if (!hasRequiredPriviledges)
|
if (!hasRequiredPriviledges)
|
||||||
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" });
|
||||||
|
|
||||||
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name });
|
||||||
|
if (existingGroup) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to create group with name '${name}'. Group with the same name already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const group = await groupDAL.create({
|
const group = await groupDAL.create({
|
||||||
name,
|
name,
|
||||||
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
|
||||||
@@ -145,6 +152,16 @@ export const groupServiceFactory = ({
|
|||||||
if (isCustomRole) customRole = customOrgRole;
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name });
|
||||||
|
|
||||||
|
if (existingGroup && existingGroup.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to update group with name '${name}'. Group with the same name already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [updatedGroup] = await groupDAL.update(
|
const [updatedGroup] = await groupDAL.update(
|
||||||
{
|
{
|
||||||
id: group.id
|
id: group.id
|
||||||
|
|||||||
@@ -790,6 +790,18 @@ export const scimServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newGroup = await groupDAL.transaction(async (tx) => {
|
const newGroup = await groupDAL.transaction(async (tx) => {
|
||||||
|
const conflictingGroup = await groupDAL.findOne({
|
||||||
|
name: displayName,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflictingGroup) {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: `Group with name '${displayName}' already exists in the organization`,
|
||||||
|
status: 409
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const group = await groupDAL.create(
|
const group = await groupDAL.create(
|
||||||
{
|
{
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { isDisposableEmail } from "./validate-email";
|
export { isDisposableEmail } from "./validate-email";
|
||||||
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
|
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
|
||||||
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
|
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
|
||||||
|
export { isUuidV4 } from "./validate-uuid";
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
|
||||||
@@ -16,7 +16,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
|
|||||||
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectId/groups/:groupId",
|
url: "/:projectId/groups/:groupIdOrName",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
@@ -30,7 +30,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
|
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
|
||||||
groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId)
|
groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId)
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
@@ -76,7 +76,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
roles: req.body.roles || [{ role: req.body.role }],
|
roles: req.body.roles || [{ role: req.body.role }],
|
||||||
projectId: req.params.projectId,
|
projectId: req.params.projectId,
|
||||||
groupId: req.params.groupId
|
groupIdOrName: req.params.groupIdOrName
|
||||||
});
|
});
|
||||||
|
|
||||||
return { groupMembership };
|
return { groupMembership };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
|
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
@@ -9,6 +9,7 @@ import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
|||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { isUuidV4 } from "@app/lib/validator";
|
||||||
|
|
||||||
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
@@ -62,7 +63,7 @@ export const groupProjectServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
roles,
|
roles,
|
||||||
projectId,
|
projectId,
|
||||||
groupId
|
groupIdOrName
|
||||||
}: TCreateProjectGroupDTO) => {
|
}: TCreateProjectGroupDTO) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
@@ -79,13 +80,23 @@ export const groupProjectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
|
const isUuid = isUuidV4(groupIdOrName);
|
||||||
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
|
|
||||||
|
let group: TGroups | null = null;
|
||||||
|
// id can only be a uuid, name can be anything
|
||||||
|
if (isUuid) {
|
||||||
|
group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName });
|
||||||
|
}
|
||||||
|
if (!group) {
|
||||||
|
group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` });
|
||||||
|
|
||||||
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
|
||||||
if (existingGroup)
|
if (existingGroup)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Group with ID ${groupId} already exists in project with id ${project.id}`
|
message: `Group with ID ${group.id} already exists in project with id ${project.id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const { role: requestedRoleChange } of roles) {
|
for await (const { role: requestedRoleChange } of roles) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
export type TCreateProjectGroupDTO = {
|
export type TCreateProjectGroupDTO = {
|
||||||
groupId: string;
|
groupIdOrName: string;
|
||||||
roles: (
|
roles: (
|
||||||
| {
|
| {
|
||||||
role: string;
|
role: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
import { isUuidV4 } from "@app/lib/validator";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
@@ -28,8 +28,6 @@ type TSecretSharingServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
|
|
||||||
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
|
|
||||||
|
|
||||||
export const secretSharingServiceFactory = ({
|
export const secretSharingServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
|
|||||||
Reference in New Issue
Block a user