geth/crates/geth-auth/src/lib.rs

151 lines
4.6 KiB
Rust
Raw Normal View History

2026-05-15 15:08:20 +02:00
use geth_types::{AuthOpId, Capability, GroupId, PrincipalId, ResourceId, SecretId, UnixMillis};
use serde::{Deserialize, Serialize};
pub const AUTH_SIGNATURE_NAMESPACE: &str = "geth.auth-op.v1@geth.local";
pub const RESOURCE_GRANT_SIGNATURE_NAMESPACE: &str = "geth.resource-grant.v1@geth.local";
pub const REVOCATION_SIGNATURE_NAMESPACE: &str = "geth.revocation.v1@geth.local";
pub type SignedAuthOp = geth_codec::SignedEnvelope<AuthOp, PrincipalId>;
pub fn auth_signing_payload(op: &AuthOp) -> Result<Vec<u8>, geth_codec::CodecError> {
geth_codec::signing_payload(AUTH_SIGNATURE_NAMESPACE, op)
}
pub fn auth_signing_payload_hash(
op: &AuthOp,
) -> Result<geth_types::BlobHash, geth_codec::CodecError> {
geth_codec::signing_payload_hash(AUTH_SIGNATURE_NAMESPACE, op)
}
#[must_use]
pub fn signed_auth_op(op: AuthOp, signer: PrincipalId, signature: Vec<u8>) -> SignedAuthOp {
geth_codec::SignedEnvelope::new(AUTH_SIGNATURE_NAMESPACE, op, signer, signature)
}
2026-05-15 15:08:20 +02:00
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthOp {
pub id: AuthOpId,
pub resource: ResourceId,
pub created_at: UnixMillis,
pub kind: AuthOpKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum AuthOpKind {
ResourceCreate,
ResourceAuthoritySet {
authority: ResourceId,
},
GrantCreate {
grant_id: String,
principal: PrincipalId,
capabilities: Vec<Capability>,
},
GrantRevoke {
grant_id: String,
},
BearerAccessCreate {
secret: SecretId,
capabilities: Vec<Capability>,
expires_at: Option<UnixMillis>,
},
BearerAccessRevoke {
secret: SecretId,
},
GroupCreate {
group: GroupId,
},
GroupAddMember {
group: GroupId,
principal: PrincipalId,
},
GroupRemoveMember {
group: GroupId,
principal: PrincipalId,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthExplanation {
pub subject: String,
pub resource: String,
pub capability: String,
pub allowed: bool,
pub reason: String,
pub evaluated_ops: usize,
}
impl AuthExplanation {
#[must_use]
pub fn stub(subject: String, resource: String, capability: String) -> Self {
Self {
subject,
resource,
capability,
allowed: false,
reason: "authorization logs are scaffolded; no grant reducer is active yet".to_owned(),
evaluated_ops: 0,
}
}
2026-05-16 14:24:21 +02:00
#[must_use]
pub fn discovered_candidate(subject: String, resource: String, capability: String) -> Self {
Self {
subject,
resource,
capability,
allowed: false,
reason: "subject is a discovered peer candidate only; discovery does not grant trust or authorization".to_owned(),
evaluated_ops: 0,
}
}
2026-05-15 15:08:20 +02:00
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_structs_roundtrip() {
let op = AuthOp {
id: "op:auth:1".into(),
resource: "resource:notes".into(),
created_at: UnixMillis(10),
kind: AuthOpKind::GrantCreate {
grant_id: "grant:1".to_owned(),
principal: "node:laptop".into(),
capabilities: vec!["kv.read".into(), "kv.write_prefix:apps/foo/".into()],
},
};
let json = serde_json::to_string(&op).expect("json");
let decoded: AuthOp = serde_json::from_str(&json).expect("decode");
assert_eq!(decoded, op);
}
#[test]
fn auth_signing_payload_is_canonical_and_namespaced() {
let op = AuthOp {
id: "op:auth:1".into(),
resource: "resource:notes".into(),
created_at: UnixMillis(10),
kind: AuthOpKind::GrantCreate {
grant_id: "grant:1".to_owned(),
principal: "node:laptop".into(),
capabilities: vec!["kv.read".into(), "kv.write_prefix:apps/foo/".into()],
},
};
assert_eq!(
auth_signing_payload(&op).expect("payload"),
auth_signing_payload(&op).expect("payload again")
);
assert_ne!(
auth_signing_payload_hash(&op).expect("hash"),
geth_codec::hash_canonical(&op).expect("raw op hash")
);
let signed = signed_auth_op(op.clone(), "node:laptop".into(), vec![1, 2, 3]);
assert_eq!(signed.namespace(), AUTH_SIGNATURE_NAMESPACE);
assert_eq!(signed.payload(), &op);
}
2026-05-15 15:08:20 +02:00
}