MemberJunction / MemberJunction/MJ

MJ: Entity Permissions has the same unguarded Developer/Integration grant that #4282 closed on roles

Open
#4,327 0 comments 0 reactions 1 assignee Claimed by @SDesai-BC View on GitHub
Dominant language
TSQL
Stars
29
Forks
6
Avg merge
2d 1h
Merged PRs (30d)
323

Description

**Type:** security · **Related:** #4260 (closed `MJ: Users`), #4282 / PR #4305 (closed `MJ: Roles` + `MJ: User Roles`)

## Why now

PR #4305's changeset names this explicitly as deliberately out of scope:

> **Not closed by this change:** `MJ: Entity Permissions` carries the same unfiltered
> `Developer`/`Integration` grant, so a holder of either role can still widen a role's permissions
> directly.

It was left out because it is an independent route with its own invariant to decide, not because
it is closed. Nothing currently tracks it, so it exists only as a sentence in a changeset that
will be consumed at the next release. This issue is that tracking.

## The gap

MJ's authority has three surfaces. Two are now guarded:

| Surface | Entity | Guard |
|---|---|---|
| Who is a superuser | `MJ: Users` (`Type`) | `MJUserEntityServer` — #4260 |
| Who holds which role | `MJ: User Roles` | `MJUserRoleEntityServer` — #4282 |
| What a role can do | **`MJ: Entity Permissions`** | **none** |

The subset rule #4282 chose is a ceiling on *distribution*: a non-Owner may only hand out
authority they already hold. That ceiling assumes the authority attached to a role is fixed.
It is not — a `Developer` who cannot grant themselves `Integration` can instead widen what
`Developer` itself is permitted to do, and arrive at the same place without ever touching a role
assignment.

## What to decide

The invariant is genuinely not obvious, which is why #4305 declined to pick one in passing:

1. **Owner-only**, like `MJ: Roles` — simplest, and breaks any non-Owner permission administration.
2. **Subset rule**, like `MJ: User Roles` — a non-Owner may only grant a permission they themselves
hold on that entity. Consistent with #4282, but "the permissions I hold" is a computed union
across every role the caller has, not a single field read, so it is not the cheap pure check
the other two guards are.
3. **Scoped** — leave it open and rely on the database-role layer. Needs evidence that layer
actually holds.

## Reproduce

Same shape as #4282's reproduction: a caller whose `Type` is `'User'` holding only `Developer`,
writing `MJ: Entity Permissions` directly. Confirm the `Developer`/`Integration` grants on that
entity first — on a baseline seed both hold unfiltered `CanCreate`/`CanUpdate`/`CanDelete`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.