feat(concord): add owner-rooted authority resolver

Implement CORD-04 authority resolution over a folded Control Plane:

- ControlEntities: Role and Grant content DTOs (kotlinx.serialization, lenient +
  extensible) and a Banlist array parser; ConcordJson facility
- AuthorityResolver: builds roster state from the entity heads + known owner and
  answers rank (lower = higher; owner = 0), effectivePermissions (union of a
  member's roles), isBanned, hasPermission, and canActOn (holds the bit AND
  strictly outranks the target — equal cannot act on equal; owner unremovable)

Grants are validated by an owner-rooted fixpoint: a Grant is honored only when
its signer already outranks every assigned Role and holds MANAGE_ROLES, so the
roster grows strictly outward from the owner and self-referential cycles never
bootstrap. Deleted/position-0 roles and banned members are dropped. Banlists heal
to their union.

Tests cover owner-rooted ranks/permissions, fixpoint order-independence,
unauthorized/insufficient-rank grant rejection, ban vanishing, and invalid-role
dropping. Green on :quartz:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-09 21:56:09 +00:00
parent c865d0f4eb
commit 71db306f8d
4 changed files with 429 additions and 0 deletions
@@ -0,0 +1,174 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord04Roles
/**
* Resolves the owner-rooted authority state of a Concord community from its
* folded Control Plane (CORD-04).
*
* "The Roster is owner-rooted: every Grant and Role is signed by an npub the
* Roster ranks strictly above it, and the chain terminates at the owner."
*
* Build one with [resolve] from the current entity heads (the values of
* [EditionFold.fold]) plus the community's known owner pubkey. It then answers:
* - [rank] — a member's authority (lower is higher; owner is [OWNER_RANK]; a
* member with no validly-granted role has no rank).
* - [effectivePermissions] — the union of a member's roles' bits (owner: all).
* - [isBanned] — membership in the healed banlist union.
* - [canActOn] — whether an actor may take a permissioned action on a target:
* the actor must hold the bit, must strictly outrank the target (equal cannot
* act on equal), and the owner is unremovable.
*
* Grants are validated by a fixpoint that only ever empowers members reachable
* from the owner: a Grant is honored when its signer already outranks every
* assigned Role and holds [ConcordPermissions.MANAGE_ROLES]. Cycles that never
* touch the owner can never bootstrap themselves.
*/
class AuthorityResolver private constructor(
private val ownerLower: String,
private val roles: Map<String, RoleEntity>,
private val memberRoles: Map<String, Set<String>>,
private val banned: Set<String>,
) {
fun isOwner(pubKey: String): Boolean = pubKey.lowercase() == ownerLower
fun isBanned(pubKey: String): Boolean = pubKey.lowercase() in banned
/** The member's rank, lower being higher authority; null = no authority. Owner = [OWNER_RANK]. */
fun rank(pubKey: String): Long? {
val m = pubKey.lowercase()
if (m == ownerLower) return OWNER_RANK
val held = memberRoles[m] ?: return null
return held.mapNotNull { roles[it]?.position }.minOrNull()
}
/** The union of a member's roles' permission bits (owner holds every bit). */
fun effectivePermissions(pubKey: String): ConcordPermissions {
val m = pubKey.lowercase()
if (m == ownerLower) return ConcordPermissions.ALL
val held = memberRoles[m] ?: return ConcordPermissions.NONE
var acc = ConcordPermissions.NONE
for (id in held) roles[id]?.let { acc = acc union it.permissionBits() }
return acc
}
/** True if the member holds [bit] and is not banned. */
fun hasPermission(
pubKey: String,
bit: Int,
): Boolean = !isBanned(pubKey) && effectivePermissions(pubKey).has(bit)
/**
* Whether [actor] may take the action guarded by permission [bit] against
* [target]. Requires: actor not banned, actor holds [bit], the owner is never
* a valid target (unremovable), and actor strictly outranks target — "equal
* cannot act on equal".
*/
fun canActOn(
actor: String,
target: String,
bit: Int,
): Boolean {
if (!hasPermission(actor, bit)) return false
if (isOwner(target)) return false
val actorRank = rank(actor) ?: return false
val targetRank = rank(target) ?: Long.MAX_VALUE // no roles ⇒ lowest authority
return actorRank < targetRank
}
companion object {
/** The owner's rank — supreme and unremovable. No Role may claim it. */
const val OWNER_RANK = 0L
fun resolve(
heads: Collection<ControlEdition>,
ownerPubKey: String,
): AuthorityResolver {
val ownerLower = ownerPubKey.lowercase()
// Roles: keyed by role id (the edition entity id). Drop deleted roles
// and any that illegally claim position 0 (owner-peer).
val roles = HashMap<String, RoleEntity>()
for (e in heads) {
if (e.entityKind != ControlEntityKind.ROLE) continue
val r = ConcordJson.decodeOrNull<RoleEntity>(e.content) ?: continue
if (r.deleted || r.position < 1) continue
roles[e.entityIdHex] = r
}
// Grants: one head per member; remember the signing actor (granter).
val grants =
heads.mapNotNull { e ->
if (e.entityKind != ControlEntityKind.GRANT) return@mapNotNull null
val g = ConcordJson.decodeOrNull<GrantEntity>(e.content) ?: return@mapNotNull null
ParsedGrant(g.member.lowercase(), g.roleIds, e.author.lowercase())
}
// Banlist: heal to the union of every banlist head.
val banned = HashSet<String>()
for (e in heads) {
if (e.entityKind != ControlEntityKind.BANLIST) continue
ConcordJson.decodeBanlist(e.content)?.forEach { banned.add(it.lowercase()) }
}
val memberRoles = HashMap<String, Set<String>>()
fun rankOf(member: String): Long? {
if (member == ownerLower) return OWNER_RANK
val held = memberRoles[member] ?: return null
return held.mapNotNull { roles[it]?.position }.minOrNull()
}
fun holdsManageRoles(member: String): Boolean {
if (member == ownerLower) return true
val held = memberRoles[member] ?: return false
return held.any { roles[it]?.permissionBits()?.has(ConcordPermissions.MANAGE_ROLES) == true }
}
// Fixpoint: apply grants whose signer outranks every assigned role and
// holds MANAGE_ROLES. Only the owner is empowered a priori, so the
// roster grows strictly outward from the owner.
var changed = true
while (changed) {
changed = false
for (g in grants) {
val granterRank = rankOf(g.granter) ?: continue
if (!holdsManageRoles(g.granter)) continue
val assigned = g.roleIds.filter { roles.containsKey(it) }
if (assigned.any { granterRank >= roles.getValue(it).position }) continue // must strictly outrank each
val newSet = assigned.toSet()
if (memberRoles[g.member] != newSet) {
memberRoles[g.member] = newSet
changed = true
}
}
}
return AuthorityResolver(ownerLower, roles, memberRoles, banned)
}
}
private class ParsedGrant(
val member: String,
val roleIds: List<String>,
val granter: String,
)
}
@@ -52,6 +52,9 @@ value class ConcordPermissions(
companion object {
val NONE = ConcordPermissions(0uL)
/** Every bit set — the owner's implicit, supreme permission set. */
val ALL = ConcordPermissions(ULong.MAX_VALUE)
// Frozen bit positions (CORD-04 §Permission Bits).
const val MANAGE_ROLES = 0
const val MANAGE_CHANNELS = 1
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord04Roles
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
/**
* JSON facility for Concord Control Plane entity content. Unknown keys are
* ignored because entity shapes are deliberately client-extensible (CORD-03/04),
* and defaults are lenient so a partial entity never throws mid-fold.
*/
object ConcordJson {
val instance =
Json {
ignoreUnknownKeys = true
isLenient = true
explicitNulls = false
}
inline fun <reified T> decodeOrNull(content: String): T? =
try {
instance.decodeFromString<T>(content)
} catch (_: Exception) {
null
}
/** Parses a Banlist edition's content (a bare JSON array of hex pubkeys). */
fun decodeBanlist(content: String): List<String>? =
try {
instance.decodeFromString(ListSerializer(String.serializer()), content)
} catch (_: Exception) {
null
}
}
/**
* A Role's content (CORD-04): a named bundle of permissions at a [position].
* The role's id is the edition's entity id, not a content field. Lower [position]
* ranks higher; no role may claim position 0 (reserved for the owner).
*/
@Serializable
class RoleEntity(
val name: String = "",
val position: Long = 0,
/** u64 permission bitfield as a decimal string. */
val permissions: String = "0",
/** "server"-wide, or a channel id for a channel-scoped role. Null = server. */
val scope: String? = null,
val color: Long = 0,
val deleted: Boolean = false,
) {
fun permissionBits(): ConcordPermissions = ConcordPermissions.fromWireOrNull(permissions) ?: ConcordPermissions.NONE
}
/**
* A Grant's content (CORD-04): maps a [member] to the set of [roleIds] they hold.
* Honored only if the granting actor outranks every assigned Role and the chain
* terminates at the owner (see [AuthorityResolver]).
*/
@Serializable
class GrantEntity(
val member: String = "",
@SerialName("role_ids") val roleIds: List<String> = emptyList(),
)
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord04Roles
import com.vitorpamplona.quartz.concord.cord04Roles.ConcordPermissions.Companion.BAN
import com.vitorpamplona.quartz.concord.cord04Roles.ConcordPermissions.Companion.KICK
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class AuthorityResolverTest {
private val owner = "0f".repeat(32)
private val alice = "a1".repeat(32)
private val bob = "b2".repeat(32)
private val carol = "c3".repeat(32)
private val dave = "d4".repeat(32)
private val adminRole = "11".repeat(32)
private val modRole = "22".repeat(32)
// admin: position 1, KICK|BAN|MANAGE_ROLES = 8|16|1 = 25
private val adminJson = """{"name":"Admin","position":1,"permissions":"25"}"""
// mod: position 5, KICK only = 8 (no MANAGE_ROLES)
private val modJson = """{"name":"Mod","position":5,"permissions":"8"}"""
private fun role(
roleId: String,
json: String,
) = ControlEdition(ControlEntityKind.ROLE, roleId.hexToByteArray(), 0, null, null, json, owner, "role-$roleId", 0)
private fun grant(
grantId: String,
member: String,
roleIds: List<String>,
granter: String,
) = ControlEdition(
ControlEntityKind.GRANT,
grantId.hexToByteArray(),
0,
null,
null,
"""{"member":"$member","role_ids":[${roleIds.joinToString(",") { "\"$it\"" }}]}""",
granter,
"grant-$grantId",
0,
)
private fun banlist(vararg banned: String) =
ControlEdition(
ControlEntityKind.BANLIST,
"44".repeat(32).hexToByteArray(),
0,
null,
null,
"[${banned.joinToString(",") { "\"$it\"" }}]",
owner,
"ban",
0,
)
@Test
fun ranksPermissionsAndActionAuthorityAreOwnerRooted() {
val heads =
listOf(
role(adminRole, adminJson),
role(modRole, modJson),
// alice's grant appears BEFORE the owner's grant to prove fixpoint order-independence
grant("ba".repeat(32), bob, listOf(modRole), granter = alice),
grant("ab".repeat(32), alice, listOf(adminRole), granter = owner),
)
val r = AuthorityResolver.resolve(heads, owner)
assertTrue(r.isOwner(owner))
assertEquals(0L, r.rank(owner))
assertEquals(1L, r.rank(alice))
assertEquals(5L, r.rank(bob))
assertNull(r.rank(carol)) // no grant ⇒ no authority
assertTrue(r.effectivePermissions(alice).has(BAN))
assertFalse(r.effectivePermissions(bob).has(BAN))
assertTrue(r.effectivePermissions(bob).has(KICK))
// Higher rank can act on lower; equal cannot act on equal; nobody on owner.
assertTrue(r.canActOn(alice, bob, BAN))
assertFalse(r.canActOn(bob, alice, KICK)) // lower cannot act on higher
assertFalse(r.canActOn(alice, alice, KICK)) // equal-on-equal
assertFalse(r.canActOn(alice, owner, BAN)) // owner unremovable
assertTrue(r.canActOn(owner, alice, BAN)) // owner supreme
}
@Test
fun grantsFromUnauthorizedSignersAreIgnored() {
val heads =
listOf(
role(adminRole, adminJson),
// carol has no authority, so her grant to dave is dropped
grant("cd".repeat(32), dave, listOf(adminRole), granter = carol),
)
val r = AuthorityResolver.resolve(heads, owner)
assertNull(r.rank(dave))
assertEquals(ConcordPermissions.NONE.bits, r.effectivePermissions(dave).bits)
}
@Test
fun granterMustHoldManageRolesAndOutrankAssignedRole() {
val heads =
listOf(
role(adminRole, adminJson),
role(modRole, modJson),
grant("ab".repeat(32), alice, listOf(modRole), granter = owner), // alice is a mod (no MANAGE_ROLES)
grant("ae".repeat(32), dave, listOf(adminRole), granter = alice), // mod cannot grant admin
)
val r = AuthorityResolver.resolve(heads, owner)
assertEquals(5L, r.rank(alice))
assertNull(r.rank(dave)) // rejected: alice lacks MANAGE_ROLES and doesn't outrank admin
}
@Test
fun bannedMembersVanishFromAuthority() {
val heads =
listOf(
role(adminRole, adminJson),
grant("ab".repeat(32), alice, listOf(adminRole), granter = owner),
banlist(alice),
)
val r = AuthorityResolver.resolve(heads, owner)
assertTrue(r.isBanned(alice))
// A banned actor can take no action even though the role bit is present.
assertFalse(r.hasPermission(alice, BAN))
assertFalse(r.canActOn(alice, bob, BAN))
}
@Test
fun deletedRolesAndPositionZeroAreDropped() {
val heads =
listOf(
role(adminRole, """{"name":"Admin","position":1,"permissions":"25","deleted":true}"""),
role(modRole, """{"name":"Peer","position":0,"permissions":"25"}"""), // illegal position 0
grant("ab".repeat(32), alice, listOf(adminRole, modRole), granter = owner),
)
val r = AuthorityResolver.resolve(heads, owner)
assertNull(r.rank(alice)) // both assigned roles are invalid
}
}