From 36181f03550462cf684ffef9f2f16b1fcfd2ed3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 7 Nov 2025 11:47:27 +0100 Subject: [PATCH 01/14] Add role badge to the sidebar --- .../app/components/blocks/sidebar/farm.tsx | 30 +++++++++++++++++-- fdm-app/app/routes/farm.$b_id_farm.tsx | 5 ++++ fdm-app/app/routes/farm.tsx | 11 +++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/sidebar/farm.tsx b/fdm-app/app/components/blocks/sidebar/farm.tsx index c3fce2dec..e7991a04d 100644 --- a/fdm-app/app/components/blocks/sidebar/farm.tsx +++ b/fdm-app/app/components/blocks/sidebar/farm.tsx @@ -1,3 +1,4 @@ +import type { getFarm } from "@svenvw/fdm-core" import { Calendar, Check, @@ -29,7 +30,20 @@ import { SidebarMenuSubItem, } from "~/components/ui/sidebar" -export function SidebarFarm() { +export function SidebarFarm({ + farm, +}: { + farm: Awaited> | undefined +}) { + function getSuperiorRole(allRoles: ("owner" | "advisor" | "researcher")[]) { + if (allRoles.length > 0) { + const ordering: unknown[] = ["owner", "advisor", "researcher"] + allRoles.sort((a, b) => ordering.indexOf(a) - ordering.indexOf(b)) + return allRoles[0] + } + return null + } + const farmId = useFarmStore((state) => state.farmId) const selectedCalendar = useCalendarStore((state) => state.calendar) @@ -40,6 +54,7 @@ export function SidebarFarm() { // Check if page contains `farm/create` in url const location = useLocation() const isCreateFarmWizard = location.pathname.includes("farm/create") + const farmRole = farm ? getSuperiorRole(farm.roles) : null // Set the farm link let farmLink: string @@ -49,7 +64,7 @@ export function SidebarFarm() { farmLinkDisplay = "Terug naar bedrijven" } else if (farmId && farmId !== "undefined") { farmLink = `/farm/${farmId}` - farmLinkDisplay = "Bedrijf" + farmLinkDisplay = farm?.b_name_farm ? farm.b_name_farm : "Bedrijf" } else { farmLink = "/farm" farmLinkDisplay = "Overzicht bedrijven" @@ -81,6 +96,17 @@ export function SidebarFarm() { {farmLinkDisplay} + {farmRole && ( + + {farmRole === "owner" + ? "Eigenaar" + : farmRole === "advisor" + ? "Adviseur" + : farmRole === "researcher" + ? "Onderzoeker" + : "Onbekend"} + + )} diff --git a/fdm-app/app/routes/farm.$b_id_farm.tsx b/fdm-app/app/routes/farm.$b_id_farm.tsx index e5758d7b6..b09df2899 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.tsx @@ -1,7 +1,9 @@ +import { getFarm } from "@svenvw/fdm-core" import { data, type LoaderFunctionArgs, type MetaFunction } from "react-router" import { getSession } from "~/lib/auth.server" import { clientConfig } from "~/lib/config" import { handleActionError } from "~/lib/error" +import { fdm } from "~/lib/fdm.server" // Meta export const meta: MetaFunction = () => { @@ -39,9 +41,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get the session const session = await getSession(request) + const farm = await getFarm(fdm, session.principal_id, b_id_farm) + // Return the farm ID and session info return { farmId: b_id_farm, + farm: farm, session, } } catch (error) { diff --git a/fdm-app/app/routes/farm.tsx b/fdm-app/app/routes/farm.tsx index a91909683..4ae81732e 100644 --- a/fdm-app/app/routes/farm.tsx +++ b/fdm-app/app/routes/farm.tsx @@ -1,3 +1,4 @@ +import { getFarm } from "@svenvw/fdm-core" import posthog from "posthog-js" import { useEffect } from "react" import type { LoaderFunctionArgs, MetaFunction } from "react-router" @@ -19,6 +20,7 @@ import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { useCalendarStore } from "~/store/calendar" import { useFarmStore } from "~/store/farm" +import { fdm } from "../lib/fdm.server" export const meta: MetaFunction = () => { return [ @@ -42,7 +44,7 @@ export const meta: MetaFunction = () => { * * @throws {Error} If an error occurs during session retrieval, processed by handleLoaderError. */ -export async function loader({ request }: LoaderFunctionArgs) { +export async function loader({ params, request }: LoaderFunctionArgs) { try { // Get the session const session = await getSession(request) @@ -52,8 +54,13 @@ export async function loader({ request }: LoaderFunctionArgs) { return sessionCheckResponse } + const farm = params.b_id_farm + ? await getFarm(fdm, session.principal_id, params.b_id_farm) + : undefined + // Return user information from loader return { + farm: farm, user: session.user, userName: session.userName, initials: session.initials, @@ -116,7 +123,7 @@ export default function App() { - + Date: Fri, 7 Nov 2025 13:38:43 +0100 Subject: [PATCH 02/14] Include organization membership in role check --- fdm-core/src/authorization.ts | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index a6c4e5142..7d61e4689 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -1,4 +1,4 @@ -import { and, eq, inArray, isNull } from "drizzle-orm" +import { and, eq, inArray, isNull, or } from "drizzle-orm" import type { Action, Permission, @@ -10,6 +10,7 @@ import type { Role, } from "./authorization.d" import * as schema from "./db/schema" +import * as authNSchema from "./db/schema-authn" import * as authZSchema from "./db/schema-authz" import { handleError } from "./error" import type { FdmType } from "./fdm" @@ -182,13 +183,26 @@ export async function checkPermission( resource_id: authZSchema.role.resource_id, }) .from(authZSchema.role) + .leftJoin( + authNSchema.member, + eq( + authZSchema.role.principal_id, + authNSchema.member.organizationId, + ), + ) .where( and( eq(authZSchema.role.resource, bead.resource), eq(authZSchema.role.resource_id, bead.resource_id), - inArray( - authZSchema.role.principal_id, - principal_ids, + or( + inArray( + authZSchema.role.principal_id, + principal_ids, + ), + inArray( + authNSchema.member.userId, + principal_ids, + ), ), inArray(authZSchema.role.role, roles), isNull(authZSchema.role.deleted), @@ -277,11 +291,24 @@ export async function getRolesOfPrincipalForResource( role: authZSchema.role.role, }) .from(authZSchema.role) + .leftJoin( + authNSchema.member, + eq( + authZSchema.role.principal_id, + authNSchema.member.organizationId, + ), + ) .where( and( eq(authZSchema.role.resource, resource), eq(authZSchema.role.resource_id, resource_id), - inArray(authZSchema.role.principal_id, principal_ids), + or( + inArray( + authZSchema.role.principal_id, + principal_ids, + ), + inArray(authNSchema.member.userId, principal_ids), + ), isNull(authZSchema.role.deleted), ), ) @@ -538,10 +565,23 @@ export async function listResources( resource_id: authZSchema.role.resource_id, }) .from(authZSchema.role) + .leftJoin( + authNSchema.member, + eq( + authZSchema.role.principal_id, + authNSchema.member.organizationId, + ), + ) .where( and( eq(authZSchema.role.resource, resource), - inArray(authZSchema.role.principal_id, principal_ids), + or( + inArray( + authZSchema.role.principal_id, + principal_ids, + ), + inArray(authNSchema.member.userId, principal_ids), + ), inArray(authZSchema.role.role, roles), isNull(authZSchema.role.deleted), ), From 481960bbaa228232c5680773a9964ea2c7cf5fdb Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:18:21 +0100 Subject: [PATCH 03/14] refactor: align role badge to the right --- fdm-app/app/components/blocks/sidebar/farm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/sidebar/farm.tsx b/fdm-app/app/components/blocks/sidebar/farm.tsx index f4ca971c2..bc14e6ef5 100644 --- a/fdm-app/app/components/blocks/sidebar/farm.tsx +++ b/fdm-app/app/components/blocks/sidebar/farm.tsx @@ -117,7 +117,7 @@ export function SidebarFarm({ {farmLinkDisplay} {farmRole && ( - + {farmRole === "owner" ? "Eigenaar" : farmRole === "advisor" From dcf9dd9b6e3a1728b23bfbb4dcbd630132a9983b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 10:56:29 +0100 Subject: [PATCH 04/14] Add tests to fdm-core --- fdm-core/src/authorization.test.ts | 180 ++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index e095b9ad0..288a8f869 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -1,5 +1,6 @@ import { and, desc, eq, isNotNull, isNull } from "drizzle-orm" import { beforeAll, beforeEach, describe, expect, inject, it } from "vitest" +import { type BetterAuth, createFdmAuth } from "./authentication" import { actions, checkPermission, @@ -17,11 +18,19 @@ import { addFarm } from "./farm" import { createFdmServer } from "./fdm-server" import type { FdmServerType } from "./fdm-server.d" import { createId } from "./id" +import { + acceptInvitation, + createOrganization, + inviteUserToOrganization, +} from "./organization" describe("Authorization Functions", () => { let fdm: FdmServerType + let fdmAuth: BetterAuth let principal_id: string let farm_id: string + let organization_id: string + let organization_member_id: string let host: string let port: number let user: string @@ -34,8 +43,42 @@ describe("Authorization Functions", () => { user = inject("user") password = inject("password") database = inject("database") + // Mock environment variables + const googleAuth = { + clientId: "mock_google_client_id", + clientSecret: "mock_google_client_secret", + } + const microsoftAuth = { + clientId: "mock_ms_client_id", + clientSecret: "mock_ms_client_secret", + } + fdm = createFdmServer(host, port, user, password, database, 10) // allow some connections - principal_id = createId() + fdmAuth = createFdmAuth(fdm, googleAuth, microsoftAuth, undefined, true) + const principal = await fdmAuth.api.signUpEmail({ + headers: undefined, + body: { + email: "principal@example.com", + name: "Principal I", + firstname: "Principal", + surname: "I", + username: "principal", + password: "password", + }, + }) + principal_id = principal.user.id + const organization_member = await fdmAuth.api.signUpEmail({ + headers: undefined, + body: { + email: "organization-member@example.com", + name: "Organization Member", + firstname: "Organization", + surname: "Member", + username: "organizationmember", + password: "password", + }, + }) + organization_member_id = organization_member.user.id }) beforeEach(async () => { @@ -53,6 +96,13 @@ describe("Authorization Functions", () => { farmPostalCode, principal_id, ) + organization_id = await createOrganization( + fdm, + principal_id, + "Test Organization", + `test-${createId()}`, + "This is an organization created for testing purposes.", + ) }) describe("checkPermission", () => { @@ -83,6 +133,65 @@ describe("Authorization Functions", () => { ) }) + it("should grant access through the organization", async () => { + await grantRole(fdm, "farm", "owner", farm_id, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + "organization-member@example.com", + "owner", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + await checkPermission( + fdm, + "farm", + "write", + farm_id, + principal_id, + "test", + ) + }) + + it("should not grant access through an organization not invited to a farm", async () => { + await expect( + checkPermission( + fdm, + "farm", + "write", + farm_id, + organization_id, + "test", + ), + ).rejects.toThrow( + "Principal does not have permission to perform this action", + ) + }) + + it("should not grant permissions higher than the organization permissions", async () => { + await grantRole(fdm, "farm", "researcher", farm_id, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + "organization-member@example.com", + "owner", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + await expect( + checkPermission( + fdm, + "farm", + "write", + farm_id, + organization_id, + "test", + ), + ).rejects.toThrow( + "Principal does not have permission to perform this action", + ) + }) + it("should throw an error for unknown resource", async () => { await grantRole(fdm, "farm", "owner", farm_id, principal_id) await expect( @@ -443,6 +552,30 @@ describe("Authorization Functions", () => { expect(accessibleResources).toContain(farm_id2) }) + it("should list resources that the user's organization has access to", async () => { + const farm_id2 = createId() + await grantRole(fdm, "farm", "owner", farm_id, organization_id) + await grantRole(fdm, "farm", "advisor", farm_id2, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + "organization-member@example.com", + "admin", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + + const accessibleResources = await listResources( + fdm, + "farm", + "read", + organization_member_id, + ) + expect(accessibleResources.length).toBe(2) + expect(accessibleResources).toContain(farm_id) + expect(accessibleResources).toContain(farm_id2) + }) + it("should handle empty list", async () => { const principal_id_new = createId() const accessibleResources = await listResources( @@ -573,6 +706,51 @@ describe("Authorization Functions", () => { expect(roles).toEqual([]) }) + it("should get roles derived from an organization", async () => { + await grantRole(fdm, "farm", "researcher", farm_id, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + "organization-member@example.com", + "admin", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + const roles = await getRolesOfPrincipalForResource( + fdm, + "farm", + farm_id, + organization_member_id, + ) + expect(roles).toEqual(["researcher"]) + }) + + it("should get all roles", async () => { + await grantRole( + fdm, + "farm", + "researcher", + farm_id, + organization_member_id, + ) + await grantRole(fdm, "farm", "owner", farm_id, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + "organization-member@example.com", + "admin", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + const roles = await getRolesOfPrincipalForResource( + fdm, + "farm", + farm_id, + organization_member_id, + ) + expect(roles).toEqual(["owner", "researcher"]) + }) + it("should throw error with invalid resource", async () => { await expect( getRolesOfPrincipalForResource( From 5c72a1846f2a51f5a40b3b4200a7898236ba0745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 11:44:27 +0100 Subject: [PATCH 05/14] Make organization member unique to each test --- fdm-core/src/authorization.test.ts | 38 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 288a8f869..98d390a12 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -30,6 +30,7 @@ describe("Authorization Functions", () => { let principal_id: string let farm_id: string let organization_id: string + let organization_member_email: string let organization_member_id: string let host: string let port: number @@ -67,18 +68,6 @@ describe("Authorization Functions", () => { }, }) principal_id = principal.user.id - const organization_member = await fdmAuth.api.signUpEmail({ - headers: undefined, - body: { - email: "organization-member@example.com", - name: "Organization Member", - firstname: "Organization", - surname: "Member", - username: "organizationmember", - password: "password", - }, - }) - organization_member_id = organization_member.user.id }) beforeEach(async () => { @@ -103,6 +92,21 @@ describe("Authorization Functions", () => { `test-${createId()}`, "This is an organization created for testing purposes.", ) + const organization_member_username = `orgmember${createId(8).toLowerCase()}` + organization_member_email = `${organization_member_username}@example.com` + const organization_member = await fdmAuth.api.signUpEmail({ + headers: undefined, + body: { + email: organization_member_email, + name: "Organization Member", + firstname: "Organization", + surname: "Member", + username: organization_member_username, + password: "password", + }, + }) + organization_member_id = organization_member.user.id + console.log(organization_member_email) }) describe("checkPermission", () => { @@ -138,7 +142,7 @@ describe("Authorization Functions", () => { const invitation_id = await inviteUserToOrganization( fdm, principal_id, - "organization-member@example.com", + organization_member_email, "owner", organization_id, ) @@ -173,7 +177,7 @@ describe("Authorization Functions", () => { const invitation_id = await inviteUserToOrganization( fdm, principal_id, - "organization-member@example.com", + organization_member_email, "owner", organization_id, ) @@ -559,7 +563,7 @@ describe("Authorization Functions", () => { const invitation_id = await inviteUserToOrganization( fdm, principal_id, - "organization-member@example.com", + organization_member_email, "admin", organization_id, ) @@ -711,7 +715,7 @@ describe("Authorization Functions", () => { const invitation_id = await inviteUserToOrganization( fdm, principal_id, - "organization-member@example.com", + organization_member_email, "admin", organization_id, ) @@ -737,7 +741,7 @@ describe("Authorization Functions", () => { const invitation_id = await inviteUserToOrganization( fdm, principal_id, - "organization-member@example.com", + organization_member_email, "admin", organization_id, ) From d63c50277892b98a6d5bc86bbcfc03ae3a397689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 11:46:22 +0100 Subject: [PATCH 06/14] Remove console.log --- fdm-core/src/authorization.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 98d390a12..0f92d3fdd 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -106,7 +106,6 @@ describe("Authorization Functions", () => { }, }) organization_member_id = organization_member.user.id - console.log(organization_member_email) }) describe("checkPermission", () => { From 2dbff5e4271490c279600f6ec719af96ab404f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:02:16 +0100 Subject: [PATCH 07/14] Dedupe listed resource ids --- fdm-core/src/authorization.test.ts | 26 ++++++++++++++++++++++++++ fdm-core/src/authorization.ts | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 0f92d3fdd..555661005 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -579,6 +579,32 @@ describe("Authorization Functions", () => { expect(accessibleResources).toContain(farm_id2) }) + it("should not list duplicates", async () => { + await grantRole( + fdm, + "farm", + "owner", + farm_id, + organization_member_id, + ) + await grantRole(fdm, "farm", "advisor", farm_id, organization_id) + const invitation_id = await inviteUserToOrganization( + fdm, + principal_id, + organization_member_email, + "admin", + organization_id, + ) + await acceptInvitation(fdm, invitation_id, organization_member_id) + const accessibleResources = await listResources( + fdm, + "farm", + "read", + organization_member_id, + ) + expect(accessibleResources).toEqual([farm_id]) + }) + it("should handle empty list", async () => { const principal_id_new = createId() const accessibleResources = await listResources( diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index 7d61e4689..b0f4e72d4 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -561,7 +561,7 @@ export async function listResources( } return await tx - .select({ + .selectDistinct({ resource_id: authZSchema.role.resource_id, }) .from(authZSchema.role) From 6f51ad5bae47440f66f4519525afe93239b5a711 Mon Sep 17 00:00:00 2001 From: Sven Verweij <37927107+SvenVw@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:03:19 +0100 Subject: [PATCH 08/14] chore: add changesets --- .changeset/hungry-shirts-scream.md | 5 +++++ .changeset/new-places-shop.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/hungry-shirts-scream.md create mode 100644 .changeset/new-places-shop.md diff --git a/.changeset/hungry-shirts-scream.md b/.changeset/hungry-shirts-scream.md new file mode 100644 index 000000000..cbbfe4223 --- /dev/null +++ b/.changeset/hungry-shirts-scream.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-core": patch +--- + +Fixes that members of an organization that has a role on a farm inherit that role diff --git a/.changeset/new-places-shop.md b/.changeset/new-places-shop.md new file mode 100644 index 000000000..e80f269bb --- /dev/null +++ b/.changeset/new-places-shop.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +In the sidebar when a farm is selected show the farm name and role the users has From 16540ae68aad05e79857995f118434977ca11575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:13:22 +0100 Subject: [PATCH 09/14] Update JSDoc for getRolesOfPrincipalForResource --- fdm-core/src/authorization.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index b0f4e72d4..3ab4cb9f7 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -258,12 +258,16 @@ export async function checkPermission( * * This function queries the database to find all roles that a principal has been granted for the given resource. * It returns an array of role strings. - * CAUTION: This function does not return inherited roles yet. + * + * When the principal id is for an user, it can get any of the user's roles derived through their organizations. + * + * CAUTION: This function does not return roles inherited from related resources yet. * * @param fdm - The FDM instance providing the connection to the database. * @param resource - The type of the resource to query for the principal's roles. * @param resource_id - The identifier of the specific resource instance. * @param principal_id - The identifier of the principal. + * If an user id is supplied, the function can also retrieve roles for the user's organizations. * @returns A promise that resolves to an array of roles (strings) that the principal has for the given resource. * Returns an empty array if the principal has no roles for the resource. * @throws {Error} If the resource type is invalid or if the database operation fails. From 66865d757e02eebcd737cd5669d15b39eec8184e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:16:11 +0100 Subject: [PATCH 10/14] Fix tests --- fdm-core/src/authorization.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/authorization.test.ts b/fdm-core/src/authorization.test.ts index 555661005..5a12bfbe9 100644 --- a/fdm-core/src/authorization.test.ts +++ b/fdm-core/src/authorization.test.ts @@ -151,7 +151,7 @@ describe("Authorization Functions", () => { "farm", "write", farm_id, - principal_id, + organization_member_id, "test", ) }) @@ -187,7 +187,7 @@ describe("Authorization Functions", () => { "farm", "write", farm_id, - organization_id, + organization_member_id, "test", ), ).rejects.toThrow( From 5366963b036c59f4d3609d75940755e0c8dbb9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:17:19 +0100 Subject: [PATCH 11/14] Nitpicks --- fdm-app/app/components/blocks/sidebar/farm.tsx | 12 +++++++++--- fdm-app/app/routes/farm.tsx | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/sidebar/farm.tsx b/fdm-app/app/components/blocks/sidebar/farm.tsx index bc14e6ef5..bef67edc9 100644 --- a/fdm-app/app/components/blocks/sidebar/farm.tsx +++ b/fdm-app/app/components/blocks/sidebar/farm.tsx @@ -39,8 +39,10 @@ export function SidebarFarm({ function getSuperiorRole(allRoles: ("owner" | "advisor" | "researcher")[]) { if (allRoles.length > 0) { const ordering: unknown[] = ["owner", "advisor", "researcher"] - allRoles.sort((a, b) => ordering.indexOf(a) - ordering.indexOf(b)) - return allRoles[0] + const sorted = [...allRoles].sort( + (a, b) => ordering.indexOf(a) - ordering.indexOf(b), + ) + return sorted[0] } return null } @@ -117,7 +119,11 @@ export function SidebarFarm({ {farmLinkDisplay} {farmRole && ( - + {farmRole === "owner" ? "Eigenaar" : farmRole === "advisor" diff --git a/fdm-app/app/routes/farm.tsx b/fdm-app/app/routes/farm.tsx index 4ae81732e..904ab5319 100644 --- a/fdm-app/app/routes/farm.tsx +++ b/fdm-app/app/routes/farm.tsx @@ -35,12 +35,13 @@ export const meta: MetaFunction = () => { /** * Retrieves the session from the HTTP request and returns user information if available. + * Also retrieves the current farm when available. * * If the session does not contain a user, the function redirects to the "/signin" route. * Any errors encountered during session retrieval are processed by the designated error handler. * * @param request - The HTTP request used for obtaining session data. - * @returns An object with a "user" property when a valid session is found. + * @returns An object with a "user" property when a valid session is found, and a "farm" property when b_id_farm is found in the URL. * * @throws {Error} If an error occurs during session retrieval, processed by handleLoaderError. */ From 53a384975256790dc3f07192f5f6a605dbf7278f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:31:18 +0100 Subject: [PATCH 12/14] Guard for null resulting from left join before using inArray --- fdm-core/src/authorization.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/fdm-core/src/authorization.ts b/fdm-core/src/authorization.ts index 3ab4cb9f7..2d72d8f70 100644 --- a/fdm-core/src/authorization.ts +++ b/fdm-core/src/authorization.ts @@ -1,4 +1,4 @@ -import { and, eq, inArray, isNull, or } from "drizzle-orm" +import { and, eq, inArray, isNotNull, isNull, or } from "drizzle-orm" import type { Action, Permission, @@ -199,9 +199,12 @@ export async function checkPermission( authZSchema.role.principal_id, principal_ids, ), - inArray( - authNSchema.member.userId, - principal_ids, + and( + isNotNull(authNSchema.member.userId), + inArray( + authNSchema.member.userId, + principal_ids, + ), ), ), inArray(authZSchema.role.role, roles), @@ -311,7 +314,13 @@ export async function getRolesOfPrincipalForResource( authZSchema.role.principal_id, principal_ids, ), - inArray(authNSchema.member.userId, principal_ids), + and( + isNotNull(authNSchema.member.userId), + inArray( + authNSchema.member.userId, + principal_ids, + ), + ), ), isNull(authZSchema.role.deleted), ), @@ -584,7 +593,13 @@ export async function listResources( authZSchema.role.principal_id, principal_ids, ), - inArray(authNSchema.member.userId, principal_ids), + and( + isNotNull(authNSchema.member.userId), + inArray( + authNSchema.member.userId, + principal_ids, + ), + ), ), inArray(authZSchema.role.role, roles), isNull(authZSchema.role.deleted), From 44770594e666d2bab71df6efc41a0af8cc02c840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:34:15 +0100 Subject: [PATCH 13/14] Make getSuperiorRole more type-safe --- fdm-app/app/components/blocks/sidebar/farm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/sidebar/farm.tsx b/fdm-app/app/components/blocks/sidebar/farm.tsx index bef67edc9..a9c815afd 100644 --- a/fdm-app/app/components/blocks/sidebar/farm.tsx +++ b/fdm-app/app/components/blocks/sidebar/farm.tsx @@ -38,7 +38,7 @@ export function SidebarFarm({ }) { function getSuperiorRole(allRoles: ("owner" | "advisor" | "researcher")[]) { if (allRoles.length > 0) { - const ordering: unknown[] = ["owner", "advisor", "researcher"] + const ordering = ["owner", "advisor", "researcher"] as const const sorted = [...allRoles].sort( (a, b) => ordering.indexOf(a) - ordering.indexOf(b), ) From 4aa2deb6e5f6862164324905c70f5a5053e1d094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 26 Nov 2025 12:36:23 +0100 Subject: [PATCH 14/14] Update changeset --- .changeset/hungry-shirts-scream.md | 2 +- .changeset/new-places-shop.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/hungry-shirts-scream.md b/.changeset/hungry-shirts-scream.md index cbbfe4223..2a1029685 100644 --- a/.changeset/hungry-shirts-scream.md +++ b/.changeset/hungry-shirts-scream.md @@ -2,4 +2,4 @@ "@svenvw/fdm-core": patch --- -Fixes that members of an organization that has a role on a farm inherit that role +Members of an organization now inherit its roles on a farm properly. diff --git a/.changeset/new-places-shop.md b/.changeset/new-places-shop.md index e80f269bb..b6c630774 100644 --- a/.changeset/new-places-shop.md +++ b/.changeset/new-places-shop.md @@ -2,4 +2,4 @@ "@svenvw/fdm-app": minor --- -In the sidebar when a farm is selected show the farm name and role the users has +In the sidebar, when a farm is selected, show the farm name and role that the user has.