feat: add local group support for OIDC users (#5952)
This commit is contained in:
1 parent
4692e7ce3a
commit
fb18b975b2
15 files changed
+540
-191
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { canManageGroupMembersLocally, isGroupMembershipManagedLocally } from "@homarr/auth/server";
|
||||
import { createId } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, handleTransactionsAsync, like, not } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from "@homarr/validation/group";
|
||||
|
||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||
|
||||
export const groupRouter = createTRPCRouter({
|
||||
@@ -315,10 +315,10 @@ export const groupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||
throwIfCredentialsDisabled();
|
||||
throwIfGroupMembersCannotBeManagedLocally();
|
||||
|
||||
const user = await ctx.db.query.users.findFirst({
|
||||
where: eq(groups.id, input.userId),
|
||||
where: eq(users.id, input.userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -328,6 +328,13 @@ export const groupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (!isGroupMembershipManagedLocally(user.provider)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User's provider is not managed locally",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
@@ -339,7 +346,7 @@ export const groupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||
throwIfCredentialsDisabled();
|
||||
throwIfGroupMembersCannotBeManagedLocally();
|
||||
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
@@ -371,6 +378,15 @@ const throwIfGroupNameIsReservedAsync = async (db: Database, id: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const throwIfGroupMembersCannotBeManagedLocally = () => {
|
||||
if (!canManageGroupMembersLocally()) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Group members cannot be managed locally",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const throwIfGroupNotFoundAsync = async (db: Database, id: string) => {
|
||||
const group = await db.query.groups.findFirst({
|
||||
where: eq(groups.id, id),
|
||||
|
||||
@@ -748,7 +748,45 @@ describe("addMember should add member to group", () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Credentials provider is disabled");
|
||||
await expect(actAsync()).rejects.toThrow("Group members cannot be managed locally");
|
||||
});
|
||||
|
||||
test("with a user from an externally managed provider it should throw FORBIDDEN error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(env, "env", "get");
|
||||
spy.mockReturnValue({ AUTH_PROVIDERS: ["credentials", "ldap"] } as never);
|
||||
const caller = groupRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
|
||||
const groupId = createId();
|
||||
const userId = createId();
|
||||
await db.insert(users).values([
|
||||
{
|
||||
id: userId,
|
||||
name: "User",
|
||||
provider: "ldap",
|
||||
},
|
||||
{
|
||||
id: defaultOwnerId,
|
||||
name: "Creator",
|
||||
},
|
||||
]);
|
||||
await db.insert(groups).values({
|
||||
id: groupId,
|
||||
name: "Group",
|
||||
ownerId: defaultOwnerId,
|
||||
position: 1,
|
||||
});
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.addMember({
|
||||
groupId,
|
||||
userId,
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("User's provider is not managed locally");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -872,7 +910,7 @@ describe("removeMember should remove member from group", () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Credentials provider is disabled");
|
||||
await expect(actAsync()).rejects.toThrow("Group members cannot be managed locally");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import { comparePasswordsAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, handleTransactionsAsync, like } from "@homarr/db";
|
||||
import { and, eq, handleTransactionsAsync, inArray, like } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { boards, groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema";
|
||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
import { credentialsAdminGroup, supportedAuthProviders } from "@homarr/definitions";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||
import {
|
||||
@@ -257,7 +257,7 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
// Is protected because also used in board access / integration access forms
|
||||
selectable: protectedProcedure
|
||||
.input(z.object({ excludeExternalProviders: z.boolean().default(false) }).optional())
|
||||
.input(z.object({ providers: z.array(z.enum(supportedAuthProviders)).optional() }).optional())
|
||||
.output(
|
||||
z.array(
|
||||
selectUserSchema.pick({
|
||||
@@ -284,7 +284,7 @@ export const userRouter = createTRPCRouter({
|
||||
image: true,
|
||||
email: true,
|
||||
},
|
||||
where: input?.excludeExternalProviders ? eq(users.provider, "credentials") : undefined,
|
||||
where: input?.providers ? inArray(users.provider, input.providers) : undefined,
|
||||
});
|
||||
}),
|
||||
search: permissionRequiredProcedure
|
||||
|
||||
Reference in new issue
Block a user