use std::collections::BTreeMap;
use std::sync::Mutex;
use crate::auth::Access;
use gunnar_proto::{
AddWebhookRequest, BranchProtection, Commit, FileChange, Grant, GrantLevel, HashKind,
MirrorConfig, MirrorCredentialKind, MirrorState,
Namespace, Principal, PrincipalKind, PublicKey, PullRequest, ReadLfsObjectResponse,
ReadObjectResponse, RefNamePolicy,
RefTarget, ReplicationStatusResponse, ReplicationTarget, Repo, RepoPath,
IssueState, RepoStorage, ResyncReplicationResponse, ReviewState, SignedForgeEvent,
SignedKeyAttestation, SignedMembershipEvent, StorageInfoResponse, WebhookListResponse,
Visibility,
};
pub fn credential_arguments(
kind: MirrorCredentialKind,
secret: &str,
) -> ControlResult<()> {
match (kind, secret.is_empty()) {
(MirrorCredentialKind::Unspecified, true) => Ok(()),
(MirrorCredentialKind::Unspecified, false) => Err(ControlError::InvalidArgument(
"a credential value was given with no kind. `kind = UNSPECIFIED` is how a stored \
credential is FORGOTTEN, and it must carry no value — otherwise \"set this token\" \
and \"forget my key\" are the same request".to_owned(),
)),
(_, true) => Err(ControlError::InvalidArgument(format!(
"a credential of kind {kind:?} was asked for with an empty value. To forget the \
stored credential, send `kind = UNSPECIFIED`"
))),
(_, false) => Ok(()),
}
}
#[derive(Debug, thiserror::Error)]
pub enum ControlError {
#[error("{0} not found")]
NotFound(String),
#[error("{0} already exists")]
AlreadyExists(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("failed precondition: {0}")]
FailedPrecondition(String),
#[error("resource exhausted: {0}")]
ResourceExhausted(String),
#[error("not implemented: {0}; the storage seam is not wired to gunnar-store yet")]
Unimplemented(&'static str),
#[error("internal error: {0}")]
Internal(String),
#[error("unavailable: {0}")]
Unavailable(String),
#[error("unauthenticated: {0}")]
Unauthenticated(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
}
pub type ControlResult<T> = Result<T, ControlError>;
pub type LogFetchProducer =
Box<dyn FnOnce(&mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<()> + Send>;
pub const LOG_PIECE_BYTES: usize = 1 << 20;
pub fn log_entry_pieces(
e: gunnar_proto::LogEntry,
sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>,
) -> ControlResult<()> {
log_entry_pieces_from(e, 0, sink)
}
pub fn log_entry_pieces_from(
e: gunnar_proto::LogEntry,
offset: u64,
sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>,
) -> ControlResult<()> {
let blob_len = e.blob.len() as u64;
let start = if offset <= blob_len { offset as usize } else { 0 };
let rest = &e.blob[start..];
let n = rest.len().div_ceil(LOG_PIECE_BYTES).max(1);
let mut meta = Some(e.meta);
for i in 0..n {
let piece = &rest[(i * LOG_PIECE_BYTES).min(rest.len())..((i + 1) * LOG_PIECE_BYTES).min(rest.len())];
sink(gunnar_proto::LogChunk {
seq: e.seq,
kind: e.kind,
meta: meta.take().unwrap_or_default(),
blob_len,
blob: piece.to_vec(),
last: i + 1 == n,
restore_head: 0,
restore_bytes: 0,
log_id: String::new(),
offset: Some((start + (i * LOG_PIECE_BYTES).min(rest.len())) as u64),
entry_crc: None,
snapshot_item: None,
snapshot_id: String::new(),
})?;
}
Ok(())
}
pub trait RestoreIntake: Send {
fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()>;
fn end_of_stream(&mut self) -> ControlResult<()>;
fn counts(&self) -> (u64, u64);
}
struct GatherIntake<'a, P: ?Sized> {
plane: &'a P,
next: u64,
open: Option<gunnar_proto::LogEntry>,
want_len: u64,
applied: u64,
lost: u64,
}
impl<P: ControlPlane + ?Sized> RestoreIntake for GatherIntake<'_, P> {
fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()> {
if chunk.seq == 0 {
if let Some(e) = &self.open {
return Err(ControlError::InvalidArgument(format!("a snapshot piece arrived with seq {} unfinished", e.seq)));
}
if let Some(at) = self.plane.restore_snapshot_chunk(chunk)? {
self.next = at + 1;
}
return Ok(());
}
let entry = match self.open.as_mut() {
Some(e) if e.seq == chunk.seq => e,
Some(e) => return Err(ControlError::InvalidArgument(format!("seq {} arrived with seq {} unfinished", chunk.seq, e.seq))),
None => {
self.want_len = chunk.blob_len;
self.open.insert(gunnar_proto::LogEntry { seq: chunk.seq, kind: chunk.kind, meta: chunk.meta.clone(), blob: Vec::with_capacity(gunnar_proto::reserve_for(chunk.blob_len)) })
}
};
entry.blob.extend_from_slice(&chunk.blob);
if !chunk.last {
return Ok(());
}
let done = self.open.take().expect("open entry");
if done.blob.len() as u64 != self.want_len {
return Err(ControlError::InvalidArgument(format!("seq {}: {} of {} blob bytes arrived", done.seq, done.blob.len(), self.want_len)));
}
if done.seq < self.next {
return Ok(());
}
if done.seq > self.next {
return Err(ControlError::FailedPrecondition(format!("seq {} arrived where {} was due; resend from {}", done.seq, self.next, self.next)));
}
if self.plane.restore_apply(done)? {
self.lost += 1;
}
self.applied += 1;
self.next += 1;
Ok(())
}
fn end_of_stream(&mut self) -> ControlResult<()> {
if self.open.is_some() {
return Err(ControlError::InvalidArgument("the stream ended inside an entry; resend from the box's head + 1".into()));
}
Ok(())
}
fn counts(&self) -> (u64, u64) {
(self.applied, self.lost)
}
}
pub struct TransferPermit(pub Box<dyn TransferHold>);
pub trait TransferHold: Send {
fn entered_service(&self);
}
#[derive(Clone, Debug)]
pub struct TransferRefused {
pub message: String,
pub in_flight: u64,
pub peak_in_flight: u64,
pub limit: usize,
pub reserved_bytes: u64,
pub refused_total: u64,
}
pub trait TransferGate: Send + Sync {
fn admit_transfer(
&self,
principal: &str,
namespace: &str,
) -> Result<TransferPermit, TransferRefused>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredCredential {
pub credential: gunnar_proto::WebCredential,
pub public_key: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewInvite {
pub id: String,
pub principal_id: String,
pub created_by: String,
pub created_at_unix_ms: i64,
pub expires_at_unix_ms: i64,
pub note: String,
pub server_level: GrantLevel,
pub secret_hash: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PrincipalSeen {
pub last_sign_in_unix_ms: Option<i64>,
pub last_operation_unix_ms: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InviteCheck {
Valid(gunnar_proto::Invite),
Used,
Expired,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebLoginCommit {
pub credential_id: Vec<u8>,
pub sign_count: u32,
pub now_unix_ms: i64,
pub session_openssh: String,
pub comment: String,
pub expires_at_unix_ms: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebLoginOutcome {
pub principal: Principal,
pub credential: gunnar_proto::WebCredential,
pub fingerprint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebRegistration {
pub invite_secret_hash: Option<String>,
pub principal_id: Option<String>,
pub credential_id: Vec<u8>,
pub public_key: Vec<u8>,
pub label: String,
pub sign_count: u32,
pub now_unix_ms: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MachineApproval {
pub machine: String,
pub server_level: GrantLevel,
pub credential_id: Vec<u8>,
pub public_key: Vec<u8>,
pub label: String,
pub sign_count: u32,
pub now_unix_ms: i64,
pub step_up: Option<(Vec<u8>, u32)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MachineApproved {
pub principal: Principal,
pub credential: gunnar_proto::WebCredential,
pub created: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebRegistered {
pub credential: gunnar_proto::WebCredential,
pub principal: Principal,
pub server_level: GrantLevel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisteredKey {
pub principal_id: String,
pub openssh: String,
pub machine: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertificateIdentity {
pub principal_id: String,
pub fingerprint: String,
}
pub trait Identity: Send + Sync {
fn registered_key(&self, fingerprint: &str) -> ControlResult<Option<RegisteredKey>>;
fn awaiting_enrolment(&self, _fingerprint: &str) -> Option<String> {
None
}
fn registered_certificate(&self, der: &[u8]) -> ControlResult<Option<CertificateIdentity>>;
fn may(&self, principal_id: &str, access: &Access, level: GrantLevel) -> ControlResult<bool>;
fn may_each_repo(&self, principal_id: &str, paths: &[RepoPath], level: GrantLevel) -> ControlResult<Vec<bool>> {
paths.iter().map(|p| self.may(principal_id, &Access::repo(p), level)).collect()
}
fn narrowed_methods(&self, _principal_id: &str) -> ControlResult<Option<Vec<String>>> {
Ok(None)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BootstrapState {
pub window_open: bool,
pub bootstrapped: bool,
pub bootstrapped_at_unix_ms: i64,
pub bootstrapped_principal: String,
pub bootstrapped_fingerprint: String,
pub principals: u64,
pub keys: u64,
pub serving_git: bool,
pub readiness: String,
pub build: String,
pub role: String,
pub recharge_from: String,
pub restoring: bool,
pub log_head_seq: u64,
pub restore_partial: Option<(u64, u64)>,
pub restore_snapshot_resumable: bool,
pub restore_snapshot: Option<(String, u64, u64, bool)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Enrolled {
pub principal: Principal,
pub key: PublicKey,
pub account: String,
pub bootstrapped_at_unix_ms: i64,
pub client_cert_pem: String,
pub client_key_pem: String,
pub issuer_cert_pem: String,
}
pub trait FirstBoot: Send + Sync {
fn bootstrap_state(&self) -> ControlResult<BootstrapState>;
fn enroll_first_identity(
&self,
name: &str,
openssh_public_key: &str,
) -> ControlResult<Enrolled>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ControlCounts {
pub namespaces: u64,
pub repos: u64,
pub principals: u64,
pub keys: u64,
pub grants: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IndexProgressRow {
pub namespace: String,
pub name: String,
pub pack_id: u64,
pub objects_done: u64,
pub objects_total: u64,
pub bytes_done: u64,
pub bytes_total: u64,
pub phase: String,
pub started_at_unix_ms: i64,
pub resumed: bool,
pub reinflations: u64,
pub pauses: u64,
}
impl IndexProgressRow {
pub fn percent(&self) -> u32 {
(self.objects_done * 100)
.checked_div(self.objects_total)
.map_or(0, |percent| percent.min(100) as u32)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergedRef {
pub ref_name: String,
pub old_oid: String,
pub new_oid: String,
}
#[derive(Debug, Clone)]
pub struct NewRepo {
pub path: RepoPath,
pub hash_kind: HashKind,
pub default_branch: String,
pub visibility: Visibility,
pub description: String,
pub adopt: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DeleteOptions {
pub purge: bool,
pub retention_days: Option<u32>,
pub principal_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeleteOutcome {
Purged {
bytes_reclaimed: u64,
},
Tombstoned {
purge_after_unix_ms: i64,
bytes_kept: u64,
},
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StatsExtras {
pub tombstoned_repos: u64,
pub tombstoned_bytes: u64,
pub replication_configured: bool,
pub replication_enqueued: u64,
pub replication_synced: u64,
pub replication_failed: u64,
pub replication_bus_gaps: u64,
pub replication_refs_total: u64,
pub replication_refs_unmirrored: u64,
pub standbys: Vec<gunnar_proto::StandbyStatus>,
pub tenants_paid: u64,
pub tenants_grace: u64,
pub tenants_suspended: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeletedRepoRow {
pub path: RepoPath,
pub deleted_at_unix_ms: i64,
pub purge_after_unix_ms: i64,
pub principal_id: String,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrphanRow {
pub path: RepoPath,
pub dir: String,
pub bytes: u64,
pub hash_marker: String,
pub modified_unix_ms: i64,
pub failed_reclaim: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ArchiveCut {
pub from_generation: u64,
pub to_generation: u64,
pub bytes_out: u64,
pub file_sha256: [u8; 32],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GcState {
Queued,
Running,
Done,
Failed,
}
#[derive(Debug, Clone)]
pub struct GcJob {
pub job_id: String,
pub path: RepoPath,
pub state: GcState,
pub queue_position: u32,
pub bytes_reclaimed: u64,
pub error: Option<String>,
pub enqueued_at_unix_ms: i64,
pub finished_at_unix_ms: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateReason {
Administrative,
Purge,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperatorClaim {
pub principal: Principal,
pub created: bool,
pub adopted_namespace: bool,
pub named_by: &'static str,
}
pub const OPERATOR_INVITE_ID: &str = "operator-bootstrap";
pub const OPERATOR_INVITE_TTL_MS: i64 = 30 * 24 * 60 * 60 * 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootstrapInvite {
Recorded,
Renewed,
Spent,
}
pub const OPERATOR_INVITE_MIN_TTL_MS: i64 = 60 * 1000;
pub fn bootstrap_ttl(ttl_ms: i64) -> ControlResult<i64> {
if (OPERATOR_INVITE_MIN_TTL_MS..=OPERATOR_INVITE_TTL_MS).contains(&ttl_ms) {
Ok(ttl_ms)
} else {
Err(ControlError::InvalidArgument(format!(
"the first invite's lifetime is {ttl_ms} ms: it lives at least a minute and at most {} days",
OPERATOR_INVITE_TTL_MS / 86_400_000
)))
}
}
pub fn bootstrap_hash(raw: &str) -> ControlResult<String> {
let hash = raw.trim();
if hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
Ok(hash.to_owned())
} else {
Err(ControlError::InvalidArgument(
"the bootstrap invite is the lowercase hex SHA-256 of the break-glass token (64 \
characters), never the token itself"
.into(),
))
}
}
pub fn operator_name<'a>(
fed: Option<&'a str>,
asked: &'a str,
) -> ControlResult<(&'a str, &'static str)> {
match (fed.map(str::trim).filter(|n| !n.is_empty()), asked.trim()) {
(Some(fed), _) => Ok((fed, "fed")),
(None, asked) if !asked.is_empty() => Ok((asked, "asked")),
_ => Err(ControlError::FailedPrecondition(
"no operator is named for this box: the installer fed no `operator_name` and the \
request named nobody"
.into(),
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGate {
pub account: String,
pub reason: GateReason,
pub holder: String,
pub actor: String,
pub note: String,
pub at_unix_ms: i64,
pub expires_unix_ms: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PurgeState {
Queued,
Running,
Done,
Failed,
}
#[derive(Debug, Clone)]
pub struct PurgeJob {
pub job_id: String,
pub account: String,
pub state: PurgeState,
pub queue_position: u32,
pub stores_found: u32,
pub stores_purged: u32,
pub bytes_reclaimed: u64,
pub principal_removed: bool,
pub gate_released: bool,
pub error: Option<String>,
pub failures: Vec<String>,
pub enqueued_at_unix_ms: i64,
pub started_at_unix_ms: i64,
pub finished_at_unix_ms: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EmployeeAsk {
pub id: String,
pub name: String,
pub room_bytes: u64,
pub seats: u32,
pub from_seat: bool,
pub cap: Option<u64>,
pub stored: u64,
pub order: String,
pub dry_run: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct EmployeeAnswer {
pub principal: Option<Principal>,
pub room_bytes: u64,
pub seats: u32,
pub seats_open: u32,
pub need_bytes: u64,
pub unused_bytes: Option<u64>,
pub shortfall_bytes: u64,
pub unchanged: bool,
}
#[derive(Debug, Clone, Default)]
pub struct PurgeAccountOptions {
pub principal_id: String,
pub reason: String,
pub remove_principal: bool,
pub expect_empty: bool,
}
pub trait ControlPlane: Identity + FirstBoot + Send + Sync + 'static {
fn create_namespace(&self, name: &str, description: &str) -> ControlResult<Namespace>;
fn list_namespaces(&self) -> ControlResult<Vec<Namespace>>;
fn namespace(&self, name: &str) -> ControlResult<Option<Namespace>> {
Ok(self
.list_namespaces()?
.into_iter()
.find(|namespace| namespace.name == name))
}
fn namespace_for_outsider(&self, principal_id: &str, name: &str) -> ControlResult<Option<Namespace>> {
let _ = principal_id;
self.namespace(name)
}
fn set_namespace_settings(
&self,
name: &str,
public: Option<bool>,
events_public: Option<bool>,
) -> ControlResult<Namespace>;
fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32>;
fn create_repo(&self, spec: NewRepo) -> ControlResult<Repo>;
fn create_room(&self, _namespace: &str, _machine: bool) -> ControlResult<()> {
Ok(())
}
fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo>;
fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>>;
fn delete_repo(&self, path: &RepoPath, options: DeleteOptions) -> ControlResult<DeleteOutcome>;
fn undelete_repo(&self, path: &RepoPath) -> ControlResult<Repo> {
let _ = path;
Err(ControlError::Unimplemented("Repos.Undelete"))
}
fn purge_repo(&self, path: &RepoPath) -> ControlResult<u64> {
let _ = path;
Err(ControlError::Unimplemented("Repos.Purge"))
}
fn gc_repo(&self, path: &RepoPath) -> ControlResult<GcJob> {
let _ = path;
Err(ControlError::Unimplemented("Repos.Gc"))
}
fn gc_status(&self, path: &RepoPath) -> ControlResult<Option<GcJob>> {
let _ = path;
Err(ControlError::Unimplemented("Repos.GcStatus"))
}
fn list_deleted(&self, namespace: Option<&str>) -> ControlResult<Vec<DeletedRepoRow>> {
let _ = namespace;
Err(ControlError::Unimplemented("Repos.ListDeleted"))
}
fn list_orphans(&self, namespace: Option<&str>) -> ControlResult<Vec<OrphanRow>> {
let _ = namespace;
Err(ControlError::Unimplemented("Repos.ListOrphans"))
}
fn reclaim_orphan(&self, path: &RepoPath) -> ControlResult<u64> {
let _ = path;
Err(ControlError::Unimplemented("Repos.ReclaimOrphan"))
}
fn set_default_branch(&self, path: &RepoPath, default_branch: &str) -> ControlResult<Repo>;
fn set_visibility(&self, path: &RepoPath, visibility: Visibility) -> ControlResult<Repo>;
fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo> {
let _ = (path, encrypted);
Err(ControlError::Unimplemented("Repos.SetEncrypted"))
}
fn recipient_keys(&self, principal_id: &str) -> ControlResult<Vec<gunnar_proto::RecipientKey>> {
let _ = principal_id;
Err(ControlError::Unimplemented("Repos.RecipientKeys"))
}
fn encryption_key(&self, principal_id: &str) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
let _ = principal_id;
Err(ControlError::Unimplemented("Access.GetEncryptionKey"))
}
fn create_encryption_key(
&self,
principal_id: &str,
public_key: &str,
first: gunnar_proto::UnlockMethod,
) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
let _ = (principal_id, public_key, first);
Err(ControlError::Unimplemented("Access.CreateEncryptionKey"))
}
fn add_unlock_method(&self, principal_id: &str, method: gunnar_proto::UnlockMethod) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
let _ = (principal_id, method);
Err(ControlError::Unimplemented("Access.AddUnlockMethod"))
}
fn remove_unlock_method(&self, principal_id: &str, id: &str) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
let _ = (principal_id, id);
Err(ControlError::Unimplemented("Access.RemoveUnlockMethod"))
}
fn gate_account(
&self,
account: &str,
actor: &str,
note: &str,
) -> ControlResult<(AccountGate, bool)> {
let _ = (account, actor, note);
Err(ControlError::Unimplemented("Accounts.Gate"))
}
fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>> {
let _ = account;
Err(ControlError::Unimplemented("Accounts.Ungate"))
}
fn account_gates(&self) -> ControlResult<Vec<AccountGate>> {
Err(ControlError::Unimplemented("Accounts.ListAccountGates"))
}
fn purge_account(
&self,
account: &str,
options: PurgeAccountOptions,
) -> ControlResult<(PurgeJob, AccountGate)> {
let _ = (account, options);
Err(ControlError::Unimplemented("Accounts.Purge"))
}
fn purge_account_status(
&self,
account: &str,
) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)> {
let _ = account;
Err(ControlError::Unimplemented("Accounts.PurgeStatus"))
}
fn archive_out(
&self,
path: &RepoPath,
from_generation: u64,
sink: &mut dyn std::io::Write,
) -> ControlResult<ArchiveCut> {
let _ = (path, from_generation, sink);
Err(ControlError::Unimplemented("Repos.ArchiveOut"))
}
fn list_refs(&self, path: &RepoPath, prefix: &str, peel: bool)
-> ControlResult<Vec<RefTarget>>;
fn get_ref(&self, path: &RepoPath, ref_name: &str, peel: bool) -> ControlResult<RefTarget>;
fn log(
&self,
path: &RepoPath,
start: &str,
limit: u32,
touching: &str,
) -> ControlResult<Vec<Commit>>;
fn log_all_refs(
&self,
path: &RepoPath,
limit: u32,
touching: &str,
) -> ControlResult<Vec<Commit>> {
let _ = (path, limit, touching);
Err(ControlError::Unimplemented("Refs.Log all_refs"))
}
fn create_pull(
&self,
path: &RepoPath,
author: &str,
title: &str,
description: &str,
source_ref: &str,
target_ref: &str,
) -> ControlResult<PullRequest>;
fn list_pulls(&self, path: &RepoPath, state: i32) -> ControlResult<Vec<PullRequest>>;
fn get_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
fn close_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
fn merge_pull(
&self,
path: &RepoPath,
number: u64,
principal_id: &str,
) -> ControlResult<(PullRequest, MergedRef)>;
fn write_first_commit(&self, path: &RepoPath, readme: &str) -> ControlResult<String> {
let _ = readme;
Err(ControlError::FailedPrecondition(format!("this control plane writes no commit into {path}: it keeps no git store")))
}
fn read_object(
&self,
path: &RepoPath,
oid: &str,
max_bytes: u64,
) -> ControlResult<ReadObjectResponse>;
fn read_lfs_object(
&self,
path: &RepoPath,
oid: &str,
max_bytes: u64,
range: Option<(u64, u64)>,
) -> ControlResult<ReadLfsObjectResponse> {
let _ = (path, oid, max_bytes, range);
Err(ControlError::Unimplemented("Refs.ReadLfsObject"))
}
fn diff(
&self,
path: &RepoPath,
from: &str,
to: &str,
path_prefixes: &[String],
) -> ControlResult<Vec<FileChange>>;
fn blame(
&self,
path: &RepoPath,
rev: &str,
file: &str,
) -> ControlResult<gunnar_proto::BlameResponse> {
let _ = (path, rev, file);
Err(ControlError::Unimplemented("Refs.Blame"))
}
fn search(
&self,
path: &RepoPath,
rev: &str,
pattern: &str,
max_file_bytes: u64,
max_matches: u32,
) -> ControlResult<gunnar_proto::SearchResponse> {
let _ = (path, rev, pattern, max_file_bytes, max_matches);
Err(ControlError::Unimplemented("Refs.Search"))
}
fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind)
-> ControlResult<Principal>;
fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer>;
fn employees(&self, account: &str) -> ControlResult<Vec<Principal>>;
fn has_employees(&self, account: &str) -> ControlResult<bool> {
Ok(!self.employees(account)?.is_empty())
}
fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
let _ = account;
Ok((0, 0))
}
fn clans(&self) -> Option<&dyn crate::clans::ClanPlane> {
None
}
fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
let _ = asked;
Err(ControlError::Unimplemented("Accounts.ClaimOperator"))
}
fn operator_bootstrap_invite(
&self,
operator: &str,
secret_sha256: &str,
now_unix_ms: i64,
ttl_ms: i64,
) -> ControlResult<BootstrapInvite> {
let _ = (operator, secret_sha256, now_unix_ms, ttl_ms);
Err(ControlError::Unimplemented("Accounts.ClaimOperator (bootstrap invite)"))
}
fn list_principals(&self) -> ControlResult<Vec<Principal>>;
fn principal_activity(&self) -> ControlResult<std::collections::BTreeMap<String, PrincipalSeen>> {
Ok(std::collections::BTreeMap::new())
}
fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>;
fn principal(&self, id: &str) -> ControlResult<Option<Principal>> {
Ok(self.list_principals()?.into_iter().find(|p| p.id == id))
}
fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
let _ = principal_id;
Ok(false)
}
fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal>;
fn add_credential(
&self,
principal_id: &str,
credential_id: &[u8],
public_key: &[u8],
label: &str,
sign_count: u32,
) -> ControlResult<gunnar_proto::WebCredential> {
let _ = (principal_id, credential_id, public_key, label, sign_count);
Err(ControlError::Unimplemented("Access.AddCredential"))
}
fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
let _ = principal_id;
Err(ControlError::Unimplemented("Access.ListCredentials"))
}
fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
let _ = credential_id;
Err(ControlError::Unimplemented("Access.RevokeCredential"))
}
fn rename_credential(
&self,
credential_id: &[u8],
label: &str,
) -> ControlResult<Option<gunnar_proto::WebCredential>> {
let _ = (credential_id, label);
Err(ControlError::Unimplemented("Access.RenameCredential"))
}
fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
let _ = invite;
Err(ControlError::Unimplemented("Access.CreateInvite"))
}
fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
Err(ControlError::Unimplemented("Access.ListInvites"))
}
fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
let _ = id;
Err(ControlError::Unimplemented("Access.RevokeInvite"))
}
fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
let _ = (secret_hash, now_unix_ms);
Err(ControlError::Unimplemented("Access.CheckInvite"))
}
fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
let _ = commit;
Err(ControlError::Unimplemented("Access.WebLogin"))
}
fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
let _ = registration;
Err(ControlError::Unimplemented("Access.WebRegister"))
}
fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
let _ = approval;
Err(ControlError::Unimplemented("Access.ApproveMachineEnrolment"))
}
fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
let _ = principal_id;
Err(ControlError::Unimplemented("Accounts.EnrollEmployee"))
}
fn sso(&self) -> Option<&dyn crate::sso::plane::SsoPlane> {
None
}
fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
let _ = credential_id;
Err(ControlError::Unimplemented("Access.RevokeCredential"))
}
fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)>;
#[allow(clippy::too_many_arguments)]
fn add_key(
&self,
principal_id: &str,
openssh_public_key: &str,
comment: &str,
expires_at_unix_ms: Option<i64>,
caller: &str,
attestation: Option<SignedKeyAttestation>,
) -> ControlResult<PublicKey>;
fn revoke_key(&self, fingerprint: &str) -> ControlResult<()>;
fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>>;
fn grant(
&self,
principal_id: &str,
namespace: &str,
repo: &str,
level: GrantLevel,
) -> ControlResult<Grant>;
fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()>;
fn list_grants(
&self,
principal_id: Option<&str>,
namespace: Option<&str>,
repo: Option<&str>,
) -> ControlResult<Vec<Grant>>;
fn add_group_member(
&self,
group_id: &str,
member_id: &str,
signed: SignedMembershipEvent,
) -> ControlResult<u64> {
let _ = (group_id, member_id, signed);
Err(ControlError::Unimplemented("Access.AddGroupMember"))
}
fn remove_group_member(
&self,
group_id: &str,
member_id: &str,
signed: SignedMembershipEvent,
) -> ControlResult<(bool, u64)> {
let _ = (group_id, member_id, signed);
Err(ControlError::Unimplemented("Access.RemoveGroupMember"))
}
fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>> {
let _ = group_id;
Err(ControlError::Unimplemented("Access.ListGroupMembers"))
}
fn membership_events(
&self,
group_id: Option<&str>,
) -> ControlResult<Vec<SignedMembershipEvent>> {
let _ = group_id;
Err(ControlError::Unimplemented("Access.ListGroupMembershipEvents"))
}
fn get_protection(
&self,
path: &RepoPath,
ref_pattern: Option<&str>,
) -> ControlResult<Vec<BranchProtection>>;
fn set_protection(
&self,
path: &RepoPath,
protection: BranchProtection,
) -> ControlResult<BranchProtection>;
fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()>;
fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy>;
fn set_ref_name_policy(
&self,
path: &RepoPath,
policy: RefNamePolicy,
) -> ControlResult<RefNamePolicy>;
fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>;
fn forge_review(
&self,
repo: &str,
pull: u64,
) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)>;
fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>;
fn forge_issue(
&self,
repo: &str,
number: u64,
) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>;
fn forge_issues(&self, repo: &str) -> ControlResult<Vec<IssueState>>;
fn counts(&self) -> ControlResult<ControlCounts>;
fn stats_extras(&self) -> ControlResult<StatsExtras> {
Ok(StatsExtras::default())
}
fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse>;
fn issuer_info(&self) -> ControlResult<gunnar_proto::IssuerInfoResponse> {
Err(ControlError::Unimplemented("Admin.IssuerInfo"))
}
fn audit_trail(&self, _at_seq: u64) -> ControlResult<gunnar_proto::AuditTrailResponse> {
Err(ControlError::Unimplemented("Admin.AuditTrail"))
}
fn setting(&self, _name: &str) -> ControlResult<Option<Vec<u8>>> {
Err(ControlError::Unimplemented("Admin.GetSetting"))
}
fn set_setting(&self, _name: &str, _value: &[u8]) -> ControlResult<Option<Vec<u8>>> {
Err(ControlError::Unimplemented("Admin.SetSetting"))
}
fn capacity(
&self,
_request: &gunnar_proto::CapacityRequest,
) -> ControlResult<gunnar_proto::CapacityResponse> {
Err(ControlError::Unimplemented("Admin.Capacity"))
}
fn grow_start(
&self,
_request: &gunnar_proto::GrowStartRequest,
) -> ControlResult<gunnar_proto::GrowStatusResponse> {
Err(ControlError::Unimplemented(
"Admin.GrowStart: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
))
}
fn grow_status(&self) -> ControlResult<gunnar_proto::GrowStatusResponse> {
Ok(gunnar_proto::GrowStatusResponse {
phase: "idle".to_owned(),
engine_present: false,
serving: true,
server_unix_ms: crate::bus::now_unix_ms(),
detail: "this control plane has no runtime grow engine; the data set grows only at a restart, if at all".to_owned(),
..Default::default()
})
}
fn grow_resume(&self, _give_up: bool) -> ControlResult<gunnar_proto::GrowStatusResponse> {
Err(ControlError::Unimplemented(
"Admin.GrowResume: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
))
}
fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
let _ = namespace;
Err(ControlError::Unimplemented("Usage"))
}
fn repo_usage(&self, namespace: &str) -> ControlResult<Vec<gunnar_proto::RepoStored>> {
let _ = namespace;
Err(ControlError::Unimplemented("Usage"))
}
fn usage_page(
&self,
page_token: &str,
page_size: usize,
changed_since: &str,
) -> ControlResult<gunnar_proto::ReadManyUsageResponse> {
let _ = changed_since;
let (_, after) = usage_page_token::decode(page_token);
let mut rows = self.usage(None)?;
rows.sort_by(|a, b| a.namespace.cmp(&b.namespace));
Ok(usage_page_token::page(rows, &after, page_size, String::new(), false))
}
fn activity(
&self,
namespace: &str,
since_unix_ms: i64,
until_unix_ms: i64,
) -> ControlResult<(Vec<gunnar_proto::DayActivity>, i64)> {
let _ = (namespace, since_unix_ms, until_unix_ms);
Err(ControlError::Unimplemented("Usage.Activity"))
}
fn admit_plan(&self, namespace: &str) -> ControlResult<gunnar_proto::AdmitPlanResponse> {
let _ = namespace;
Err(ControlError::Unimplemented("Usage.AdmitPlan"))
}
#[cfg(feature = "metered")]
fn entitlement_get(
&self,
namespace: &str,
) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
let _ = namespace;
Err(ControlError::Unimplemented("Entitlement.Get"))
}
#[cfg(feature = "metered")]
fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
Err(ControlError::Unimplemented("Entitlement.List"))
}
#[cfg(feature = "metered")]
fn entitlement_refused_at_boot(&self) -> ControlResult<Option<gunnar_proto::RefusedAtBoot>> {
Ok(None)
}
#[cfg(feature = "metered")]
fn entitlement_set(
&self,
fact: gunnar_proto::EntitlementFact,
) -> ControlResult<(gunnar_proto::EntitlementState, u64)> {
let _ = fact;
Err(ControlError::Unimplemented("Entitlement.Set"))
}
#[cfg(feature = "metered")]
fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
Err(ControlError::Unimplemented("Billing.ApplianceIdentity"))
}
#[cfg(feature = "metered")]
fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
let _ = message;
Err(ControlError::Unimplemented("Billing.MintActorTicket"))
}
#[cfg(feature = "metered")]
fn sealed_get(&self, name: &str) -> ControlResult<Option<(String, Vec<u8>)>> {
let _ = name;
Err(ControlError::Unimplemented("Sealed.Get"))
}
#[cfg(feature = "metered")]
fn sealed_put(&self, name: &str, owner: &str, value: &[u8]) -> ControlResult<bool> {
let _ = (name, owner, value);
Err(ControlError::Unimplemented("Sealed.Put"))
}
fn verify_chain(
&self,
request: &gunnar_proto::VerifyChainRequest,
) -> ControlResult<gunnar_proto::VerifyChainResponse> {
let _ = request;
Err(ControlError::Unimplemented("Admin.VerifyChain"))
}
fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>> {
Ok(Vec::new())
}
fn mirror_add(
&self,
path: &RepoPath,
remote_url: &str,
prune: bool,
create_repo: bool,
) -> ControlResult<MirrorConfig>;
fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig>;
fn mirror_set_credential(
&self,
path: &RepoPath,
kind: MirrorCredentialKind,
secret: &str,
username: &str,
) -> ControlResult<MirrorConfig>;
fn mirror_status(
&self,
namespace: Option<&str>,
path: Option<&RepoPath>,
) -> ControlResult<Vec<MirrorConfig>>;
fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()>;
fn replication_set(
&self,
_target: ReplicationTarget,
) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"this server was built without replication (`--features replication`), so it cannot be given a standby",
))
}
fn replication_remove(&self, _name: &str) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"this server was built without replication (`--features replication`), so it has no standby to remove",
))
}
fn replication_demote(&self) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"this server was built without replication (`--features replication`), so it has no standby to demote",
))
}
fn replication_set_role(
&self,
_role: gunnar_proto::ReplicationRole,
) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"this server was built without replication (`--features replication`), so it has no role to set",
))
}
fn replication_abdicate(&self) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"this server was built without replication (`--features replication`), so it cannot abdicate",
))
}
fn replication_status(
&self,
_include_refs: bool,
) -> ControlResult<ReplicationStatusResponse> {
Err(ControlError::Unimplemented(
"replication: this server was built without the `replication` feature",
))
}
fn replication_resync(
&self,
_name: &str,
_repo: Option<&RepoPath>,
_except: &[RepoPath],
) -> ControlResult<ResyncReplicationResponse> {
Err(ControlError::Unimplemented(
"replication: this server was built without the `replication` feature, so it has no standby to backfill",
))
}
fn replication_flush(
&self,
_request: &gunnar_proto::FlushReplicationRequest,
) -> ControlResult<gunnar_proto::FlushReplicationResponse> {
Err(ControlError::Unimplemented(
"replication: this server was built without the `replication` feature, so it has no standby to flush to",
))
}
fn log_head(&self) -> ControlResult<gunnar_proto::LogHeadResponse> {
Err(ControlError::Unimplemented(
"transaction log: this control plane keeps none",
))
}
fn log_fetch(
&self,
_from: u64,
_to: u64,
_max_bytes: u64,
) -> ControlResult<Vec<gunnar_proto::LogEntry>> {
Err(ControlError::Unimplemented(
"transaction log: this control plane keeps none",
))
}
fn log_fetch_stream(&self, from: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
let entries = self.log_fetch(from, to, max_bytes)?;
Ok(Box::new(move |sink| {
for e in entries {
log_entry_pieces(e, sink)?;
}
Ok(())
}))
}
fn log_fetch_stream_at(&self, from: u64, from_offset: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
if from_offset == 0 {
return self.log_fetch_stream(from, to, max_bytes);
}
let entries = self.log_fetch(from, to, max_bytes)?;
Ok(Box::new(move |sink| {
for e in entries {
let at = if e.seq == from { from_offset } else { 0 };
log_entry_pieces_from(e, at, sink)?;
}
Ok(())
}))
}
fn primary_heartbeat(&self, _silent_after_ms: u64) -> Option<gunnar_proto::PrimaryHeartbeat> {
None
}
fn topology_facts(&self) -> crate::topology::Facts {
crate::topology::Facts::default()
}
fn is_twin(&self) -> bool {
false
}
fn log_follows(&self) -> bool {
false
}
fn health_alarms(&self) -> Vec<gunnar_proto::HealthAlarm> {
Vec::new()
}
fn maintenance_reasons(&self) -> Vec<String> {
Vec::new()
}
fn is_group_member(&self, _principal_id: &str, _group: &str) -> ControlResult<bool> {
Ok(false)
}
fn become_twin(&self, _primary: &str) -> ControlResult<String> {
Err(ControlError::Unimplemented("twin: this control plane cannot become a twin"))
}
fn assign_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
}
fn set_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
}
fn twin_csr(&self) -> ControlResult<String> {
Err(ControlError::Unimplemented("Bootstrap.TwinCsr"))
}
fn install_twin_certificate(&self, _cert_pem: &str, _ca_pem: &str, _replication_port: u16, _replication_host: &str) -> ControlResult<i64> {
Err(ControlError::Unimplemented("Bootstrap.InstallTwinCertificate"))
}
fn issue_twin_certificate(&self, _principal: &str, _csr_pem: &str) -> ControlResult<(String, String, u16, String)> {
Err(ControlError::Unimplemented("Admin.IssueTwinCertificate"))
}
fn log_head_signed(&self, _nonce: &str) -> ControlResult<gunnar_proto::LogHeadResponse> {
self.log_head()
}
fn fence_by_generation(&self, _proof: &gunnar_proto::GenerationProof) -> ControlResult<()> {
Ok(())
}
fn twin_git_listener_needs_restart(&self) -> bool {
false
}
fn restore_begin(&self, _owner: &str) -> ControlResult<u64> {
Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
}
fn restore_keys(&self) -> ControlResult<Vec<String>> {
Ok(Vec::new())
}
fn restore_room(&self, _announced_bytes: u64) -> ControlResult<()> {
Ok(())
}
fn restore_intake<'a>(&'a self, owner: &str) -> ControlResult<Option<Box<dyn RestoreIntake + 'a>>> {
let next = self.restore_begin(owner)?;
Ok(Some(Box::new(GatherIntake { plane: self, next, open: None, want_len: 0, applied: 0, lost: 0 })))
}
fn restore_apply(&self, _entry: gunnar_proto::LogEntry) -> ControlResult<bool> {
Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
}
fn restore_finish(&self, _announced_head: u64, _log_id_of_source: &str) -> ControlResult<u64> {
Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
}
fn snapshot_stream(&self, _sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
Err(ControlError::Unimplemented("snapshot: this control plane keeps no log"))
}
fn snapshot_stream_from(&self, from_item: u64, from_offset: u64, expect_id: &str, sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
if from_item == 0 && from_offset == 0 && expect_id.trim().is_empty() {
return self.snapshot_stream(sink);
}
Err(ControlError::Unimplemented("snapshot: this control plane cannot resume a snapshot"))
}
fn log_retain(&self, _from: u64) -> ControlResult<gunnar_proto::LogHeadResponse> {
Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
}
fn log_layout(&self) -> ControlResult<Vec<(u64, u64, u64)>> {
Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
}
fn data_volume(&self) -> Option<(u64, u64)> {
None
}
fn twin_owed_read(&self) -> ControlResult<Vec<u8>> {
Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
}
fn twin_owed_write(&self, _bytes: &[u8]) -> ControlResult<()> {
Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
}
fn restore_snapshot_chunk(&self, _chunk: &gunnar_proto::LogChunk) -> ControlResult<Option<u64>> {
Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
}
fn webhook_add(&self, _request: AddWebhookRequest) -> ControlResult<WebhookListResponse> {
Err(ControlError::Unimplemented(
"outbound webhooks: this server was built without the `webhooks` feature, so it cannot be given an outbound endpoint",
))
}
fn webhook_list(&self) -> ControlResult<WebhookListResponse> {
Err(ControlError::Unimplemented(
"outbound webhooks: this server was built without the `webhooks` feature, so it has no outbound endpoints",
))
}
fn webhook_remove(&self, _name: &str) -> ControlResult<WebhookListResponse> {
Err(ControlError::Unimplemented(
"outbound webhooks: this server was built without the `webhooks` feature, so it has no outbound endpoint to remove",
))
}
fn enterprise(&self) -> Option<&dyn crate::enterprise::EnterprisePlane> {
None
}
}
#[derive(Debug, Default)]
pub struct MemoryControlPlane {
inner: Mutex<Inner>,
stall_ms: std::sync::atomic::AtomicU64,
}
type SeatLot = (u64, u32, Vec<String>, String);
#[derive(Debug, Default)]
struct Inner {
namespaces: BTreeMap<String, Namespace>,
repos: BTreeMap<(String, String), Repo>,
principals: BTreeMap<String, Principal>,
employers: BTreeMap<String, String>,
rooms: BTreeMap<String, (u64, String)>,
seats: BTreeMap<String, Vec<SeatLot>>,
room_orders: BTreeMap<String, (String, String, u32, u64, u64)>,
usage: BTreeMap<String, gunnar_proto::NamespaceUsage>,
credentials: BTreeMap<String, StoredCredential>,
credential_sessions: BTreeMap<String, Vec<String>>,
invites: BTreeMap<String, (gunnar_proto::Invite, String)>,
redeemed_invites: std::collections::BTreeSet<String>,
keys: BTreeMap<String, PublicKey>,
grants: Vec<Grant>,
protections: BTreeMap<(String, String), BTreeMap<String, BranchProtection>>,
ref_policies: BTreeMap<(String, String), RefNamePolicy>,
mirrors: BTreeMap<(String, String), MirrorConfig>,
openssh: BTreeMap<String, String>,
certificates: BTreeMap<Vec<u8>, CertificateIdentity>,
server_grants: BTreeMap<String, GrantLevel>,
narrowed: BTreeMap<String, Vec<String>>,
forced_may: Option<bool>,
twin: bool,
log_follows: bool,
awaiting: BTreeMap<String, String>,
forged_principal_id: Option<String>,
bootstrap: Option<(i64, String, String)>,
#[cfg(feature = "metered")]
entitlements: BTreeMap<String, gunnar_proto::EntitlementFact>,
#[cfg(feature = "metered")]
actor_tickets_signed: u64,
#[cfg(feature = "metered")]
actor_ticket_key: Option<ed25519_dalek::SigningKey>,
}
impl Inner {
#[cfg(feature = "metered")]
fn actor_ticket_key(inner: &mut Inner) -> &ed25519_dalek::SigningKey {
inner.actor_ticket_key.get_or_insert_with(|| {
use rand::Rng as _;
let mut seed = [0u8; 32];
rand::rand_core::UnwrapErr(rand::rngs::SysRng).fill_bytes(&mut seed);
ed25519_dalek::SigningKey::from_bytes(&seed)
})
}
fn name_holder(inner: &Inner, name: &str, own_id: &str) -> Option<String> {
inner
.principals
.values()
.find(|p| p.id != own_id && (p.name == name || p.id == name))
.map(|p| p.id.clone())
}
}
pub const DEFAULT_BRANCH: &str = "refs/heads/main";
pub const DEFAULT_HASH_KIND: HashKind = HashKind::Sha256;
impl MemoryControlPlane {
pub fn set_twin(&self, yes: bool) {
self.inner.lock().expect("memory control plane").twin = yes;
}
pub fn set_log_follows(&self, yes: bool) {
self.inner.lock().expect("memory control plane").log_follows = yes;
}
pub fn await_enrolment(&self, fingerprint: &str, openssh: &str) {
self.inner.lock().expect("memory control plane").awaiting.insert(fingerprint.to_owned(), openssh.to_owned());
}
pub fn new() -> Self {
Self::default()
}
pub fn stall_bootstrap_state(&self, by: std::time::Duration) {
self.stall_ms.store(
by.as_millis().min(u64::MAX as u128) as u64,
std::sync::atomic::Ordering::SeqCst,
);
}
pub fn with_repo(namespace: &str, repo: &str) -> Self {
let plane = Self::new();
plane
.create_namespace(namespace, "")
.expect("fresh namespace");
plane
.create_repo(NewRepo {
path: RepoPath::new(namespace, repo),
hash_kind: DEFAULT_HASH_KIND,
default_branch: String::new(),
visibility: Visibility::Private,
description: String::new(),
adopt: false,
})
.expect("fresh repo");
plane
}
fn key(path: &RepoPath) -> (String, String) {
(path.namespace.clone(), path.repo.clone())
}
pub fn narrow(&self, principal_id: &str, methods: &[&str]) {
self.inner.lock().unwrap().narrowed.insert(principal_id.to_owned(), methods.iter().map(|m| m.to_string()).collect());
}
pub fn grant_server(&self, principal_id: &str, level: GrantLevel) {
self.inner
.lock()
.unwrap()
.server_grants
.insert(principal_id.to_owned(), level);
}
pub fn set_usage(&self, usage: gunnar_proto::NamespaceUsage) {
self.inner
.lock()
.unwrap()
.usage
.insert(usage.namespace.clone(), usage);
}
#[cfg(feature = "metered")]
pub fn set_entitlement_fact(&self, fact: gunnar_proto::EntitlementFact) {
self.inner
.lock()
.unwrap()
.entitlements
.insert(fact.namespace.clone(), fact);
}
#[cfg(feature = "metered")]
pub fn actor_tickets_signed(&self) -> u64 {
self.inner.lock().unwrap().actor_tickets_signed
}
pub fn force_may(&self, forced: Option<bool>) {
self.inner.lock().unwrap().forced_may = forced;
}
pub fn forge_principal_id(&self, id: Option<&str>) {
self.inner.lock().unwrap().forged_principal_id = id.map(str::to_owned);
}
}
fn level_rank(level: GrantLevel) -> u8 {
match level {
GrantLevel::Unspecified => 0,
GrantLevel::Read => 1,
GrantLevel::Write => 2,
GrantLevel::Admin => 3,
}
}
impl Identity for MemoryControlPlane {
fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String> {
self.inner.lock().unwrap().awaiting.get(fingerprint).cloned()
}
fn narrowed_methods(&self, principal_id: &str) -> ControlResult<Option<Vec<String>>> {
Ok(self.inner.lock().unwrap().narrowed.get(principal_id).cloned())
}
fn registered_key(&self, fingerprint: &str) -> ControlResult<Option<RegisteredKey>> {
let inner = self.inner.lock().unwrap();
let Some(key) = inner.keys.get(fingerprint) else {
return Ok(None);
};
if let Some(expires) = key.expires_at_unix_ms {
if now_ms() >= expires {
return Ok(None);
}
}
let Some(openssh) = inner.openssh.get(fingerprint) else {
return Ok(None);
};
Ok(Some(RegisteredKey {
principal_id: inner
.forged_principal_id
.clone()
.unwrap_or_else(|| key.principal_id.clone()),
openssh: openssh.clone(),
machine: inner
.principals
.get(&key.principal_id)
.is_some_and(|p| p.kind == PrincipalKind::Machine as i32),
}))
}
fn registered_certificate(&self, der: &[u8]) -> ControlResult<Option<CertificateIdentity>> {
let inner = self.inner.lock().unwrap();
Ok(inner.certificates.get(der).cloned())
}
fn may(&self, principal_id: &str, access: &Access, level: GrantLevel) -> ControlResult<bool> {
if level == GrantLevel::Unspecified {
return Ok(false);
}
let inner = self.inner.lock().unwrap();
if let Some(forced) = inner.forced_may {
return Ok(forced);
}
if !inner.principals.contains_key(principal_id) {
return Ok(false);
}
let member = inner.employers.contains_key(principal_id);
let owns_account = |namespace: &str| !member && !namespace.is_empty() && namespace == principal_id;
match access {
Access::Namespace(name) if owns_account(name) => return Ok(true),
Access::Repo(path) if owns_account(&path.namespace) => return Ok(true),
_ => {}
}
if let Access::Repo(path) = access {
if level == GrantLevel::Read
&& inner
.repos
.get(&(path.namespace.clone(), path.repo.clone()))
.is_some_and(|repo| repo.visibility == Visibility::Public as i32)
{
return Ok(true);
}
}
let want = level_rank(level);
let box_identity = inner.bootstrap.as_ref().map(|(_, id, _)| id.as_str());
let machine = box_identity == Some(principal_id)
|| inner
.principals
.get(principal_id)
.is_some_and(|p| p.kind == PrincipalKind::Machine as i32);
let shielded = |namespace: &str| {
let account = namespace.split('/').next().unwrap_or("");
!machine
&& !account.is_empty()
&& box_identity != Some(account)
&& account != principal_id
&& inner
.principals
.get(account)
.is_some_and(|p| p.kind == PrincipalKind::User as i32)
&& !inner.employers.contains_key(account)
&& inner.employers.get(principal_id).map(String::as_str) != Some(account)
};
let inside_closed_account = match access {
Access::Server => false,
Access::Namespace(name) => shielded(name),
Access::Repo(path) => shielded(&path.namespace),
};
let server = inner
.server_grants
.get(principal_id)
.copied()
.map(level_rank)
.unwrap_or(0);
if server >= want && !inside_closed_account {
return Ok(true);
}
let scoped = inner
.grants
.iter()
.filter(|grant| grant.principal_id == principal_id)
.filter(|grant| match access {
Access::Server => false,
Access::Namespace(name) => grant.repo.is_empty() && &grant.namespace == name,
Access::Repo(path) => {
grant.namespace == path.namespace
&& (grant.repo.is_empty() || grant.repo == path.repo)
}
})
.filter_map(|grant| GrantLevel::try_from(grant.level).ok())
.map(level_rank)
.max()
.unwrap_or(0);
Ok(scoped >= want)
}
}
impl FirstBoot for MemoryControlPlane {
fn bootstrap_state(&self) -> ControlResult<BootstrapState> {
let stall = self.stall_ms.load(std::sync::atomic::Ordering::SeqCst);
if stall > 0 {
std::thread::sleep(std::time::Duration::from_millis(stall));
}
let inner = self.inner.lock().unwrap();
let (at, principal, fingerprint) =
inner
.bootstrap
.clone()
.unwrap_or((0, String::new(), String::new()));
Ok(BootstrapState {
window_open: inner.bootstrap.is_none()
&& inner.principals.is_empty()
&& inner.keys.is_empty(),
bootstrapped: inner.bootstrap.is_some(),
bootstrapped_at_unix_ms: at,
bootstrapped_principal: principal,
bootstrapped_fingerprint: fingerprint,
principals: inner.principals.len() as u64,
keys: inner.keys.len() as u64,
serving_git: true,
readiness: "serving git".to_owned(),
build: String::new(),
role: "primary".to_owned(),
..Default::default()
})
}
fn enroll_first_identity(
&self,
name: &str,
openssh_public_key: &str,
) -> ControlResult<Enrolled> {
let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
ControlError::InvalidArgument(format!(
"{openssh_public_key:?} is not an authorized_keys line"
))
})?;
let mut inner = self.inner.lock().unwrap();
if let Some((at, principal, _)) = &inner.bootstrap {
return Err(ControlError::FailedPrecondition(format!(
"this server was bootstrapped at {at} by \"{principal}\"; the window is closed \
permanently and deleting identities does not reopen it"
)));
}
if !inner.principals.is_empty() || !inner.keys.is_empty() {
return Err(ControlError::FailedPrecondition(format!(
"this server already holds {} principals and {} keys, so it is not a fresh server",
inner.principals.len(),
inner.keys.len()
)));
}
if name.is_empty() {
return Err(ControlError::InvalidArgument("`name` is required".into()));
}
let at = now_ms();
let principal = Principal {
id: name.to_owned(),
name: name.to_owned(),
kind: PrincipalKind::User as i32,
created_at_unix_ms: at,
};
let key = PublicKey {
fingerprint: fingerprint.clone(),
algo: String::new(),
principal_id: name.to_owned(),
comment: String::new(),
added_at_unix_ms: at,
last_used_unix_ms: None,
expires_at_unix_ms: None,
};
inner.principals.insert(name.to_owned(), principal.clone());
inner.keys.insert(fingerprint.clone(), key.clone());
inner
.openssh
.insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
inner
.server_grants
.insert(name.to_owned(), GrantLevel::Admin);
inner.namespaces.insert(
name.to_owned(),
Namespace {
name: name.to_owned(),
description: String::new(),
created_at_unix_ms: at,
owner_kind: 0,
..Default::default()
},
);
inner.bootstrap = Some((at, name.to_owned(), fingerprint));
Ok(Enrolled {
principal,
key,
account: name.to_owned(),
bootstrapped_at_unix_ms: at,
client_cert_pem: String::new(),
client_key_pem: String::new(),
issuer_cert_pem: String::new(),
})
}
}
fn now_ms() -> i64 {
crate::bus::now_unix_ms()
}
fn fingerprint_of_openssh(line: &str) -> Option<String> {
let key = ssh_key::PublicKey::from_openssh(line).ok()?;
Some(key.fingerprint(ssh_key::HashAlg::Sha256).to_string())
}
fn require_attestation(
target: &str,
caller: &str,
holders: &[String],
adding: &str,
attestation: Option<&SignedKeyAttestation>,
) -> ControlResult<()> {
if holders.is_empty() || (!caller.is_empty() && caller == target) {
return Ok(());
}
let Some(signed) = attestation else {
return Err(ControlError::FailedPrecondition(format!(
"principal {target} already holds {} key(s), so adding another needs {target}'s own \
attestation over the new key's fingerprint ({adding})",
holders.len()
)));
};
let Some(subject) = signed.attestation.as_ref() else {
return Err(ControlError::InvalidArgument("`attestation.attestation` is required".into()));
};
if subject.principal_id != target || subject.fingerprint != adding {
return Err(ControlError::InvalidArgument(format!(
"the attestation is for key {} on {:?} but the request adds key {adding} to {target:?}",
subject.fingerprint, subject.principal_id
)));
}
if !holders.iter().any(|held| held == &signed.key_fingerprint) {
return Err(ControlError::InvalidArgument(format!(
"the attestation is signed by {}, which is not a key {target:?} holds",
signed.key_fingerprint
)));
}
Ok(())
}
impl ControlPlane for MemoryControlPlane {
fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
let inner = self.inner.lock().unwrap();
if inner.usage.is_empty() {
return Err(ControlError::Unimplemented("Usage"));
}
Ok(inner
.usage
.values()
.filter(|row| namespace.is_none_or(|ns| row.namespace == ns))
.cloned()
.collect())
}
fn is_twin(&self) -> bool {
self.inner.lock().expect("memory control plane").twin
}
fn log_follows(&self) -> bool {
let inner = self.inner.lock().expect("memory control plane");
inner.twin || inner.log_follows
}
#[cfg(feature = "metered")]
fn entitlement_get(
&self,
namespace: &str,
) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
Ok(self
.inner
.lock()
.unwrap()
.entitlements
.get(namespace)
.cloned())
}
#[cfg(feature = "metered")]
fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
Ok(self
.inner
.lock()
.unwrap()
.entitlements
.values()
.cloned()
.collect())
}
#[cfg(feature = "metered")]
fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
let mut inner = self.inner.lock().unwrap();
let key = Inner::actor_ticket_key(&mut inner);
Ok(key
.verifying_key()
.to_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
#[cfg(feature = "metered")]
fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
use ed25519_dalek::Signer as _;
let mut inner = self.inner.lock().unwrap();
let signature = Inner::actor_ticket_key(&mut inner).sign(message).to_bytes();
inner.actor_tickets_signed += 1;
Ok(signature)
}
fn create_namespace(&self, name: &str, description: &str) -> ControlResult<Namespace> {
if name.is_empty() {
return Err(ControlError::InvalidArgument("namespace is empty".into()));
}
let mut inner = self.inner.lock().unwrap();
if inner.namespaces.contains_key(name) {
return Err(ControlError::AlreadyExists(format!("namespace {name}")));
}
let namespace = Namespace {
name: name.to_string(),
description: description.to_string(),
created_at_unix_ms: now_ms(),
owner_kind: 0,
..Default::default()
};
inner.namespaces.insert(name.to_string(), namespace.clone());
Ok(namespace)
}
fn list_namespaces(&self) -> ControlResult<Vec<Namespace>> {
let inner = self.inner.lock().unwrap();
Ok(inner
.namespaces
.values()
.cloned()
.map(|mut ns| {
ns.employees = inner
.employers
.values()
.filter(|account| **account == ns.name)
.count() as u32;
ns
})
.collect())
}
fn set_namespace_settings(
&self,
name: &str,
public: Option<bool>,
events_public: Option<bool>,
) -> ControlResult<Namespace> {
{
let mut inner = self.inner.lock().unwrap();
let ns = inner
.namespaces
.get_mut(name)
.ok_or_else(|| ControlError::NotFound(format!("namespace {name}")))?;
if let Some(public) = public {
ns.public = public;
}
if let Some(events_public) = events_public {
ns.events_public = events_public;
}
}
Ok(self
.namespace(name)?
.expect("the namespace was just written"))
}
fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32> {
let mut inner = self.inner.lock().unwrap();
if !inner.namespaces.contains_key(name) {
return Err(ControlError::NotFound(format!("namespace {name}")));
}
let held: Vec<(String, String)> = inner
.repos
.keys()
.filter(|(ns, _)| ns == name)
.cloned()
.collect();
if !held.is_empty() && !delete_repos {
return Err(ControlError::FailedPrecondition(format!(
"namespace {name} holds {} repos; deleting repos is never implicit",
held.len()
)));
}
for key in &held {
inner.repos.remove(key);
inner.mirrors.remove(key);
inner.protections.remove(key);
inner.ref_policies.remove(key);
}
inner.namespaces.remove(name);
inner.grants.retain(|g| g.namespace != name);
Ok(held.len() as u32)
}
fn create_repo(&self, spec: NewRepo) -> ControlResult<Repo> {
spec.path
.validate()
.map_err(|e| ControlError::InvalidArgument(e.to_string()))?;
let mut inner = self.inner.lock().unwrap();
if !inner.namespaces.contains_key(&spec.path.namespace) {
return Err(ControlError::NotFound(format!(
"namespace {}",
spec.path.namespace
)));
}
let key = MemoryControlPlane::key(&spec.path);
if inner.repos.contains_key(&key) {
return Err(ControlError::AlreadyExists(format!(
"repository {}",
spec.path
)));
}
let hash_kind = if spec.hash_kind == HashKind::Unspecified {
DEFAULT_HASH_KIND
} else {
spec.hash_kind
};
let default_branch = if spec.default_branch.is_empty() {
DEFAULT_BRANCH.to_string()
} else {
spec.default_branch.clone()
};
let visibility = if spec.visibility == Visibility::Unspecified {
Visibility::Private
} else {
spec.visibility
};
let repo = Repo {
path: Some(spec.path.clone()),
hash_kind: hash_kind as i32,
default_branch,
visibility: visibility as i32,
created_at_unix_ms: now_ms(),
description: spec.description,
encrypted: false,
};
inner.repos.insert(key, repo.clone());
Ok(repo)
}
fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo> {
self.inner
.lock()
.unwrap()
.repos
.get(&MemoryControlPlane::key(path))
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("repository {path}")))
}
fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>> {
Ok(self
.inner
.lock()
.unwrap()
.repos
.iter()
.filter(|((ns, _), _)| namespace.is_none_or(|want| want == ns))
.map(|(_, repo)| repo.clone())
.collect())
}
fn delete_repo(&self, path: &RepoPath, options: DeleteOptions) -> ControlResult<DeleteOutcome> {
let key = MemoryControlPlane::key(path);
let mut inner = self.inner.lock().unwrap();
if inner.repos.remove(&key).is_none() {
return Err(ControlError::NotFound(format!("repository {path}")));
}
inner.mirrors.remove(&key);
inner.protections.remove(&key);
inner.ref_policies.remove(&key);
inner
.grants
.retain(|g| !(g.namespace == path.namespace && g.repo == path.repo));
Ok(if options.purge {
DeleteOutcome::Purged { bytes_reclaimed: 0 }
} else {
DeleteOutcome::Tombstoned {
purge_after_unix_ms: now_ms()
+ i64::from(options.retention_days.unwrap_or(30)) * 86_400_000,
bytes_kept: 0,
}
})
}
fn set_default_branch(&self, path: &RepoPath, default_branch: &str) -> ControlResult<Repo> {
if default_branch.is_empty() {
return Err(ControlError::InvalidArgument(
"default_branch is empty".into(),
));
}
let mut inner = self.inner.lock().unwrap();
let repo = inner
.repos
.get_mut(&MemoryControlPlane::key(path))
.ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
repo.default_branch = default_branch.to_string();
Ok(repo.clone())
}
fn set_visibility(&self, path: &RepoPath, visibility: Visibility) -> ControlResult<Repo> {
if visibility == Visibility::Unspecified {
return Err(ControlError::InvalidArgument(
"visibility unspecified".into(),
));
}
let mut inner = self.inner.lock().unwrap();
let repo = inner
.repos
.get_mut(&MemoryControlPlane::key(path))
.ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
repo.visibility = visibility as i32;
Ok(repo.clone())
}
fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo> {
let mut inner = self.inner.lock().unwrap();
let repo = inner
.repos
.get_mut(&MemoryControlPlane::key(path))
.ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
repo.encrypted = encrypted;
Ok(repo.clone())
}
fn list_refs(
&self,
path: &RepoPath,
_prefix: &str,
_peel: bool,
) -> ControlResult<Vec<RefTarget>> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("Refs.List"))
}
fn get_ref(&self, path: &RepoPath, _ref_name: &str, _peel: bool) -> ControlResult<RefTarget> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("Refs.Get"))
}
fn log(
&self,
path: &RepoPath,
_start: &str,
_limit: u32,
_touching: &str,
) -> ControlResult<Vec<Commit>> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("Refs.Log"))
}
fn create_pull(
&self,
path: &RepoPath,
_author: &str,
_title: &str,
_description: &str,
_source_ref: &str,
_target_ref: &str,
) -> ControlResult<PullRequest> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("PullRequests.Create"))
}
fn list_pulls(&self, path: &RepoPath, _state: i32) -> ControlResult<Vec<PullRequest>> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("PullRequests.List"))
}
fn get_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("PullRequests.Get"))
}
fn close_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("PullRequests.Close"))
}
fn merge_pull(
&self,
path: &RepoPath,
_number: u64,
_principal_id: &str,
) -> ControlResult<(PullRequest, MergedRef)> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("PullRequests.Merge"))
}
fn read_object(
&self,
path: &RepoPath,
_oid: &str,
_max_bytes: u64,
) -> ControlResult<ReadObjectResponse> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("Refs.ReadObject"))
}
fn diff(
&self,
path: &RepoPath,
_from: &str,
_to: &str,
_path_prefixes: &[String],
) -> ControlResult<Vec<FileChange>> {
self.get_repo(path)?;
Err(ControlError::Unimplemented("Refs.Diff"))
}
fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind) -> ControlResult<Principal> {
if name.is_empty() || id.is_empty() {
return Err(ControlError::InvalidArgument(
"principal name and id are required".into(),
));
}
let mut inner = self.inner.lock().unwrap();
if inner.principals.contains_key(id) {
return Err(ControlError::AlreadyExists(format!("principal {id}")));
}
if let Some(holder) = Inner::name_holder(&inner, name, id) {
return Err(ControlError::AlreadyExists(format!(
"the name \"{name}\" is already taken by principal {holder}"
)));
}
let principal = Principal {
id: id.to_string(),
name: name.to_string(),
kind: kind as i32,
created_at_unix_ms: now_ms(),
};
inner.principals.insert(id.to_string(), principal.clone());
Ok(principal)
}
fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer> {
let named = ask.seats == 0;
if named && (ask.name.is_empty() || ask.id.is_empty()) {
return Err(ControlError::InvalidArgument(
"an employee's id and name are required".into(),
));
}
let mut inner = self.inner.lock().unwrap();
if !inner.namespaces.contains_key(account) {
return Err(ControlError::NotFound(format!("account {account}")));
}
let open = |lots: &[(u64, u32, Vec<String>, String)]| -> u32 {
lots.iter()
.map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
.sum()
};
let lots = inner.seats.get(account).cloned().unwrap_or_default();
let total: u32 = lots.iter().map(|(_, total, _, _)| *total).sum();
if !ask.order.is_empty() {
if let Some((owner, principal, _, room, need)) = inner.room_orders.get(&ask.order).cloned() {
if owner != account {
return Err(ControlError::InvalidArgument(format!(
"order {} was granted to {owner}, not to {account}",
ask.order
)));
}
return Ok(EmployeeAnswer {
principal: inner.principals.get(&principal).cloned(),
room_bytes: room,
seats: total,
seats_open: open(&lots),
need_bytes: need,
unused_bytes: None,
shortfall_bytes: 0,
unchanged: true,
});
}
}
if named {
if ask.id == account {
return Err(ControlError::InvalidArgument(format!(
"{account} is the account itself, not an employee of it"
)));
}
if inner.principals.contains_key(&ask.id)
|| inner.principals.values().any(|p| p.name == ask.name)
{
return Err(ControlError::AlreadyExists(format!(
"principal {} already exists, and an existing principal can never become \
an employee",
ask.id
)));
}
if inner.namespaces.contains_key(&ask.id) {
return Err(ControlError::AlreadyExists(format!(
"namespace {} exists, so an employee may not be called that",
ask.id
)));
}
}
let lot = if ask.from_seat {
Some(
lots.iter()
.position(|(_, total, filled, _)| (filled.len() as u32) < *total)
.ok_or_else(|| {
ControlError::FailedPrecondition(format!(
"{account} has no open seat: every seat it holds is filled. Its \
admin adds users first"
))
})?,
)
} else {
None
};
let room_bytes = lot.map_or(ask.room_bytes, |lot| lots[lot].0);
let need_bytes = match (lot, ask.seats) {
(Some(_), _) => 0,
(None, 0) => room_bytes,
(None, seats) => room_bytes.saturating_mul(u64::from(seats)),
};
let committed = ask.cap.map(|_| {
let promised: u64 = lots
.iter()
.map(|(room, total, filled, _)| room * u64::from(total.saturating_sub(filled.len() as u32)))
.sum::<u64>()
+ inner
.employers
.iter()
.filter(|(_, employer)| *employer == account)
.filter_map(|(id, _)| inner.rooms.get(id).map(|(room, _)| *room))
.sum::<u64>();
promised.saturating_add(ask.stored)
});
let unused_bytes = ask.cap.zip(committed).map(|(cap, committed)| cap.saturating_sub(committed));
let shortfall_bytes = ask
.cap
.zip(committed)
.map_or(0, |(cap, committed)| need_bytes.saturating_add(committed).saturating_sub(cap));
let mut answer = EmployeeAnswer {
principal: None,
room_bytes,
seats: total,
seats_open: open(&lots),
need_bytes,
unused_bytes,
shortfall_bytes,
unchanged: ask.dry_run || shortfall_bytes > 0,
};
if answer.unchanged {
return Ok(answer);
}
let mut lots = lots;
if named {
let principal = Principal {
id: ask.id.clone(),
name: ask.name.clone(),
kind: PrincipalKind::User as i32,
created_at_unix_ms: now_ms(),
};
inner.principals.insert(ask.id.clone(), principal.clone());
inner.employers.insert(ask.id.clone(), account.to_owned());
inner.grants.push(Grant {
principal_id: ask.id.clone(),
namespace: account.to_owned(),
repo: String::new(),
level: GrantLevel::Read as i32,
..Default::default()
});
let order = match lot {
Some(lot) => {
lots[lot].2.push(ask.id.clone());
lots[lot].3.clone()
}
None => ask.order.clone(),
};
if room_bytes > 0 {
inner.rooms.insert(ask.id.clone(), (room_bytes, order));
}
answer.principal = Some(principal);
} else {
lots.push((room_bytes, ask.seats, Vec::new(), ask.order.clone()));
}
answer.seats = lots.iter().map(|(_, total, _, _)| *total).sum();
answer.seats_open = open(&lots);
if !lots.is_empty() {
inner.seats.insert(account.to_owned(), lots);
}
if !ask.order.is_empty() {
inner.room_orders.insert(
ask.order.clone(),
(
account.to_owned(),
if named { ask.id.clone() } else { String::new() },
ask.seats,
room_bytes,
need_bytes,
),
);
}
Ok(answer)
}
fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
let inner = self.inner.lock().unwrap();
let lots = inner.seats.get(account).cloned().unwrap_or_default();
Ok((
lots.iter().map(|(_, total, _, _)| *total).sum(),
lots.iter()
.map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
.sum(),
))
}
fn employees(&self, account: &str) -> ControlResult<Vec<Principal>> {
let inner = self.inner.lock().unwrap();
Ok(inner
.employers
.iter()
.filter(|(_, employer)| *employer == account)
.filter_map(|(id, _)| inner.principals.get(id).cloned())
.collect())
}
fn operator_bootstrap_invite(
&self,
operator: &str,
secret_sha256: &str,
now_unix_ms: i64,
ttl_ms: i64,
) -> ControlResult<BootstrapInvite> {
let hash = bootstrap_hash(secret_sha256)?;
let ttl_ms = bootstrap_ttl(ttl_ms)?;
let mut inner = self.inner.lock().unwrap();
let principal = inner
.principals
.get(operator)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("principal {operator}")))?;
if principal.kind != PrincipalKind::User as i32 {
return Err(ControlError::FailedPrecondition(format!(
"principal {operator} is not a person: the operator's first invite is a person's"
)));
}
if inner
.credentials
.values()
.any(|stored| stored.credential.principal_id == operator)
{
inner.invites.remove(OPERATOR_INVITE_ID);
return Ok(BootstrapInvite::Spent);
}
let renewed = inner
.invites
.get(OPERATOR_INVITE_ID)
.is_some_and(|(row, held)| *held == hash && row.principal_id == operator);
let row = gunnar_proto::Invite {
invite_id: OPERATOR_INVITE_ID.to_owned(),
principal_id: operator.to_owned(),
created_by: "installer".to_owned(),
created_at_unix_ms: now_unix_ms,
expires_at_unix_ms: now_unix_ms + ttl_ms,
note: "the operator's first passkey: the installer's first invite".to_owned(),
server_level: GrantLevel::Unspecified as i32,
};
inner.invites.insert(OPERATOR_INVITE_ID.to_owned(), (row, hash));
Ok(if renewed { BootstrapInvite::Renewed } else { BootstrapInvite::Recorded })
}
fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
let (name, named_by) = operator_name(None, asked)?;
let held = {
let inner = self.inner.lock().unwrap();
inner
.principals
.get(name)
.cloned()
.or_else(|| inner.principals.values().find(|p| p.name == name).cloned())
};
let (principal, created) = match held {
Some(p) if p.kind != PrincipalKind::User as i32 => {
return Err(ControlError::FailedPrecondition(format!(
"the operator's name {name:?} is held by a principal that is not a person"
)))
}
Some(p) => (p, false),
None => (self.add_principal(name, name, PrincipalKind::User)?, true),
};
let mut inner = self.inner.lock().unwrap();
let adopted_namespace = created && inner.namespaces.contains_key(&principal.id);
if !inner.namespaces.contains_key(&principal.id) {
inner.namespaces.insert(
principal.id.clone(),
Namespace {
name: principal.id.clone(),
created_at_unix_ms: now_ms(),
..Default::default()
},
);
}
inner
.server_grants
.insert(principal.id.clone(), GrantLevel::Admin);
Ok(OperatorClaim {
principal,
created,
adopted_namespace,
named_by,
})
}
fn list_principals(&self) -> ControlResult<Vec<Principal>> {
Ok(self
.inner
.lock()
.unwrap()
.principals
.values()
.cloned()
.collect())
}
fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>> {
Ok(self
.inner
.lock()
.unwrap()
.principals
.values()
.find(|p| p.name == name)
.cloned())
}
fn add_credential(
&self,
principal_id: &str,
credential_id: &[u8],
public_key: &[u8],
label: &str,
sign_count: u32,
) -> ControlResult<gunnar_proto::WebCredential> {
let mut inner = self.inner.lock().unwrap();
let principal = inner
.principals
.get(principal_id)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("principal {principal_id}")))?;
if principal.kind != PrincipalKind::User as i32 {
return Err(ControlError::FailedPrecondition(format!(
"principal {principal_id} is not a person: no human credential on a machine principal"
)));
}
let key = crate::webauthn::b64url(credential_id);
if inner.credentials.contains_key(&key) {
return Err(ControlError::AlreadyExists(format!("web credential {key}")));
}
let credential = gunnar_proto::WebCredential {
credential_id: credential_id.to_vec(),
principal_id: principal_id.to_owned(),
label: label.to_owned(),
created_at_unix_ms: now_ms(),
last_used_unix_ms: None,
sign_count,
};
inner.credentials.insert(
key,
StoredCredential {
credential: credential.clone(),
public_key: public_key.to_vec(),
},
);
Ok(credential)
}
fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
Ok(self
.inner
.lock()
.unwrap()
.credentials
.values()
.filter(|c| principal_id.is_none_or(|want| c.credential.principal_id == want))
.map(|c| c.credential.clone())
.collect())
}
fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
Ok(self
.inner
.lock()
.unwrap()
.credentials
.get(&crate::webauthn::b64url(credential_id))
.cloned())
}
fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
let key = crate::webauthn::b64url(credential_id);
let mut inner = self.inner.lock().unwrap();
let removed = inner.credentials.remove(&key).map(|c| c.credential);
for fingerprint in inner.credential_sessions.remove(&key).unwrap_or_default() {
inner.keys.remove(&fingerprint);
inner.openssh.remove(&fingerprint);
}
Ok(removed)
}
fn rename_credential(
&self,
credential_id: &[u8],
label: &str,
) -> ControlResult<Option<gunnar_proto::WebCredential>> {
let mut inner = self.inner.lock().unwrap();
Ok(inner
.credentials
.get_mut(&crate::webauthn::b64url(credential_id))
.map(|stored| {
stored.credential.label = label.to_owned();
stored.credential.clone()
}))
}
fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
let mut inner = self.inner.lock().unwrap();
let principal = inner
.principals
.get(&invite.principal_id)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("principal {}", invite.principal_id)))?;
if principal.kind != PrincipalKind::User as i32 {
return Err(ControlError::FailedPrecondition(format!(
"principal {} is not a person: no human credential on a machine principal",
invite.principal_id
)));
}
if invite.server_level == GrantLevel::Admin {
return Err(ControlError::InvalidArgument(crate::service::access::NO_ADMIN_BY_INVITE.to_owned()));
}
if inner.invites.contains_key(&invite.id) {
return Err(ControlError::AlreadyExists(format!("invite {}", invite.id)));
}
let row = gunnar_proto::Invite {
invite_id: invite.id.clone(),
principal_id: invite.principal_id,
created_by: invite.created_by,
created_at_unix_ms: invite.created_at_unix_ms,
expires_at_unix_ms: invite.expires_at_unix_ms,
note: invite.note,
server_level: invite.server_level as i32,
};
inner.invites.insert(invite.id, (row.clone(), invite.secret_hash));
Ok(row)
}
fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
let mut rows: Vec<gunnar_proto::Invite> =
self.inner.lock().unwrap().invites.values().map(|(row, _)| row.clone()).collect();
rows.sort_by(|a, b| (a.created_at_unix_ms, &a.invite_id).cmp(&(b.created_at_unix_ms, &b.invite_id)));
Ok(rows)
}
fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
Ok(self.inner.lock().unwrap().invites.remove(id).map(|(row, _)| row))
}
fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
let inner = self.inner.lock().unwrap();
if let Some((row, _)) = inner.invites.values().find(|(_, held)| held == secret_hash) {
return Ok(if row.expires_at_unix_ms > now_unix_ms { InviteCheck::Valid(row.clone()) } else { InviteCheck::Expired });
}
Ok(if inner.redeemed_invites.contains(secret_hash) { InviteCheck::Used } else { InviteCheck::Unknown })
}
fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
Ok(self.inner.lock().unwrap().server_grants.contains_key(principal_id))
}
fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
Ok(self.inner.lock().unwrap().employers.get(principal_id).cloned())
}
fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
let key = crate::webauthn::b64url(&commit.credential_id);
let mut fields = commit.session_openssh.split_whitespace();
let algo = fields.next().unwrap_or_default().to_owned();
let fingerprint = fingerprint_of_openssh(&commit.session_openssh).ok_or_else(|| {
ControlError::InvalidArgument("the session key is not an OpenSSH public key".into())
})?;
let mut inner = self.inner.lock().unwrap();
let mut stored = inner
.credentials
.get(&key)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("web credential {key}")))?;
let principal = inner
.principals
.get(&stored.credential.principal_id)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("principal {}", stored.credential.principal_id)))?;
let held = stored.credential.sign_count;
if held != 0 && commit.sign_count != 0 && commit.sign_count <= held {
return Err(ControlError::PermissionDenied(format!(
"the signature counter of web credential {key} went from {held} to {}: a cloned \
authenticator, or a replay",
commit.sign_count
)));
}
if inner.keys.contains_key(&fingerprint) {
return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
}
if commit.sign_count != 0 {
stored.credential.sign_count = commit.sign_count;
}
stored.credential.last_used_unix_ms = Some(commit.now_unix_ms);
inner.credentials.insert(key.clone(), stored.clone());
inner
.credential_sessions
.entry(key)
.or_default()
.push(fingerprint.clone());
inner
.openssh
.insert(fingerprint.clone(), commit.session_openssh.trim().to_owned());
inner.keys.insert(
fingerprint.clone(),
PublicKey {
fingerprint: fingerprint.clone(),
algo,
principal_id: principal.id.clone(),
comment: commit.comment,
added_at_unix_ms: commit.now_unix_ms,
last_used_unix_ms: None,
expires_at_unix_ms: Some(commit.expires_at_unix_ms),
},
);
Ok(WebLoginOutcome {
principal,
credential: stored.credential,
fingerprint,
})
}
fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
const INVALID: &str =
"this invite is not valid: it is unknown, already used, expired, or for somebody else";
let mut inner = self.inner.lock().unwrap();
let (target, level, redeemed) = match ®istration.invite_secret_hash {
Some(hash) => {
let (row, _) = inner
.invites
.values()
.find(|(row, held)| held == hash && row.expires_at_unix_ms > registration.now_unix_ms)
.cloned()
.ok_or_else(|| ControlError::PermissionDenied(INVALID.into()))?;
if registration
.principal_id
.as_deref()
.is_some_and(|asked| asked != row.principal_id)
{
return Err(ControlError::PermissionDenied(INVALID.into()));
}
(
row.principal_id.clone(),
GrantLevel::try_from(row.server_level).unwrap_or(GrantLevel::Unspecified),
Some(row.invite_id),
)
}
None => (
registration.principal_id.clone().ok_or_else(|| {
ControlError::InvalidArgument("name the principal, or redeem an invite".into())
})?,
GrantLevel::Unspecified,
None,
),
};
let principal = inner
.principals
.get(&target)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("principal {target}")))?;
if principal.kind != PrincipalKind::User as i32 {
return Err(ControlError::FailedPrecondition(format!(
"principal {target} is not a person: no human credential on a machine principal"
)));
}
let key = crate::webauthn::b64url(®istration.credential_id);
if inner.credentials.contains_key(&key) {
return Err(ControlError::AlreadyExists(format!("web credential {key}")));
}
let credential = gunnar_proto::WebCredential {
credential_id: registration.credential_id.clone(),
principal_id: target.clone(),
label: registration.label,
created_at_unix_ms: registration.now_unix_ms,
last_used_unix_ms: None,
sign_count: registration.sign_count,
};
inner.credentials.insert(
key,
StoredCredential {
credential: credential.clone(),
public_key: registration.public_key,
},
);
if let Some(id) = redeemed {
inner.invites.remove(&id);
if let Some(hash) = registration.invite_secret_hash.clone() {
inner.redeemed_invites.insert(hash);
}
}
if level != GrantLevel::Unspecified {
inner.server_grants.insert(target, level);
}
Ok(WebRegistered {
credential,
principal,
server_level: level,
})
}
fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
let mut inner = self.inner.lock().unwrap();
let key = crate::webauthn::b64url(&approval.credential_id);
if inner.credentials.contains_key(&key) {
return Err(ControlError::AlreadyExists(format!("web credential {key}")));
}
let mut step_up = None;
if let Some((id, presented)) = &approval.step_up {
let held = crate::webauthn::b64url(id);
let stored = inner
.credentials
.get(&held)
.cloned()
.ok_or_else(|| ControlError::NotFound(format!("web credential {held}")))?;
let count = stored.credential.sign_count;
if count != 0 && *presented != 0 && *presented <= count {
return Err(ControlError::PermissionDenied(format!(
"the signature counter of web credential {held} went from {count} to {presented}: \
a cloned authenticator, or a replay"
)));
}
step_up = Some((held, stored, *presented));
}
let (principal, created) = match inner.principals.get(&approval.machine).cloned() {
Some(p) if p.kind != PrincipalKind::Machine as i32 => {
return Err(ControlError::FailedPrecondition(format!(
"principal {} is not a machine: a machine's credential never lands on a person",
approval.machine
)))
}
Some(p) => (p, false),
None => {
if let Some(holder) = Inner::name_holder(&inner, &approval.machine, &approval.machine) {
return Err(ControlError::AlreadyExists(format!(
"the name \"{}\" is already taken by principal {holder}",
approval.machine
)));
}
let p = Principal {
id: approval.machine.clone(),
name: approval.machine.clone(),
kind: PrincipalKind::Machine as i32,
created_at_unix_ms: approval.now_unix_ms,
};
inner.principals.insert(p.id.clone(), p.clone());
(p, true)
}
};
if let Some((held, mut stored, presented)) = step_up {
if presented != 0 {
stored.credential.sign_count = presented;
}
stored.credential.last_used_unix_ms = Some(approval.now_unix_ms);
inner.credentials.insert(held, stored);
}
inner
.server_grants
.insert(principal.id.clone(), approval.server_level);
let credential = gunnar_proto::WebCredential {
credential_id: approval.credential_id.clone(),
principal_id: principal.id.clone(),
label: approval.label,
created_at_unix_ms: approval.now_unix_ms,
last_used_unix_ms: None,
sign_count: approval.sign_count,
};
inner.credentials.insert(
key,
StoredCredential {
credential: credential.clone(),
public_key: approval.public_key,
},
);
Ok(MachineApproved {
principal,
credential,
created,
})
}
fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal> {
if name.is_empty() {
return Err(ControlError::InvalidArgument("the new name is empty".into()));
}
let mut inner = self.inner.lock().unwrap();
if !inner.principals.contains_key(principal_id) {
return Err(ControlError::NotFound(format!("principal {principal_id}")));
}
if let Some(holder) = Inner::name_holder(&inner, name, principal_id) {
return Err(ControlError::AlreadyExists(format!(
"the name \"{name}\" is already taken by principal {holder}"
)));
}
let row = inner.principals.get_mut(principal_id).expect("checked above");
row.name = name.to_string();
Ok(row.clone())
}
fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)> {
let mut inner = self.inner.lock().unwrap();
inner.employers.remove(principal_id);
if inner.principals.remove(principal_id).is_none() {
return Err(ControlError::NotFound(format!("principal {principal_id}")));
}
let before_keys = inner.keys.len();
inner.keys.retain(|_, k| k.principal_id != principal_id);
let live: Vec<String> = inner.keys.keys().cloned().collect();
inner.openssh.retain(|fp, _| live.contains(fp));
let before_grants = inner.grants.len();
inner.grants.retain(|g| g.principal_id != principal_id);
inner.server_grants.remove(principal_id);
inner
.credentials
.retain(|_, c| c.credential.principal_id != principal_id);
inner
.invites
.retain(|_, (row, _)| row.principal_id != principal_id);
Ok((
(before_keys - inner.keys.len()) as u32,
(before_grants - inner.grants.len()) as u32,
))
}
fn add_key(
&self,
principal_id: &str,
openssh_public_key: &str,
comment: &str,
expires_at_unix_ms: Option<i64>,
caller: &str,
attestation: Option<SignedKeyAttestation>,
) -> ControlResult<PublicKey> {
let mut fields = openssh_public_key.split_whitespace();
let (Some(algo), Some(_blob)) = (fields.next(), fields.next()) else {
return Err(ControlError::InvalidArgument(
"not an OpenSSH public key line: expected `<algo> <base64> [comment]`".into(),
));
};
if !(algo.starts_with("ssh-") || algo.starts_with("ecdsa-") || algo.starts_with("sk-")) {
return Err(ControlError::InvalidArgument(format!(
"unrecognised public key algorithm {algo}"
)));
}
let mut inner = self.inner.lock().unwrap();
if !inner.principals.contains_key(principal_id) {
return Err(ControlError::NotFound(format!("principal {principal_id}")));
}
let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
ControlError::InvalidArgument(format!(
"the {algo} key does not parse as an OpenSSH public key line; the fingerprint \
the SSH listener will match against cannot be computed from it"
))
})?;
if inner.keys.contains_key(&fingerprint) {
return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
}
let holders: Vec<String> = inner
.keys
.values()
.filter(|key| key.principal_id == principal_id)
.filter(|key| key.expires_at_unix_ms.is_none_or(|expires| now_ms() < expires))
.map(|key| key.fingerprint.clone())
.collect();
require_attestation(principal_id, caller, &holders, &fingerprint, attestation.as_ref())?;
inner
.openssh
.insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
let key = PublicKey {
fingerprint: fingerprint.clone(),
algo: algo.to_string(),
principal_id: principal_id.to_string(),
comment: comment.to_string(),
added_at_unix_ms: now_ms(),
last_used_unix_ms: None,
expires_at_unix_ms,
};
inner.keys.insert(fingerprint, key.clone());
Ok(key)
}
fn revoke_key(&self, fingerprint: &str) -> ControlResult<()> {
let mut inner = self.inner.lock().unwrap();
inner.openssh.remove(fingerprint);
inner
.keys
.remove(fingerprint)
.map(|_| ())
.ok_or_else(|| ControlError::NotFound(format!("key {fingerprint}")))
}
fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>> {
Ok(self
.inner
.lock()
.unwrap()
.keys
.values()
.filter(|k| principal_id.is_none_or(|want| want == k.principal_id))
.cloned()
.collect())
}
fn grant(
&self,
principal_id: &str,
namespace: &str,
repo: &str,
level: GrantLevel,
) -> ControlResult<Grant> {
if level == GrantLevel::Unspecified {
return Err(ControlError::InvalidArgument(
"grant level unspecified".into(),
));
}
if namespace.is_empty() {
return Err(ControlError::InvalidArgument(
"a grant must name a namespace; multi-tenancy has no ungrouped repos".into(),
));
}
let mut inner = self.inner.lock().unwrap();
if !inner.principals.contains_key(principal_id) {
return Err(ControlError::NotFound(format!("principal {principal_id}")));
}
if !inner.namespaces.contains_key(namespace) {
return Err(ControlError::NotFound(format!("namespace {namespace}")));
}
if !repo.is_empty()
&& !inner
.repos
.contains_key(&(namespace.to_string(), repo.to_string()))
{
return Err(ControlError::NotFound(format!(
"repository {namespace}/{repo}"
)));
}
let grant = Grant {
principal_id: principal_id.to_string(),
namespace: namespace.to_string(),
repo: repo.to_string(),
level: level as i32,
granted_at_unix_ms: now_ms(),
};
inner.grants.retain(|g| {
!(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
});
inner.grants.push(grant.clone());
Ok(grant)
}
fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()> {
let mut inner = self.inner.lock().unwrap();
let before = inner.grants.len();
inner.grants.retain(|g| {
!(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
});
if inner.grants.len() == before {
return Err(ControlError::NotFound(format!(
"grant for {principal_id} on {namespace}/{repo}"
)));
}
Ok(())
}
fn list_grants(
&self,
principal_id: Option<&str>,
namespace: Option<&str>,
repo: Option<&str>,
) -> ControlResult<Vec<Grant>> {
Ok(self
.inner
.lock()
.unwrap()
.grants
.iter()
.filter(|g| principal_id.is_none_or(|w| w == g.principal_id))
.filter(|g| namespace.is_none_or(|w| w == g.namespace))
.filter(|g| repo.is_none_or(|w| w == g.repo))
.cloned()
.collect())
}
fn get_protection(
&self,
path: &RepoPath,
ref_pattern: Option<&str>,
) -> ControlResult<Vec<BranchProtection>> {
self.get_repo(path)?;
let inner = self.inner.lock().unwrap();
let Some(rules) = inner.protections.get(&MemoryControlPlane::key(path)) else {
return Ok(Vec::new());
};
Ok(rules
.values()
.filter(|p| ref_pattern.is_none_or(|w| w == p.ref_pattern))
.cloned()
.collect())
}
fn set_protection(
&self,
path: &RepoPath,
protection: BranchProtection,
) -> ControlResult<BranchProtection> {
if protection.ref_pattern.is_empty() {
return Err(ControlError::InvalidArgument("ref_pattern is empty".into()));
}
self.get_repo(path)?;
let mut inner = self.inner.lock().unwrap();
inner
.protections
.entry(MemoryControlPlane::key(path))
.or_default()
.insert(protection.ref_pattern.clone(), protection.clone());
Ok(protection)
}
fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()> {
self.get_repo(path)?;
let mut inner = self.inner.lock().unwrap();
let removed = inner
.protections
.get_mut(&MemoryControlPlane::key(path))
.and_then(|rules| rules.remove(ref_pattern));
if removed.is_none() {
return Err(ControlError::NotFound(format!(
"protection {ref_pattern} on {path}"
)));
}
Ok(())
}
fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy> {
self.get_repo(path)?;
Ok(self
.inner
.lock()
.unwrap()
.ref_policies
.get(&MemoryControlPlane::key(path))
.cloned()
.unwrap_or_default())
}
fn set_ref_name_policy(
&self,
path: &RepoPath,
policy: RefNamePolicy,
) -> ControlResult<RefNamePolicy> {
self.get_repo(path)?;
self.inner
.lock()
.unwrap()
.ref_policies
.insert(MemoryControlPlane::key(path), policy.clone());
Ok(policy)
}
fn forge_append(&self, _event: SignedForgeEvent) -> ControlResult<u64> {
Err(ControlError::Unimplemented("Forge.Append"))
}
fn forge_review(
&self,
_repo: &str,
_pull: u64,
) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)> {
Err(ControlError::Unimplemented("Forge.GetReview"))
}
fn forge_reviews(&self, _repo: &str) -> ControlResult<Vec<ReviewState>> {
Err(ControlError::Unimplemented("Forge.ListReviews"))
}
fn forge_issue(
&self,
_repo: &str,
_number: u64,
) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>> {
Err(ControlError::Unimplemented("Forge.GetIssue"))
}
fn forge_issues(&self, _repo: &str) -> ControlResult<Vec<IssueState>> {
Err(ControlError::Unimplemented("Forge.ListIssues"))
}
fn counts(&self) -> ControlResult<ControlCounts> {
let inner = self.inner.lock().unwrap();
Ok(ControlCounts {
namespaces: inner.namespaces.len() as u64,
repos: inner.repos.len() as u64,
principals: inner.principals.len() as u64,
keys: inner.keys.len() as u64,
grants: inner.grants.len() as u64,
})
}
fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse> {
let repos = match path {
Some(path) => vec![self.get_repo(path)?],
None => self.list_repos(None)?,
};
Ok(StorageInfoResponse {
total_tombstoned_bytes: 0,
tombstoned: Vec::new(),
reality: None,
object_backend: "none (MemoryControlPlane test double)".into(),
ref_backend: "none (MemoryControlPlane test double)".into(),
engine: None,
total_hot_bytes: 0,
total_cold_bytes: 0,
cold_measured: false,
total_explode_bytes: 0,
total_explode_budget_bytes: 0,
repos: repos
.into_iter()
.map(|repo| RepoStorage {
repo: repo.path.clone(),
hash_kind: repo.hash_kind,
..Default::default()
})
.collect(),
})
}
fn mirror_add(
&self,
path: &RepoPath,
remote_url: &str,
prune: bool,
create_repo: bool,
) -> ControlResult<MirrorConfig> {
if remote_url.is_empty() {
return Err(ControlError::InvalidArgument("remote_url is empty".into()));
}
if self.get_repo(path).is_err() {
if !create_repo {
return Err(ControlError::NotFound(format!("repository {path}")));
}
self.create_repo(NewRepo {
path: path.clone(),
hash_kind: HashKind::Unspecified,
default_branch: String::new(),
visibility: Visibility::Private,
description: format!("mirror of {remote_url}"),
adopt: false,
})?;
}
let mut inner = self.inner.lock().unwrap();
let key = MemoryControlPlane::key(path);
if inner.mirrors.contains_key(&key) {
return Err(ControlError::AlreadyExists(format!("mirror for {path}")));
}
let mirror = MirrorConfig {
repo: Some(path.clone()),
remote_url: remote_url.to_string(),
prune,
state: MirrorState::Idle as i32,
added_at_unix_ms: now_ms(),
last_sync_at_unix_ms: None,
last_error: String::new(),
sync_count: 0,
credential: MirrorCredentialKind::Unspecified as i32,
credential_username: String::new(),
job: None,
repo_pending: false,
lfs_not_copied: false,
};
inner.mirrors.insert(key, mirror.clone());
Ok(mirror)
}
fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig> {
let inner = self.inner.lock().unwrap();
if !inner.mirrors.contains_key(&MemoryControlPlane::key(path)) {
return Err(ControlError::NotFound(format!("mirror for {path}")));
}
Err(ControlError::Unimplemented("Mirror.Sync"))
}
fn mirror_set_credential(
&self,
path: &RepoPath,
kind: MirrorCredentialKind,
secret: &str,
username: &str,
) -> ControlResult<MirrorConfig> {
credential_arguments(kind, secret)?;
let mut inner = self.inner.lock().unwrap();
let mirror = inner
.mirrors
.get_mut(&MemoryControlPlane::key(path))
.ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))?;
mirror.credential = kind as i32;
mirror.credential_username = if kind == MirrorCredentialKind::Token {
username.to_owned()
} else {
String::new()
};
Ok(mirror.clone())
}
fn mirror_status(
&self,
namespace: Option<&str>,
path: Option<&RepoPath>,
) -> ControlResult<Vec<MirrorConfig>> {
Ok(self
.inner
.lock()
.unwrap()
.mirrors
.iter()
.filter(|((ns, _), _)| namespace.is_none_or(|w| w == ns))
.filter(|((ns, repo), _)| path.is_none_or(|w| w.namespace == *ns && w.repo == *repo))
.map(|(_, mirror)| mirror.clone())
.collect())
}
fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()> {
self.inner
.lock()
.unwrap()
.mirrors
.remove(&MemoryControlPlane::key(path))
.map(|_| ())
.ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))
}
}
pub mod usage_page_token {
pub fn decode(token: &str) -> (String, String) {
let mut parts = token.splitn(3, ';');
match (parts.next(), parts.next(), parts.next()) {
(Some("v1"), Some(cursor), Some(after)) => (cursor.to_owned(), after.to_owned()),
_ => (String::new(), String::new()),
}
}
pub fn encode(cursor: &str, after: &str) -> String {
format!("v1;{cursor};{after}")
}
pub fn page(
rows: Vec<gunnar_proto::NamespaceUsage>,
after: &str,
page_size: usize,
cursor: String,
changed_only: bool,
) -> gunnar_proto::ReadManyUsageResponse {
let page_size = page_size.max(1);
let start = if after.is_empty() { 0 } else { rows.partition_point(|r| r.namespace.as_str() <= after) };
let mut rest: Vec<gunnar_proto::NamespaceUsage> = rows.into_iter().skip(start).collect();
let more = rest.len() > page_size;
rest.truncate(page_size);
let next_page_token = match (more, rest.last()) {
(true, Some(last)) => encode(&cursor, &last.namespace),
_ => String::new(),
};
gunnar_proto::ReadManyUsageResponse {
usages: rest,
next_page_token,
cursor,
changed_only,
as_of_unix_ms: crate::bus::now_unix_ms(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(ns: &str) -> gunnar_proto::NamespaceUsage {
gunnar_proto::NamespaceUsage { namespace: ns.into(), ..Default::default() }
}
#[test]
fn pages_resume_after_the_last_namespace_and_the_last_page_has_no_token() {
let rows = || vec![row("a"), row("b"), row("c"), row("d"), row("e")];
let one = page(rows(), "", 2, "E.1.7".into(), true);
assert_eq!(one.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["a", "b"]);
let (cursor, after) = decode(&one.next_page_token);
assert_eq!((cursor.as_str(), after.as_str()), ("E.1.7", "b"));
let two = page(rows(), &after, 2, cursor.clone(), true);
assert_eq!(two.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["c", "d"]);
let (_, after) = decode(&two.next_page_token);
let three = page(rows(), &after, 2, cursor, true);
assert_eq!(three.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["e"]);
assert!(three.next_page_token.is_empty(), "the last page says so");
assert_eq!(decode("garbage"), (String::new(), String::new()), "a foreign token starts over");
}
}
}