fix: permissions not restricted for certain management pages / actions (#1219)
* fix: restrict parts of manage navigation to admins * fix: restrict stats cards on manage home page * fix: restrict access to amount of certain stats for manage home * fix: restrict visibility of board create button * fix: restrict access to integration pages * fix: restrict access to tools pages for admins * fix: restrict access to user and group pages * test: adjust tests to match permission changes for routes * fix: remove certain pages from spotlight without admin * fix: app management not restricted
This commit is contained in:
@@ -5,84 +5,92 @@ import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite";
|
||||
import { validation, z } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||
|
||||
export const groupRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||
const groupCount = await ctx.db
|
||||
.select({
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(groups)
|
||||
.where(whereQuery);
|
||||
getPaginated: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.paginated)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||
const groupCount = await ctx.db
|
||||
.select({
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(groups)
|
||||
.where(whereQuery);
|
||||
|
||||
const dbGroups = await ctx.db.query.groups.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
const dbGroups = await ctx.db.query.groups.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
limit: input.pageSize,
|
||||
offset: (input.page - 1) * input.pageSize,
|
||||
where: whereQuery,
|
||||
});
|
||||
limit: input.pageSize,
|
||||
offset: (input.page - 1) * input.pageSize,
|
||||
where: whereQuery,
|
||||
});
|
||||
|
||||
return {
|
||||
items: dbGroups.map((group) => ({
|
||||
return {
|
||||
items: dbGroups.map((group) => ({
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
})),
|
||||
totalCount: groupCount[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
getById: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.byId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
provider: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Group not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
})),
|
||||
totalCount: groupCount[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
getById: protectedProcedure.input(validation.common.byId).query(async ({ input, ctx }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
provider: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Group not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...group,
|
||||
members: group.members.map((member) => member.user),
|
||||
permissions: group.permissions.map((permission) => permission.permission),
|
||||
};
|
||||
}),
|
||||
permissions: group.permissions.map((permission) => permission.permission),
|
||||
};
|
||||
}),
|
||||
// Is protected because also used in board access / integration access forms
|
||||
selectable: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await ctx.db.query.groups.findMany({
|
||||
columns: {
|
||||
@@ -91,7 +99,8 @@ export const groupRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
search: publicProcedure
|
||||
search: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string(),
|
||||
@@ -108,85 +117,108 @@ export const groupRouter = createTRPCRouter({
|
||||
limit: input.limit,
|
||||
});
|
||||
}),
|
||||
createGroup: protectedProcedure.input(validation.group.create).mutation(async ({ input, ctx }) => {
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
||||
createGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.create)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName);
|
||||
|
||||
const id = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id,
|
||||
name: normalizedName,
|
||||
ownerId: ctx.session.user.id,
|
||||
});
|
||||
|
||||
return id;
|
||||
}),
|
||||
updateGroup: protectedProcedure.input(validation.group.update).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
const id = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id,
|
||||
name: normalizedName,
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePermissions: protectedProcedure.input(validation.group.savePermissions).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db.delete(groupPermissions).where(eq(groupPermissions.groupId, input.groupId));
|
||||
|
||||
await ctx.db.insert(groupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: input.groupId,
|
||||
permission,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
transferOwnership: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
ownerId: input.userId,
|
||||
})
|
||||
.where(eq(groups.id, input.groupId));
|
||||
}),
|
||||
deleteGroup: protectedProcedure.input(validation.common.byId).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
||||
}),
|
||||
addMember: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(groups.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
ownerId: ctx.session.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
});
|
||||
}),
|
||||
removeMember: protectedProcedure.input(validation.group.groupUser).mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
return id;
|
||||
}),
|
||||
updateGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.update)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
.where(and(eq(groupMembers.groupId, input.groupId), eq(groupMembers.userId, input.userId)));
|
||||
}),
|
||||
const normalizedName = normalizeName(input.name);
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
name: normalizedName,
|
||||
})
|
||||
.where(eq(groups.id, input.id));
|
||||
}),
|
||||
savePermissions: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db.delete(groupPermissions).where(eq(groupPermissions.groupId, input.groupId));
|
||||
|
||||
await ctx.db.insert(groupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: input.groupId,
|
||||
permission,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
transferOwnership: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
await ctx.db
|
||||
.update(groups)
|
||||
.set({
|
||||
ownerId: input.userId,
|
||||
})
|
||||
.where(eq(groups.id, input.groupId));
|
||||
}),
|
||||
deleteGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.byId)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
await ctx.db.delete(groups).where(eq(groups.id, input.id));
|
||||
}),
|
||||
addMember: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
throwIfCredentialsDisabled();
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(groups.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
});
|
||||
}),
|
||||
removeMember: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
throwIfCredentialsDisabled();
|
||||
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
.where(and(eq(groupMembers.groupId, input.groupId), eq(groupMembers.userId, input.userId)));
|
||||
}),
|
||||
});
|
||||
|
||||
const normalizeName = (name: string) => name.trim();
|
||||
|
||||
Reference in New Issue
Block a user