use std::fmt;
use crate::error::{Error, Result};
use crate::sandbox::{check_component, RepoRef};
pub type UnixMillis = i64;
pub use gunnar_store::DEFAULT_BRANCH;
pub fn now_ms() -> UnixMillis {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as UnixMillis)
.unwrap_or(0)
}
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum PrincipalKind {
User,
Machine,
Deploy,
Group,
Anonymous,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Principal {
pub id: String,
pub name: String,
pub kind: PrincipalKind,
#[serde(default)]
pub created: UnixMillis,
}
pub const ANONYMOUS_PRINCIPAL_ID: &str = "anonymous";
impl Principal {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
kind: PrincipalKind,
) -> Result<Self> {
let id = id.into();
check_component(&id, &id)?;
if id == ANONYMOUS_PRINCIPAL_ID {
return Err(Error::ReservedPrincipalId(id));
}
Ok(Principal {
id,
name: name.into(),
kind,
created: 0,
})
}
pub fn anonymous() -> Self {
Principal {
id: ANONYMOUS_PRINCIPAL_ID.to_owned(),
name: "anonymous".to_owned(),
kind: PrincipalKind::Anonymous,
created: 0,
}
}
pub fn is_anonymous(&self) -> bool {
self.id == ANONYMOUS_PRINCIPAL_ID
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct PublicKey {
pub fingerprint_sha256: String,
pub algo: String,
pub principal_id: String,
pub added: UnixMillis,
pub last_used: Option<UnixMillis>,
pub expires: Option<UnixMillis>,
pub openssh: String,
}
impl PublicKey {
pub fn is_valid_at(&self, now: UnixMillis) -> bool {
match self.expires {
None => true,
Some(expires) => now < expires,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WebCredential {
pub id: String,
pub principal_id: String,
pub label: String,
pub public_key_hex: String,
pub sign_count: u32,
pub created: UnixMillis,
#[serde(default)]
pub last_used: Option<UnixMillis>,
#[serde(default)]
pub sessions: Vec<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FirstInviteRow {
pub id: String,
pub principal_id: String,
pub secret_hash: String,
pub created_by: String,
pub created: UnixMillis,
pub expires: UnixMillis,
#[serde(default)]
pub note: String,
#[serde(default)]
pub server_level: Option<Level>,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct AccessToken {
pub hash: String,
pub principal_id: String,
pub label: String,
pub created: UnixMillis,
pub last_used: Option<UnixMillis>,
pub expires: Option<UnixMillis>,
}
impl AccessToken {
pub fn is_valid_at(&self, now: UnixMillis) -> bool {
match self.expires {
None => true,
Some(expires) => now < expires,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Namespace {
pub name: String,
#[serde(default)]
pub explode_budget_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub public: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub events_public: bool,
}
#[derive(
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub enum HashKind {
Sha1,
#[default]
Sha256,
}
impl From<HashKind> for gix_hash::Kind {
fn from(kind: HashKind) -> Self {
match kind {
HashKind::Sha1 => gix_hash::Kind::Sha1,
HashKind::Sha256 => gix_hash::Kind::Sha256,
}
}
}
impl TryFrom<gix_hash::Kind> for HashKind {
type Error = Error;
fn try_from(kind: gix_hash::Kind) -> Result<Self> {
match kind {
gix_hash::Kind::Sha1 => Ok(HashKind::Sha1),
gix_hash::Kind::Sha256 => Ok(HashKind::Sha256),
other => Err(Error::Other(anyhow::anyhow!(
"gunnar does not know the hash kind {other}"
))),
}
}
}
impl std::fmt::Display for HashKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
HashKind::Sha1 => "sha1",
HashKind::Sha256 => "sha256",
})
}
}
#[derive(
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Debug,
Default,
serde::Serialize,
serde::Deserialize,
)]
pub enum Visibility {
#[default]
Private,
Public,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Repo {
pub namespace: String,
pub name: String,
pub hash_kind: HashKind,
pub default_branch: String,
pub visibility: Visibility,
pub created: UnixMillis,
pub description: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub encrypted: bool,
}
impl Repo {
pub fn path(&self) -> RepoRef {
RepoRef::new(&self.namespace, &self.name)
.expect("a catalogued repo was built from a validated RepoRef")
}
pub fn key(&self) -> String {
format!("{}/{}", self.namespace, self.name)
}
}
impl fmt::Display for Repo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.namespace, self.name)
}
}
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum Level {
Read,
Write,
Admin,
}
impl Level {
pub fn permits(self, needed: Level) -> bool {
self >= needed
}
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Level::Read => "read",
Level::Write => "write",
Level::Admin => "admin",
})
}
}
#[derive(
Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum Scope {
Server,
Namespace(String),
Repo(RepoRef),
}
impl Scope {
pub fn covers(&self, repo: &Repo) -> bool {
match self {
Scope::Server => true,
Scope::Namespace(name) => name == &repo.namespace,
Scope::Repo(path) => path.namespace() == repo.namespace && path.name() == repo.name,
}
}
pub fn covers_namespace(&self, name: &str) -> bool {
match self {
Scope::Server => true,
Scope::Namespace(scoped) => scoped == name,
Scope::Repo(_) => false,
}
}
}
impl std::fmt::Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scope::Server => f.write_str("server"),
Scope::Namespace(name) => write!(f, "namespace {name}"),
Scope::Repo(path) => write!(f, "repo {path}"),
}
}
}
#[derive(
Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct Grant {
pub principal: String,
pub scope: Scope,
pub level: Level,
}
impl Grant {
pub fn repo(principal: impl Into<String>, path: RepoRef, level: Level) -> Self {
Grant {
principal: principal.into(),
scope: Scope::Repo(path),
level,
}
}
pub fn namespace(
principal: impl Into<String>,
namespace: impl Into<String>,
level: Level,
) -> Self {
Grant {
principal: principal.into(),
scope: Scope::Namespace(namespace.into()),
level,
}
}
pub fn server(principal: impl Into<String>, level: Level) -> Self {
Grant {
principal: principal.into(),
scope: Scope::Server,
level,
}
}
}
pub(crate) fn check_branch(name: &str) -> Result<String> {
if !name.starts_with("refs/heads/") {
return Err(Error::BadBranchName(format!(
"{name:?}: a default branch must be a full name under refs/heads/"
)));
}
gunnar_store::refname::validate(name)
.map_err(|err| Error::BadBranchName(format!("{name:?}: {err}")))?;
Ok(name.to_owned())
}