Skip to content

Commit 18d0675

Browse files
authored
use isOwner as part of SpacesPolicy.isMember (#1139)
* use isOwner as part of SpacesPolicy.isMember * deny SpacesPolicy.isOwner on 404
1 parent c9be241 commit 18d0675

File tree

3 files changed

+50
-9
lines changed

3 files changed

+50
-9
lines changed

packages/web-backend/src/Spaces/SpacesPolicy.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,27 @@ export class SpacesPolicy extends Effect.Service<SpacesPolicy>()(
1111
effect: Effect.gen(function* () {
1212
const repo = yield* SpacesRepo;
1313

14-
const isMember = (spaceId: string) =>
14+
const hasMembership = (spaceId: string) =>
1515
Policy.policy(
1616
Effect.fn(function* (user) {
1717
return Option.isSome(yield* repo.membership(user.id, spaceId));
1818
}),
1919
);
2020

21-
return { isMember };
21+
const isOwner = (spaceId: string) =>
22+
Policy.policy(
23+
Effect.fn(function* (user) {
24+
const space = yield* repo.getById(spaceId);
25+
if (Option.isNone(space)) return false;
26+
27+
return space.value.createdById === user.id;
28+
}),
29+
);
30+
31+
const isMember = (spaceId: string) =>
32+
Policy.any(isOwner(spaceId), hasMembership(spaceId));
33+
34+
return { isMember, isOwner };
2235
}),
2336
dependencies: [
2437
OrganisationsRepo.Default,

packages/web-backend/src/Spaces/SpacesRepo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export class SpacesRepo extends Effect.Service<SpacesRepo>()("SpacesRepo", {
4545
),
4646
)
4747
.pipe(Effect.map(Array.get(0))),
48+
getById: (spaceId: string) =>
49+
db
50+
.execute((db) =>
51+
db.select().from(Db.spaces).where(Dz.eq(Db.spaces.id, spaceId)),
52+
)
53+
.pipe(Effect.map(Array.get(0))),
4854
};
4955
}),
5056
dependencies: [Database.Default],

packages/web-domain/src/Policy.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
// shoutout https://lucas-barake.github.io/building-a-composable-policy-system/
22

33
import { type Brand, Context, Data, Effect, type Option, Schema } from "effect";
4-
4+
import type { NonEmptyReadonlyArray } from "effect/Array";
55
import { CurrentUser } from "./Authentication.ts";
66

7-
export type Policy<E = never, R = never> = Brand.Branded<
8-
Effect.Effect<void, PolicyDeniedError | E, CurrentUser | R>,
9-
"Private"
7+
export type Policy<E = never, R = never> = Effect.Effect<
8+
void,
9+
PolicyDeniedError | E,
10+
CurrentUser | R
1011
>;
1112

12-
export type PublicPolicy<E = never, R = never> = Brand.Branded<
13-
Effect.Effect<void, PolicyDeniedError | E, R>,
14-
"Public"
13+
export type PublicPolicy<E = never, R = never> = Effect.Effect<
14+
void,
15+
PolicyDeniedError | E,
16+
R
1517
>;
1618

1719
export class PolicyDeniedError extends Schema.TaggedError<PolicyDeniedError>()(
@@ -73,3 +75,23 @@ export const withPublicPolicy =
7375
<E, R>(policy: PublicPolicy<E, R>) =>
7476
<A, E2, R2>(self: Effect.Effect<A, E2, R2>) =>
7577
Effect.zipRight(policy, self);
78+
79+
/**
80+
* Composes multiple policies with AND semantics - all policies must pass.
81+
* Returns a new policy that succeeds only if all the given policies succeed.
82+
*/
83+
export const all = <E, R>(
84+
...policies: NonEmptyReadonlyArray<Policy<E, R>>
85+
): Policy<E, R> =>
86+
Effect.all(policies, {
87+
concurrency: 1,
88+
discard: true,
89+
});
90+
91+
/**
92+
* Composes multiple policies with OR semantics - at least one policy must pass.
93+
* Returns a new policy that succeeds if any of the given policies succeed.
94+
*/
95+
export const any = <E, R>(
96+
...policies: NonEmptyReadonlyArray<Policy<E, R>>
97+
): Policy<E, R> => Effect.firstSuccessOf(policies);

0 commit comments

Comments
 (0)