Flow
Rule set for each membership mutation
1Scope
Actor and team belong to the same organization.
2Grant ceiling
Requested role must not exceed the actor level.
3Target floor
Target current role must not exceed the actor level; equal rank is a written decision.
4Strict inputs
Unknown role strings are rejected before any comparison; missing actor role means least privilege.
5Verify
Direct-call test matrix over actor, target and requested role.
After my change, a regular member in my admin app could demote a team manager or remove them from a team. The change was meant to make it safe to let everyone manage teams. I found the holes by reading the diff back, not because anything went wrong.
Three actions lose their gate
Adding someone to a team, removing them and changing their team role used to need a senior role. I opened all three to every member. Making and deleting teams stayed locked. To keep it safe, I added a ceiling to add and change-role: you can't hand out a team role that ranks above your own. The dialog hides the roles you can't give. There were no tests.
The ceiling stops one thing, which is promotion. I'd guess that's the only abuse I was picturing.
Four ways past it
Demotion. Changing a role only checks the new role. It never looks at the role the person has now. A member can set a manager to member, because member is a role they're allowed to give.
Removal. I took the old gate off and put nothing back. Any member could remove anyone from a team, and the diff doesn't show whether that stops at their own organization.
Unknown roles. A role name the system doesn't know counts as the lowest rank, so it always passes. A capitalized spelling of a real role falls into that bucket too.
Missing roles. A caller with no role on record was already treated as a normal member. That didn't matter while normal members were blocked. Once I opened the gate, it did.
Two copies of the rule
The dropdown is a second copy of the rule, in the browser. Anyone can skip it by calling the server directly. Both sides should use one shared function.
Two people, not one
Every change has someone doing it and someone it's done to. Check both:
- Add: you can't give a role above your own.
- Change role: same, and you can't change someone who ranks above you.
- Remove: you can't remove someone who ranks above you.
The old check only compared the actor to the required role.
It never looked at the target person's role, so nothing stopped a caller from changing someone who outranks them.
Refuse role names you don't know. Don't rank them. Treat a missing role as the lowest rank, or say no.
Leaving, the last manager, and peers
Leaving a team on your own should always work. Removing the last manager leaves a team that only a senior person can fix. And whether two managers can demote each other is a choice I still have to make.
A table with every actor and every target
The tests I'd want come from a table: who is acting, who they're acting on, and what role they ask for, with yes or no in each box. Each test calls the server, not the dialog. Add cases for a made-up role, a capitalized one, a caller with no role, and a team in another organization. The demotion box is the one I'd fill in first.