use std::collections::BTreeMap;
use std::sync::Arc;
use alkcall::core::ownership::{OwnershipError, OwnershipStore};
use alkcall::core::Identity;
use alkcall::protocol::wire::{CallError, ResponseEnvelope};
use alkcall::registry::registration::{
make_handler, HandlerKind, HandlerRegistration, OperationProvenance, OperationRegistry,
};
use alkcall::registry::spec::{
AccessControl, ErrorDefinition, OperationSpec, OperationType, Visibility as OpVisibility,
};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::authorize::authorize_repo_op;
use crate::backend::GitRegistryStore;
use crate::error::RegistryError;
use crate::registry::{validate_repo_id, AccessAction, RepoRecord, Visibility};
pub const OP_GIT_REPO_CREATE: &str = "git/repo/create";
pub const OP_GIT_REPO_DELETE: &str = "git/repo/delete";
pub const OP_GIT_REPO_UPDATE: &str = "git/repo/update";
pub const OP_GIT_REPO_GET: &str = "git/repo/get";
pub const GIT_REPO_CREATE_SCOPE: &str = "git:repo:create";
pub const REPO_RESOURCE_TYPE: &str = "git/repo";
const CODE_UNAUTHORIZED: &str = "unauthorized";
const CODE_ALREADY_EXISTS: &str = "already_exists";
const CODE_INVALID: &str = "invalid";
const CODE_IO: &str = "io";
const GENERIC_DENIAL: &str = "access denied";
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateParams {
repo_id: String,
visibility: Visibility,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoIdParams {
repo_id: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct UpdateParams {
repo_id: String,
visibility: Option<Visibility>,
grants: Option<BTreeMap<String, Vec<AccessAction>>>,
}
fn call_error_schema(code: &str) -> Value {
json!({
"type": "object",
"properties": {
"code": { "type": "string", "const": code },
"message": { "type": "string" },
"retryable": { "type": "boolean" },
"details": { "type": "object" }
},
"required": ["code", "message", "retryable"]
})
}
fn error_definition(code: &str, description: &str) -> ErrorDefinition {
ErrorDefinition {
code: code.to_string(),
description: description.to_string(),
schema: call_error_schema(code),
http_status: None,
}
}
fn visibility_schema() -> Value {
json!({ "type": "string", "enum": ["public", "private"] })
}
fn grants_schema() -> Value {
json!({
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "type": "string", "enum": ["read", "write", "manage"] }
}
})
}
pub fn git_repo_create_spec() -> OperationSpec {
OperationSpec::new(
OP_GIT_REPO_CREATE,
OperationType::Mutation,
OpVisibility::External,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": visibility_schema()
},
"required": ["repo_id", "visibility"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": visibility_schema()
},
"required": ["repo_id", "visibility"]
}),
vec![
error_definition(
CODE_ALREADY_EXISTS,
"The requested repo id is already taken. Create-side only: the caller \
knows the id it sent, so the disclosure is no oracle (ADR-008/R-6) — \
a create-scope identity probing arbitrary ids is inside the trust \
boundary the scope grants.",
),
error_definition(
CODE_INVALID,
"Malformed request or a grammar-invalid repo_id: the message names \
the field and carries the correction.",
),
error_definition(
CODE_IO,
"The backing registry store failed to write the record. The message \
is the store's serialized failure reason.",
),
],
AccessControl {
required_scopes: vec![GIT_REPO_CREATE_SCOPE.to_string()],
..AccessControl::default()
},
None,
)
.with_description(
"Create a repo record. The caller's identity is seeded with the full grant \
set {read, write, manage} (ADR-015 §4) and alkcall ownership is minted \
(resource type `git/repo`). The response echoes {repo_id, visibility} — \
the grants live in the record, readable back through git/repo/get.",
)
}
pub fn git_repo_delete_spec() -> OperationSpec {
OperationSpec::new(
OP_GIT_REPO_DELETE,
OperationType::Mutation,
OpVisibility::External,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" }
},
"required": ["repo_id"],
"additionalProperties": false
}),
json!({ "type": "object" }),
vec![
error_definition(
CODE_UNAUTHORIZED,
"The collapsed denial (ADR-008 + review 001 N-2): an unknown repo_id \
and a caller lacking the manage-or-admin gate deny identically — \
one code, one generic message, no existence oracle. Also covers \
the registry store's Forbidden and a grammar-invalid id (the \
collapse is this op's rule for every id-shaped denial).",
),
error_definition(
CODE_IO,
"The backing registry store failed (resolve or removal). The message \
is the store's serialized failure reason.",
),
],
AccessControl::default(),
None,
)
.with_description(
"Delete a repo record (the GC consequence is substrate-side — the objects \
become liveness-uncovered and the sweep reclaims them; no synchronous \
object deletion). Gated on the resolved record: global admin scope OR a \
manage grant.",
)
}
pub fn git_repo_update_spec() -> OperationSpec {
OperationSpec::new(
OP_GIT_REPO_UPDATE,
OperationType::Mutation,
OpVisibility::External,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": visibility_schema(),
"grants": grants_schema()
},
"required": ["repo_id"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": visibility_schema(),
"grants": grants_schema()
},
"required": ["repo_id", "visibility", "grants"]
}),
vec![
error_definition(
CODE_UNAUTHORIZED,
"The collapsed denial (ADR-008 + review 001 N-2): an unknown repo_id \
and a caller lacking the manage-or-admin gate deny identically — \
one code, one generic message, no existence oracle.",
),
error_definition(
CODE_INVALID,
"Malformed request, a grammar-invalid repo_id, or a grant set \
rejected before write (e.g. an empty identity id) — the message \
names the field and carries the correction.",
),
error_definition(
CODE_IO,
"The backing registry store failed (resolve or write). The message \
is the store's serialized failure reason.",
),
],
AccessControl::default(),
None,
)
.with_description(
"Patch a repo record (review 002 R-3): omitted fields are left unchanged; \
present fields are replaced wholesale — `grants` present replaces the \
whole grant map (partial grant edits are caller-side read-modify-write). \
The response echoes the post-write record so the caller confirms what \
landed. Gated on the resolved record: global admin scope OR a manage \
grant.",
)
}
pub fn git_repo_get_spec() -> OperationSpec {
OperationSpec::new(
OP_GIT_REPO_GET,
OperationType::Query,
OpVisibility::External,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" }
},
"required": ["repo_id"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": visibility_schema(),
"grants": grants_schema()
},
"required": ["repo_id", "visibility", "grants"]
}),
vec![
error_definition(
CODE_UNAUTHORIZED,
"The collapsed denial (ADR-008 + review 001 N-2): an unknown repo_id \
and a caller lacking the manage-or-admin gate deny identically — \
one code, one generic message, no existence oracle.",
),
error_definition(
CODE_IO,
"The backing registry store failed to resolve. The message is the \
store's serialized failure reason.",
),
],
AccessControl::default(),
None,
)
.with_description(
"Read a repo record (single read; list is a v2 additive op). An op-gated \
read: global admin scope OR a manage grant — an anonymous public-repo \
read does not exist here (that is the git access path's rule, not the \
record-read tier).",
)
}
fn wire_error(error: &RegistryError) -> CallError {
let (code, message) = match error {
RegistryError::NotFound | RegistryError::Forbidden => {
(CODE_UNAUTHORIZED, GENERIC_DENIAL.to_string())
}
RegistryError::AlreadyExists => (CODE_ALREADY_EXISTS, error.to_string()),
RegistryError::Invalid => (CODE_INVALID, error.to_string()),
RegistryError::Io(_) => (CODE_IO, error.to_string()),
};
CallError::new(code, message, false)
}
fn invalid_field(field: &str, cause: impl std::fmt::Display) -> CallError {
CallError::new(CODE_INVALID, format!("invalid {field}: {cause}"), false)
}
fn malformed_request(cause: impl std::fmt::Display) -> CallError {
CallError::new(CODE_INVALID, format!("malformed request: {cause}"), false)
}
fn collapsed_denial() -> CallError {
CallError::new(CODE_UNAUTHORIZED, GENERIC_DENIAL, false)
}
async fn create_repo(
store: &dyn GitRegistryStore,
ownership: &dyn OwnershipStore,
identity: Option<&Identity>,
input: &Value,
) -> Result<Value, CallError> {
let params: CreateParams = serde_json::from_value(input.clone()).map_err(malformed_request)?;
if let Err(cause) = validate_repo_id(¶ms.repo_id) {
return Err(invalid_field("repo_id", cause));
}
let Some(identity) = identity else {
return Err(collapsed_denial());
};
let record = RepoRecord {
repo_id: params.repo_id.clone(),
visibility: params.visibility,
grants: BTreeMap::from([(
identity.id.clone(),
vec![
AccessAction::Read,
AccessAction::Write,
AccessAction::Manage,
],
)]),
};
store.put_repo(&record).await.map_err(|e| wire_error(&e))?;
ownership
.record(identity, REPO_RESOURCE_TYPE, ¶ms.repo_id)
.await
.map_err(|cause| ownership_mint_failure(&cause))?;
Ok(json!({
"repo_id": params.repo_id,
"visibility": params.visibility,
}))
}
fn ownership_mint_failure(cause: &OwnershipError) -> CallError {
CallError::new(CODE_IO, format!("ownership mint failed: {cause}"), false)
}
async fn delete_repo(
store: &dyn GitRegistryStore,
identity: Option<&Identity>,
input: &Value,
) -> Result<Value, CallError> {
let params: RepoIdParams = serde_json::from_value(input.clone()).map_err(malformed_request)?;
let record = store
.resolve(¶ms.repo_id)
.await
.map_err(|e| wire_error(&e))?;
if !authorize_repo_op(&record, identity) {
return Err(collapsed_denial());
}
store
.remove_repo(¶ms.repo_id)
.await
.map_err(|e| wire_error(&e))?;
Ok(json!({}))
}
async fn update_repo_op(
store: &dyn GitRegistryStore,
identity: Option<&Identity>,
input: &Value,
) -> Result<Value, CallError> {
let params: UpdateParams = serde_json::from_value(input.clone()).map_err(malformed_request)?;
if let Err(cause) = validate_repo_id(¶ms.repo_id) {
return Err(invalid_field("repo_id", cause));
}
if let Some(grants) = ¶ms.grants {
if grants.keys().any(|key| key.is_empty()) {
return Err(invalid_field("grants", "empty identity id"));
}
}
let mut record = store
.resolve(¶ms.repo_id)
.await
.map_err(|e| wire_error(&e))?;
if !authorize_repo_op(&record, identity) {
return Err(collapsed_denial());
}
if let Some(visibility) = params.visibility {
record.visibility = visibility;
}
if let Some(grants) = params.grants {
record.grants = grants;
}
store
.update_repo(&record)
.await
.map_err(|e| wire_error(&e))?;
serde_json::to_value(&record).map_err(malformed_request)
}
async fn get_repo(
store: &dyn GitRegistryStore,
identity: Option<&Identity>,
input: &Value,
) -> Result<Value, CallError> {
let params: RepoIdParams = serde_json::from_value(input.clone()).map_err(malformed_request)?;
let record = store
.resolve(¶ms.repo_id)
.await
.map_err(|e| wire_error(&e))?;
if !authorize_repo_op(&record, identity) {
return Err(collapsed_denial());
}
serde_json::to_value(&record).map_err(malformed_request)
}
pub fn register_repo_ops(
op_registry: &mut OperationRegistry,
store: Arc<dyn GitRegistryStore>,
ownership: Arc<dyn OwnershipStore>,
) -> Result<(), String> {
for (spec, handler) in [
(
git_repo_create_spec(),
make_create_handler(Arc::clone(&store), Arc::clone(&ownership)),
),
(
git_repo_delete_spec(),
make_delete_handler(Arc::clone(&store)),
),
(
git_repo_update_spec(),
make_update_handler(Arc::clone(&store)),
),
(git_repo_get_spec(), make_get_handler(Arc::clone(&store))),
] {
op_registry.register(HandlerRegistration::new(
spec,
HandlerKind::Once(handler),
OperationProvenance::Local,
None,
None,
Default::default(),
))?;
}
Ok(())
}
fn make_create_handler(
store: Arc<dyn GitRegistryStore>,
ownership: Arc<dyn OwnershipStore>,
) -> alkcall::registry::registration::Handler {
make_handler(move |input, context| {
let store = Arc::clone(&store);
let ownership = Arc::clone(&ownership);
let identity = context.identity.clone();
let request_id = context.request_id.clone();
async move {
match create_repo(&*store, &*ownership, identity.as_ref(), &input).await {
Ok(output) => ResponseEnvelope::ok(request_id, output),
Err(err) => ResponseEnvelope::error(request_id, err),
}
}
})
}
fn make_delete_handler(
store: Arc<dyn GitRegistryStore>,
) -> alkcall::registry::registration::Handler {
make_handler(move |input, context| {
let store = Arc::clone(&store);
let identity = context.identity.clone();
let request_id = context.request_id.clone();
async move {
match delete_repo(&*store, identity.as_ref(), &input).await {
Ok(output) => ResponseEnvelope::ok(request_id, output),
Err(err) => ResponseEnvelope::error(request_id, err),
}
}
})
}
fn make_update_handler(
store: Arc<dyn GitRegistryStore>,
) -> alkcall::registry::registration::Handler {
make_handler(move |input, context| {
let store = Arc::clone(&store);
let identity = context.identity.clone();
let request_id = context.request_id.clone();
async move {
match update_repo_op(&*store, identity.as_ref(), &input).await {
Ok(output) => ResponseEnvelope::ok(request_id, output),
Err(err) => ResponseEnvelope::error(request_id, err),
}
}
})
}
fn make_get_handler(store: Arc<dyn GitRegistryStore>) -> alkcall::registry::registration::Handler {
make_handler(move |input, context| {
let store = Arc::clone(&store);
let identity = context.identity.clone();
let request_id = context.request_id.clone();
async move {
match get_repo(&*store, identity.as_ref(), &input).await {
Ok(output) => ResponseEnvelope::ok(request_id, output),
Err(err) => ResponseEnvelope::error(request_id, err),
}
}
})
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};
use alkcall::core::ownership::InMemoryOwnershipStore;
use alkcall::core::Identity;
use alkcall::registry::context::OperationContext;
use async_trait::async_trait;
use super::*;
use crate::authorize::GIT_ADMIN_SCOPE;
#[derive(Default)]
struct MemoryRegistry {
rows: parking_lot::Mutex<BTreeMap<String, RepoRecord>>,
}
impl MemoryRegistry {
async fn put(&self, record: RepoRecord) {
self.rows.lock().insert(record.repo_id.clone(), record);
}
async fn get(&self, repo_id: &str) -> Option<RepoRecord> {
self.rows.lock().get(repo_id).cloned()
}
}
#[async_trait]
impl crate::backend::GitRegistry for MemoryRegistry {
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError> {
self.get(repo_id).await.ok_or(RegistryError::NotFound)
}
}
#[async_trait]
impl GitRegistryStore for MemoryRegistry {
async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError> {
let mut rows = self.rows.lock();
if rows.contains_key(&record.repo_id) {
return Err(RegistryError::AlreadyExists);
}
rows.insert(record.repo_id.clone(), record.clone());
Ok(())
}
async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError> {
let mut rows = self.rows.lock();
if !rows.contains_key(&record.repo_id) {
return Err(RegistryError::NotFound);
}
rows.insert(record.repo_id.clone(), record.clone());
Ok(())
}
async fn remove_repo(&self, repo_id: &str) -> Result<(), RegistryError> {
self.rows
.lock()
.remove(repo_id)
.map(|_| ())
.ok_or(RegistryError::NotFound)
}
}
#[derive(Default)]
struct SpiedOwnership {
inner: InMemoryOwnershipStore,
mints: parking_lot::Mutex<Vec<(String, String, String)>>,
}
#[async_trait]
impl OwnershipStore for SpiedOwnership {
async fn record(
&self,
identity: &Identity,
resource_type: &str,
resource_id: &str,
) -> Result<(), OwnershipError> {
self.mints.lock().push((
identity.id.clone(),
resource_type.to_string(),
resource_id.to_string(),
));
self.inner
.record(identity, resource_type, resource_id)
.await
}
async fn revoke(
&self,
resource_type: &str,
resource_id: &str,
) -> Result<(), OwnershipError> {
self.inner.revoke(resource_type, resource_id).await
}
}
fn identity(id: &str) -> Identity {
Identity {
id: id.to_string(),
scopes: Vec::new(),
resources: HashMap::new(),
}
}
fn scoped(id: &str, scopes: &[&str]) -> Identity {
Identity {
id: id.to_string(),
scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
resources: HashMap::new(),
}
}
fn request(input: Value, identity: Option<Identity>) -> (Value, OperationContext) {
(
input,
OperationContext {
request_id: "r-test".to_string(),
parent_request_id: None,
identity,
handler_identity: None,
forwarded_for: None,
capabilities: Default::default(),
metadata: HashMap::new(),
scoped_env: alkcall::registry::context::ScopedPeerEnv::empty(),
env: Arc::new(alkcall::registry::env::LocalOperationEnv::new(Arc::new(
OperationRegistry::new(),
))),
abort_policy: Default::default(),
deadline: None,
internal: false,
ownership: None,
},
)
}
async fn call_on(
store: Arc<dyn GitRegistryStore>,
ownership: Arc<dyn OwnershipStore>,
name: &str,
input: Value,
identity: Option<Identity>,
) -> ResponseEnvelope {
let mut op_registry = OperationRegistry::new();
register_repo_ops(&mut op_registry, store, ownership).expect("the four ops register");
let registration = op_registry
.registration(name)
.expect("the op is registered");
let handler = match registration.handler {
HandlerKind::Once(handler) => handler,
_ => panic!("a call op is Once-typed"),
};
let (input, context) = request(input, identity);
handler(input, context).await
}
type SeedRow<'a> = (&'a str, Visibility, &'a [(&'a str, &'a [AccessAction])]);
fn seed_record(
repo_id: &str,
visibility: Visibility,
grants: &[(&str, &[AccessAction])],
) -> RepoRecord {
RepoRecord {
repo_id: repo_id.to_string(),
visibility,
grants: grants
.iter()
.map(|(key, actions)| ((*key).to_string(), actions.to_vec()))
.collect(),
}
}
fn store_with(rows: &[SeedRow<'_>]) -> Arc<MemoryRegistry> {
let store = Arc::new(MemoryRegistry::default());
for &(repo_id, visibility, grants) in rows {
let record = seed_record(repo_id, visibility, grants);
futures::executor::block_on(store.put(record));
}
store
}
fn full_grants() -> Vec<AccessAction> {
vec![
AccessAction::Read,
AccessAction::Write,
AccessAction::Manage,
]
}
#[test]
fn the_four_specs_pin_the_backend_names_and_external_visibility() {
let specs = [
git_repo_create_spec(),
git_repo_delete_spec(),
git_repo_update_spec(),
git_repo_get_spec(),
];
assert_eq!(specs[0].name, "git/repo/create");
assert_eq!(specs[1].name, "git/repo/delete");
assert_eq!(specs[2].name, "git/repo/update");
assert_eq!(specs[3].name, "git/repo/get");
for (spec, op_type) in specs.iter().zip([
OperationType::Mutation,
OperationType::Mutation,
OperationType::Mutation,
OperationType::Query,
]) {
assert_eq!(spec.op_type, op_type);
assert_eq!(spec.visibility, OpVisibility::External);
assert!(spec.channel_open.is_none());
}
}
#[test]
fn the_gate_split_is_the_spec_access_control_shape() {
let create = git_repo_create_spec();
assert_eq!(
create.access_control.required_scopes,
vec!["git:repo:create".to_string()]
);
for spec in [
git_repo_delete_spec(),
git_repo_update_spec(),
git_repo_get_spec(),
] {
assert!(
!spec.access_control.has_restrictions(),
"handler-gated ops carry an empty AccessControl"
);
}
}
#[test]
fn the_input_schemas_are_byte_faithful_to_the_pinned_table() {
let create = git_repo_create_spec();
assert_eq!(
create.input_schema,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": { "type": "string", "enum": ["public", "private"] }
},
"required": ["repo_id", "visibility"],
"additionalProperties": false
})
);
let delete = git_repo_delete_spec();
assert_eq!(
delete.input_schema,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" }
},
"required": ["repo_id"],
"additionalProperties": false
})
);
assert_eq!(
delete.output_schema,
json!({ "type": "object" }),
"the empty-object response shape"
);
let update = git_repo_update_spec();
assert_eq!(
update.input_schema,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": { "type": "string", "enum": ["public", "private"] },
"grants": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "type": "string", "enum": ["read", "write", "manage"] }
}
}
},
"required": ["repo_id"],
"additionalProperties": false
}),
"visibility and grants optional, both optional — the PATCH request shape"
);
let get = git_repo_get_spec();
assert_eq!(
get.input_schema,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" }
},
"required": ["repo_id"],
"additionalProperties": false
})
);
}
#[test]
fn the_response_schemas_are_the_pinned_shapes() {
let create = git_repo_create_spec();
assert_eq!(
create.output_schema,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": { "type": "string", "enum": ["public", "private"] }
},
"required": ["repo_id", "visibility"]
})
);
for (record_out, why) in [
(
git_repo_update_spec().output_schema,
"update echoes the post-write record",
),
(git_repo_get_spec().output_schema, "get echoes the record"),
] {
assert_eq!(
record_out,
json!({
"type": "object",
"properties": {
"repo_id": { "type": "string" },
"visibility": { "type": "string", "enum": ["public", "private"] },
"grants": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "type": "string", "enum": ["read", "write", "manage"] }
}
}
},
"required": ["repo_id", "visibility", "grants"]
}),
"{why}"
);
}
}
#[test]
fn the_error_columns_pin_the_wire_codes_per_op() {
for (spec, codes) in [
(
git_repo_create_spec(),
vec!["already_exists", "invalid", "io"],
),
(git_repo_delete_spec(), vec!["unauthorized", "io"]),
(
git_repo_update_spec(),
vec!["unauthorized", "invalid", "io"],
),
(git_repo_get_spec(), vec!["unauthorized", "io"]),
] {
let declared: Vec<&str> = spec
.error_schemas
.iter()
.map(|def| def.code.as_str())
.collect();
for code in &codes {
let def = spec
.error_schemas
.iter()
.find(|d| d.code == **code)
.unwrap_or_else(|| panic!("{code} declared"));
assert_eq!(
def.schema["properties"]["code"]["const"],
json!(code),
"the schema pins the code"
);
}
assert_eq!(declared.len(), codes.len(), "one ErrorDefinition per code");
}
assert!(
!git_repo_delete_spec()
.error_schemas
.iter()
.any(|d| d.code == "already_exists"),
"already_exists is create-side only"
);
assert!(
!git_repo_get_spec()
.error_schemas
.iter()
.any(|d| d.code == "already_exists"),
"already_exists is create-side only"
);
assert!(
!git_repo_update_spec()
.error_schemas
.iter()
.any(|d| d.code == "already_exists"),
"already_exists is create-side only"
);
}
#[test]
fn register_repo_ops_registers_all_four_pairs() {
let mut op_registry = OperationRegistry::new();
register_repo_ops(
&mut op_registry,
Arc::new(MemoryRegistry::default()),
Arc::new(SpiedOwnership::default()),
)
.expect("all four register");
for name in [
OP_GIT_REPO_CREATE,
OP_GIT_REPO_DELETE,
OP_GIT_REPO_UPDATE,
OP_GIT_REPO_GET,
] {
let registration = op_registry
.registration(name)
.unwrap_or_else(|| panic!("{name} registered"));
assert!(matches!(registration.handler, HandlerKind::Once(_)));
}
}
#[tokio::test]
async fn the_ops_gate_the_matrix_through_the_handlers() {
let ownership = Arc::new(SpiedOwnership::default());
for (name, req, op_name) in [
(
OP_GIT_REPO_DELETE,
json!({ "repo_id": "owner/doubled" }),
"delete",
),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "owner/doubled", "visibility": "public" }),
"update",
),
(
OP_GIT_REPO_GET,
json!({ "repo_id": "owner/doubled" }),
"get",
),
] {
let manage = store_with(&[(
"owner/doubled",
Visibility::Private,
&[("u-7731", &[AccessAction::Manage])],
)]);
let granted = call_on(
manage.clone(),
ownership.clone(),
name,
req.clone(),
Some(identity("u-7731")),
)
.await;
assert!(
granted.result.is_ok(),
"{op_name}: a manage grant passes the two-tier gate"
);
if op_name == "delete" {
assert!(
manage.get("owner/doubled").await.is_none(),
"the record was removed"
);
}
}
let read_only = store_with(&[(
"owner/doubled",
Visibility::Private,
&[("u-7731", &[AccessAction::Read, AccessAction::Write])],
)]);
for (name, req) in [
(OP_GIT_REPO_DELETE, json!({ "repo_id": "owner/doubled" })),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "owner/doubled", "visibility": "public" }),
),
(OP_GIT_REPO_GET, json!({ "repo_id": "owner/doubled" })),
] {
let denied = call_on(
read_only.clone(),
ownership.clone(),
name,
req.clone(),
Some(identity("u-7731")),
)
.await;
match denied.result {
Err(err) => assert_eq!(err.code, "unauthorized", "{name}: read/write denied"),
Ok(_) => panic!("{name}: read/write grants deny"),
}
}
}
#[tokio::test]
async fn the_ops_deny_anonymous_callers() {
let ownership = Arc::new(SpiedOwnership::default());
let store = store_with(&[("owner/doubled", Visibility::Private, &[])]);
for (name, req) in [
(OP_GIT_REPO_DELETE, json!({ "repo_id": "owner/doubled" })),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "owner/doubled", "visibility": "public" }),
),
(OP_GIT_REPO_GET, json!({ "repo_id": "owner/doubled" })),
] {
let denied = call_on(store.clone(), ownership.clone(), name, req, None).await;
match denied.result {
Err(err) => {
assert_eq!(err.code, "unauthorized");
assert_eq!(err.message, GENERIC_DENIAL);
}
Ok(_) => panic!("{name}: anonymous denies"),
}
}
}
#[tokio::test]
async fn unknown_repo_and_unauthorized_collapse_to_one_denial() {
let ownership = Arc::new(SpiedOwnership::default());
let empty = Arc::new(MemoryRegistry::default());
let existing = store_with(&[("owner/doubled", Visibility::Private, &[])]);
let stranger = identity("stranger");
for (name, req) in [
(OP_GIT_REPO_DELETE, "owner/nothing"),
(OP_GIT_REPO_UPDATE, "owner/nothing"),
(OP_GIT_REPO_GET, "owner/nothing"),
] {
let request = json!({ "repo_id": req });
let unknown = call_on(
empty.clone(),
ownership.clone(),
name,
request,
Some(stranger.clone()),
)
.await;
let denied = call_on(
existing.clone(),
ownership.clone(),
name,
json!({ "repo_id": "owner/doubled" }),
Some(stranger.clone()),
)
.await;
match (unknown.result, denied.result) {
(Err(u), Err(d)) => {
assert_eq!(u.code, "unauthorized");
assert_eq!(d.code, "unauthorized");
assert_eq!(u.message, d.message, "one generic message — no oracle");
}
_ => panic!("{name}: both deny as the collapsed denial"),
}
}
}
#[tokio::test]
async fn the_admin_scope_passes_the_gate_on_a_grantless_private_repo() {
let ownership = Arc::new(SpiedOwnership::default());
let grantless = store_with(&[("owner/doubled", Visibility::Private, &[])]);
let admin = scoped("u-admin", &[GIT_ADMIN_SCOPE]);
let deleted = call_on(
grantless.clone(),
ownership.clone(),
OP_GIT_REPO_DELETE,
json!({ "repo_id": "owner/doubled" }),
Some(admin.clone()),
)
.await;
assert!(deleted.result.is_ok(), "admin scope passes delete");
assert_eq!(deleted.result.unwrap(), json!({}));
assert!(grantless.get("owner/doubled").await.is_none(), "removed");
let grantless = store_with(&[("owner/doubled", Visibility::Private, &[])]);
let read = call_on(
grantless.clone(),
ownership.clone(),
OP_GIT_REPO_GET,
json!({ "repo_id": "owner/doubled" }),
Some(admin.clone()),
)
.await;
assert!(read.result.is_ok(), "admin scope passes get");
}
#[tokio::test]
async fn create_seeds_the_callers_full_grant_set_and_mints_ownership() {
let store = Arc::new(MemoryRegistry::default());
let ownership = Arc::new(SpiedOwnership::default());
let creator = identity("u-7731");
let created = call_on(
store.clone(),
ownership.clone(),
OP_GIT_REPO_CREATE,
json!({ "repo_id": "owner/new", "visibility": "private" }),
Some(creator.clone()),
)
.await;
let output = created.result.expect("create succeeds");
assert_eq!(
output,
json!({ "repo_id": "owner/new", "visibility": "private" }),
"the response echoes repo_id + visibility — no grants field"
);
let record = store.get("owner/new").await.expect("the record landed");
assert_eq!(record.visibility, Visibility::Private);
assert_eq!(
record.grants.get("u-7731").map(Vec::as_slice),
Some(full_grants().as_slice()),
"the creator's grants are seeded as the full set"
);
let mints = ownership.mints.lock().clone();
assert_eq!(
mints,
vec![(
"u-7731".to_string(),
REPO_RESOURCE_TYPE.to_string(),
"owner/new".to_string()
)],
"the mint recorded (identity, resource_type, resource_id)"
);
}
#[tokio::test]
async fn a_taken_id_is_already_exists_create_side() {
let store = store_with(&[("owner/new", Visibility::Public, &[])]);
let ownership = Arc::new(SpiedOwnership::default());
let error = call_on(
store,
ownership,
OP_GIT_REPO_CREATE,
json!({ "repo_id": "owner/new", "visibility": "private" }),
Some(identity("u-7731")),
)
.await
.result;
match error {
Err(err) => assert_eq!(err.code, "already_exists", "{err:?}"),
Ok(_) => panic!("a taken id is already_exists"),
}
}
#[tokio::test]
async fn an_identity_less_create_fails_closed() {
let store = Arc::new(MemoryRegistry::default());
let ownership = Arc::new(SpiedOwnership::default());
let denied = call_on(
store.clone(),
ownership.clone(),
OP_GIT_REPO_CREATE,
json!({ "repo_id": "owner/new", "visibility": "private" }),
None,
)
.await
.result;
match denied {
Err(err) => {
assert_eq!(err.code, "unauthorized");
assert!(store.get("owner/new").await.is_none());
}
Ok(_) => panic!("an identity-less create denies"),
}
assert!(
ownership.mints.lock().is_empty(),
"no mint for a denied create"
);
}
#[tokio::test]
async fn unknown_input_fields_are_rejected() {
let store = Arc::new(MemoryRegistry::default());
let ownership = Arc::new(SpiedOwnership::default());
for (name, input) in [
(
OP_GIT_REPO_CREATE,
json!({ "repo_id": "owner/new", "visibility": "private", "storage_root": "/srv" }),
),
(
OP_GIT_REPO_DELETE,
json!({ "repo_id": "o/n", "root": "/srv" }),
),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "o/n", "visibility": "private", "root": "/srv" }),
),
(OP_GIT_REPO_GET, json!({ "repo_id": "o/n", "root": "/srv" })),
] {
let rejected = call_on(
store.clone(),
ownership.clone(),
name,
input,
Some(identity("u-7731")),
)
.await
.result;
match rejected {
Err(err) => assert_eq!(err.code, "invalid", "{name}: {err:?}"),
Ok(_) => panic!("{name}: an unknown input field fails closed"),
}
}
}
#[tokio::test]
async fn a_grammar_invalid_id_is_invalid_with_the_field_name() {
let store = Arc::new(MemoryRegistry::default());
let ownership = Arc::new(SpiedOwnership::default());
for (name, input) in [
(
OP_GIT_REPO_CREATE,
json!({ "repo_id": "Owner/not-me", "visibility": "private" }),
),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "a/b/c", "visibility": "private" }),
),
] {
let rejected = call_on(
store.clone(),
ownership.clone(),
name,
input,
Some(scoped("u-7731", &[GIT_REPO_CREATE_SCOPE])),
)
.await
.result;
match rejected {
Err(err) => {
assert_eq!(err.code, "invalid", "{name}: {err:?}");
assert_eq!(err.code, "invalid");
assert!(
err.message.contains("repo_id"),
"the field name is the correction: {}",
err.message
);
}
Ok(_) => panic!("{name}: a grammar-invalid id is invalid"),
}
}
assert!(store.rows.lock().is_empty(), "nothing was written");
}
#[tokio::test]
async fn delete_and_get_treat_a_grammar_invalid_id_as_the_collapse() {
let ownership = Arc::new(SpiedOwnership::default());
let store = store_with(&[("owner/doubled", Visibility::Private, &[])]);
for (name, req) in [
(OP_GIT_REPO_DELETE, json!({ "repo_id": "a/b/c" })),
(OP_GIT_REPO_GET, json!({ "repo_id": "a/b/c" })),
] {
let denied = call_on(
store.clone(),
ownership.clone(),
name,
req,
Some(scoped("u-7731", &[GIT_ADMIN_SCOPE])),
)
.await
.result;
match denied {
Err(err) => {
assert_eq!(err.code, "unauthorized", "{name}: {err:?}");
assert_eq!(err.message, GENERIC_DENIAL);
}
Ok(_) => panic!("{name}: a grammar-invalid id denies"),
}
}
}
#[tokio::test]
async fn a_visibility_only_update_preserves_the_grants() {
let store = store_with(&[(
"owner/doubled",
Visibility::Public,
&[
("u-7731", &full_grants()),
("u-other", &[AccessAction::Read]),
],
)]);
let ownership = Arc::new(SpiedOwnership::default());
let patched = call_on(
store.clone(),
ownership,
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "owner/doubled", "visibility": "private" }),
Some(identity("u-7731")),
)
.await
.result
.expect("the update lands");
let record = store.get("owner/doubled").await.expect("the record");
assert_eq!(record.visibility, Visibility::Private);
assert_eq!(record.grants.len(), 2, "the grants are untouched");
assert_eq!(
patched,
serde_json::to_value(&record).unwrap(),
"the response echoes the post-write record"
);
}
#[tokio::test]
async fn a_grants_present_update_replaces_the_whole_map() {
let store = store_with(&[(
"owner/doubled",
Visibility::Public,
&[
("u-7731", &full_grants()),
("u-other", &[AccessAction::Read]),
],
)]);
let ownership = Arc::new(SpiedOwnership::default());
let patched = call_on(
store.clone(),
ownership,
OP_GIT_REPO_UPDATE,
json!({
"repo_id": "owner/doubled",
"grants": { "u-bot": ["read"] }
}),
Some(identity("u-7731")),
)
.await
.result
.expect("the update lands");
let record = store.get("owner/doubled").await.expect("the record");
assert_eq!(
record.visibility,
Visibility::Public,
"visibility untouched"
);
assert_eq!(record.grants.len(), 1, "the whole map replaced");
assert_eq!(
record.grants.get("u-bot").map(Vec::as_slice),
Some(&[AccessAction::Read] as &[AccessAction])
);
assert_eq!(
patched,
serde_json::to_value(&record).unwrap(),
"the response echoes the post-write record"
);
}
#[tokio::test]
async fn get_returns_the_record_shape() {
let store = store_with(&[(
"owner/doubled",
Visibility::Public,
&[(
"u-7731",
&[
AccessAction::Read,
AccessAction::Write,
AccessAction::Manage,
],
)],
)]);
let ownership = Arc::new(SpiedOwnership::default());
let read = call_on(
store,
ownership,
OP_GIT_REPO_GET,
json!({ "repo_id": "owner/doubled" }),
Some(identity("u-7731")),
)
.await
.result
.expect("the read lands");
assert_eq!(
read,
json!({
"repo_id": "owner/doubled",
"visibility": "public",
"grants": { "u-7731": ["read", "write", "manage"] }
}),
"the wire record shape, grants lowercase"
);
}
#[test]
fn every_registry_error_variant_maps_to_its_wire_code() {
let rows: Vec<(RegistryError, &str, &str)> = vec![
(RegistryError::NotFound, "unauthorized", GENERIC_DENIAL),
(RegistryError::Forbidden, "unauthorized", GENERIC_DENIAL),
(
RegistryError::AlreadyExists,
"already_exists",
"repo already exists",
),
(
RegistryError::Invalid,
"invalid",
"invalid registry record or field",
),
(
RegistryError::Io("sqlite step failed".to_string()),
"io",
"registry backing-store failure: sqlite step failed",
),
];
for (error, code, message) in rows {
let wire = wire_error(&error);
assert_eq!(wire.code, code);
assert_eq!(wire.message, message);
assert!(!wire.retryable);
}
}
#[tokio::test]
async fn an_io_failure_maps_to_the_io_wire_code() {
struct FailingStore;
#[async_trait]
impl crate::backend::GitRegistry for FailingStore {
async fn resolve(&self, _repo_id: &str) -> Result<RepoRecord, RegistryError> {
Err(RegistryError::Io("sqlite step failed".to_string()))
}
}
#[async_trait]
impl GitRegistryStore for FailingStore {
async fn put_repo(&self, _record: &RepoRecord) -> Result<(), RegistryError> {
Err(RegistryError::Io("write failed".to_string()))
}
async fn update_repo(&self, _record: &RepoRecord) -> Result<(), RegistryError> {
Err(RegistryError::Io("write failed".to_string()))
}
async fn remove_repo(&self, _repo_id: &str) -> Result<(), RegistryError> {
Err(RegistryError::Io("remove failed".to_string()))
}
}
let failing: Arc<dyn GitRegistryStore> = Arc::new(FailingStore);
let ownership = Arc::new(SpiedOwnership::default());
for (name, req) in [
(
OP_GIT_REPO_CREATE,
json!({ "repo_id": "o/n", "visibility": "private" }),
),
(OP_GIT_REPO_DELETE, json!({ "repo_id": "o/n" })),
(
OP_GIT_REPO_UPDATE,
json!({ "repo_id": "o/n", "visibility": "public" }),
),
(OP_GIT_REPO_GET, json!({ "repo_id": "o/n" })),
] {
let denied = call_on(
failing.clone(),
ownership.clone(),
name,
req,
Some(scoped("u-admin", &[GIT_ADMIN_SCOPE])),
)
.await
.result;
match denied {
Err(err) => assert_eq!(err.code, "io", "{name}: {err:?}"),
Ok(_) => panic!("{name}: a backing-store failure is io"),
}
}
}
#[tokio::test]
async fn an_empty_grant_key_is_invalid_with_the_field_name() {
let store = store_with(&[(
"owner/doubled",
Visibility::Private,
&[("u-7731", &full_grants())],
)]);
let ownership = Arc::new(SpiedOwnership::default());
let rejected = call_on(
store,
ownership,
OP_GIT_REPO_UPDATE,
json!({
"repo_id": "owner/doubled",
"grants": { "": ["read"] }
}),
Some(identity("u-7731")),
)
.await
.result;
match rejected {
Err(err) => {
assert_eq!(err.code, "invalid");
assert!(
err.message.contains("grants"),
"the field name is the correction: {}",
err.message
);
}
Ok(_) => panic!("an empty grant key is invalid"),
}
}
}