refactor(core): clarify permission replies
This commit is contained in:
parent
7456598cde
commit
f4bd181656
2 changed files with 106 additions and 52 deletions
|
|
@ -229,70 +229,76 @@ const layer = Layer.effect(
|
|||
),
|
||||
)
|
||||
|
||||
const publishReply = (item: Pending, reply: Reply) =>
|
||||
events.publish(Event.Replied, {
|
||||
sessionID: item.request.sessionID,
|
||||
requestID: item.request.id,
|
||||
reply,
|
||||
})
|
||||
|
||||
const settle = Effect.fnUntraced(function* (item: Pending, error?: DeclinedError | CorrectedError) {
|
||||
yield* error ? Deferred.fail(item.deferred, error) : Deferred.succeed(item.deferred, undefined)
|
||||
pending.delete(item.request.id)
|
||||
})
|
||||
|
||||
const rejectSession = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) {
|
||||
for (const item of pending.values()) {
|
||||
if (item.request.sessionID !== sessionID) continue
|
||||
yield* publishReply(item, "reject")
|
||||
yield* settle(item, new DeclinedError())
|
||||
}
|
||||
})
|
||||
|
||||
const approveSavedMatches = Effect.fnUntraced(function* () {
|
||||
const rememberedRules = yield* savedRules()
|
||||
for (const item of pending.values()) {
|
||||
const request = item.request
|
||||
const rules = yield* configured(request.sessionID, item.agent).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
|
||||
)
|
||||
if (!rules) continue
|
||||
if (denied(request, rules)) continue
|
||||
const effective = [...rules, ...rememberedRules]
|
||||
if (!request.resources.every((resource) => evaluate(request.action, resource, effective).effect === "allow"))
|
||||
continue
|
||||
yield* publishReply(item, "always")
|
||||
yield* settle(item)
|
||||
}
|
||||
})
|
||||
|
||||
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||
yield* events.publish(Event.Replied, {
|
||||
sessionID: existing.request.sessionID,
|
||||
requestID: existing.request.id,
|
||||
reply: input.reply,
|
||||
})
|
||||
yield* publishReply(existing, input.reply)
|
||||
|
||||
if (input.reply === "reject") {
|
||||
yield* Deferred.fail(
|
||||
existing.deferred,
|
||||
yield* settle(
|
||||
existing,
|
||||
input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(),
|
||||
)
|
||||
pending.delete(input.requestID)
|
||||
for (const [id, item] of pending) {
|
||||
if (item.request.sessionID !== existing.request.sessionID) continue
|
||||
yield* events.publish(Event.Replied, {
|
||||
sessionID: item.request.sessionID,
|
||||
requestID: item.request.id,
|
||||
reply: "reject",
|
||||
})
|
||||
yield* Deferred.fail(item.deferred, new DeclinedError())
|
||||
pending.delete(id)
|
||||
}
|
||||
yield* rejectSession(existing.request.sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
if (input.reply === "always" && existing.request.save?.length) {
|
||||
yield* saved.add({
|
||||
projectID: location.project.id,
|
||||
action: existing.request.action,
|
||||
resources: existing.request.save,
|
||||
})
|
||||
if (input.reply === "once") {
|
||||
yield* settle(existing)
|
||||
return
|
||||
}
|
||||
yield* Deferred.succeed(existing.deferred, undefined)
|
||||
pending.delete(input.requestID)
|
||||
if (input.reply !== "always" || !existing.request.save?.length) return
|
||||
|
||||
const rememberedRules = yield* savedRules()
|
||||
for (const [id, item] of pending) {
|
||||
const input = { ...item.request }
|
||||
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
|
||||
)
|
||||
if (!rules) continue
|
||||
if (denied(input, rules)) continue
|
||||
const effective = [...rules, ...rememberedRules]
|
||||
if (
|
||||
!item.request.resources.every(
|
||||
(resource) => evaluate(item.request.action, resource, effective).effect === "allow",
|
||||
)
|
||||
)
|
||||
continue
|
||||
yield* events.publish(Event.Replied, {
|
||||
sessionID: item.request.sessionID,
|
||||
requestID: item.request.id,
|
||||
reply: "always",
|
||||
})
|
||||
yield* Deferred.succeed(item.deferred, undefined)
|
||||
pending.delete(id)
|
||||
if (!existing.request.save?.length) {
|
||||
yield* settle(existing)
|
||||
return
|
||||
}
|
||||
|
||||
yield* saved.add({
|
||||
projectID: location.project.id,
|
||||
action: existing.request.action,
|
||||
resources: existing.request.save,
|
||||
})
|
||||
yield* settle(existing)
|
||||
yield* approveSavedMatches()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,10 +47,18 @@ function setup(rules: PermissionV2.Ruleset = []) {
|
|||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* setupSession(SessionV2.ID.make("ses_test"))
|
||||
yield* setRules(rules)
|
||||
})
|
||||
}
|
||||
|
||||
function setupSession(sessionID: SessionV2.ID) {
|
||||
return Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: SessionV2.ID.make("ses_test"),
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
slug: "test",
|
||||
directory: "/project",
|
||||
|
|
@ -61,7 +69,6 @@ function setup(rules: PermissionV2.Ruleset = []) {
|
|||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* setRules(rules)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +291,21 @@ describe("PermissionV2", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects every pending permission in the same session", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup()
|
||||
const service = yield* PermissionV2.Service
|
||||
const first = assertion({ id: PermissionV2.ID.create("per_first") })
|
||||
const second = assertion({ id: PermissionV2.ID.create("per_second") })
|
||||
yield* service.ask(first)
|
||||
yield* service.ask(second)
|
||||
|
||||
yield* service.reply({ requestID: first.id, reply: "reject" })
|
||||
|
||||
expect(yield* service.list()).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("stores and removes saved resources for a project", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup()
|
||||
|
|
@ -313,4 +335,30 @@ describe("PermissionV2", () => {
|
|||
expect(yield* saved.list()).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("resolves pending permissions covered by a saved approval", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup()
|
||||
const otherSession = SessionV2.ID.make("ses_other")
|
||||
yield* setupSession(otherSession)
|
||||
const service = yield* PermissionV2.Service
|
||||
const selected = assertion({ id: PermissionV2.ID.create("per_selected"), save: ["src/*"] })
|
||||
const covered = assertion({
|
||||
id: PermissionV2.ID.create("per_covered"),
|
||||
sessionID: otherSession,
|
||||
resources: ["src/covered.ts"],
|
||||
})
|
||||
const uncovered = assertion({
|
||||
id: PermissionV2.ID.create("per_uncovered"),
|
||||
resources: ["README.md"],
|
||||
})
|
||||
yield* service.ask(selected)
|
||||
yield* service.ask(covered)
|
||||
yield* service.ask(uncovered)
|
||||
|
||||
yield* service.reply({ requestID: selected.id, reply: "always" })
|
||||
|
||||
expect(yield* service.list()).toEqual([uncovered])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue