use std::collections::BTreeSet;
use serde_json::{Value, json};
use trust_tasks_rs::specs::git_ns::view::v0_1 as view_wire;
use vti_common::error::AppError;
use super::model::{RepoState, Resource};
use super::ops::now;
use super::rules;
use super::store::Snapshot;
use super::wire;
pub enum Viewer<'a> {
Member(&'a str),
Administrator,
}
fn related(filter: Option<&Resource>, r: &Resource) -> bool {
match filter {
None => true,
Some(f) => f.contains(r) || r.contains(f),
}
}
pub fn build(snap: &Snapshot, viewer: Viewer<'_>, filter: Option<&Resource>) -> Value {
build_with(snap, viewer, filter, Shape::default(), None)
}
#[derive(Debug, Clone, Copy, Default)]
struct Shape {
break_glass: bool,
community_admin: bool,
}
fn build_with(
snap: &Snapshot,
viewer: Viewer<'_>,
filter: Option<&Resource>,
shape: Shape,
within: Option<&BTreeSet<String>>,
) -> Value {
let t = now();
let inside = |ns_id: &str| within.is_none_or(|w| w.contains(ns_id));
let governs = |res: &Resource| match &viewer {
Viewer::Administrator => true,
Viewer::Member(did) => rules::governs(snap, did, res, t),
};
let namespaces: Vec<Value> = snap
.namespaces
.iter()
.filter(|n| related(filter, &n.resource()) && inside(&n.id))
.map(wire::namespace)
.collect();
let mut repos = Vec::new();
for repo in &snap.repos {
let Some(res) = repo.resource() else {
continue;
};
if within.is_some()
&& (!inside(&repo.namespace_id) || snap.namespace(&repo.namespace_id).is_none())
{
continue;
}
if let Some(f) = filter
&& !f.contains(&res)
{
continue;
}
if matches!(viewer, Viewer::Member(_)) && snap.namespace(&repo.namespace_id).is_none() {
continue;
}
if repo.state == RepoState::Unmanaged {
let admin = match &viewer {
Viewer::Administrator => true,
Viewer::Member(did) => rules::governs(snap, did, &res.namespace_resource(), t),
};
if !admin {
continue;
}
}
let owners = rules::owners(snap, &repo.id, t);
repos.push(wire::repo_summary(repo, &owners));
}
let mut rights = Vec::new();
let mut waived = Vec::new();
for (scope, set) in &snap.rights {
let Some(res) = snap.scope_resource(scope) else {
continue;
};
if within.is_some() && !snap.scope_namespace(scope).is_some_and(|n| inside(&n.id)) {
continue;
}
if let Some(f) = filter
&& !f.contains(&res)
{
continue;
}
let governed = governs(&res);
let bg_visible = shape.break_glass && (governed || shape.community_admin);
for row in set.rows.iter() {
let unratified_bg = row.is_recorded(t) && row.is_unratified_break_glass();
if !(row.is_live(t) || (shape.break_glass && unratified_bg)) {
continue;
}
let own = matches!(&viewer, Viewer::Member(did) if *did == row.subject);
if governed || own || (unratified_bg && bg_visible) {
if shape.break_glass {
rights.push(wire::right_record_full(row, &res, governed));
if let Some(w) = super::single_admin::view_entry(row, &res) {
waived.push(w);
}
} else if row.is_live(t) {
rights.push(wire::right_record(row, &res, governed));
}
}
}
}
let mut v = json!({ "namespaces": namespaces, "repos": repos, "rights": rights });
if !waived.is_empty() {
v["ext"] = json!({ "org.openvtc": { super::single_admin::EXT_MARKER: waived } });
}
v
}
pub fn for_member(
snap: &Snapshot,
did: &str,
filter: Option<&Resource>,
) -> Result<view_wire::Response, AppError> {
wire::into(build(snap, Viewer::Member(did), filter))
}
pub fn linked_accounts_of(
member: Option<&crate::members::Member>,
filter: Option<&Resource>,
) -> Vec<Value> {
let Some(forges) = member
.filter(|m| m.removed_at.is_none())
.and_then(|m| m.extensions.get("forges"))
.and_then(Value::as_object)
else {
return Vec::new();
};
let mut out = Vec::new();
for (host, acct) in forges {
if filter.is_some_and(|f| f.forge != *host) {
continue;
}
let s = |k: &str| acct.get(k).and_then(Value::as_str);
let (Some(id), Some(login), Some(at)) = (s("id"), s("login"), s("linkedAt")) else {
continue;
};
out.push(json!({
"account": { "forge": host, "id": id, "login": login },
"linkedAt": at,
}));
}
out
}
pub fn for_member_v2(
snap: &Snapshot,
did: &str,
filter: Option<&Resource>,
member: Option<&crate::members::Member>,
) -> Result<trust_tasks_rs::specs::git_ns::view::v0_2::Response, AppError> {
let mut v = build(snap, Viewer::Member(did), filter);
v["accounts"] = Value::Array(linked_accounts_of(member, filter));
wire::into(v)
}
pub fn for_member_v4(
snap: &Snapshot,
did: &str,
community_admin: bool,
filter: Option<&Resource>,
member: Option<&crate::members::Member>,
) -> Result<trust_tasks_rs::specs::git_ns::view::v0_4::Response, AppError> {
wire::into(member_v4(snap, did, community_admin, filter, member))
}
pub fn member_v4(
snap: &Snapshot,
did: &str,
community_admin: bool,
filter: Option<&Resource>,
member: Option<&crate::members::Member>,
) -> Value {
let mut v = build_with(
snap,
Viewer::Member(did),
filter,
Shape {
break_glass: true,
community_admin,
},
None,
);
v["accounts"] = Value::Array(linked_accounts_of(member, filter));
v
}
pub fn administrator_v5(
snap: &Snapshot,
within: Option<&BTreeSet<String>>,
filter: Option<&Resource>,
member: Option<&crate::members::Member>,
) -> Value {
let mut v = build_with(
snap,
Viewer::Administrator,
filter,
Shape {
break_glass: true,
community_admin: true,
},
within,
);
v["accounts"] = Value::Array(linked_accounts_of(member, filter));
v
}
pub fn narrow_to_break_glass(v: &mut Value) {
let rights: Vec<Value> = v["rights"]
.as_array()
.into_iter()
.flatten()
.filter(|r| r.get("breakGlass").is_some())
.cloned()
.collect();
let held: Vec<Resource> = rights
.iter()
.filter_map(|r| r["resource"].as_str())
.filter_map(|r| Resource::parse(r).ok())
.collect();
let keep_ns = |n: &Value| {
let (Some(forge), Some(owner)) = (n["forge"].as_str(), n["owner"].as_str()) else {
return false;
};
let ns = Resource::namespace(forge, owner);
held.iter().any(|r| ns.contains(r))
};
let keep_repo = |r: &Value| {
r["resource"]
.as_str()
.and_then(|s| Resource::parse(s).ok())
.is_some_and(|res| held.contains(&res))
};
if let Some(ns) = v["namespaces"].as_array_mut() {
ns.retain(keep_ns);
}
if let Some(repos) = v["repos"].as_array_mut() {
repos.retain(keep_repo);
}
v["rights"] = Value::Array(rights);
if let Some(ours) = v
.pointer_mut("/ext/org.openvtc")
.and_then(Value::as_object_mut)
{
ours.remove(super::single_admin::EXT_MARKER);
if ours.is_empty()
&& let Some(ext) = v.get_mut("ext").and_then(Value::as_object_mut)
{
ext.remove("org.openvtc");
}
}
if v.get("ext")
.and_then(Value::as_object)
.is_some_and(serde_json::Map::is_empty)
&& let Some(o) = v.as_object_mut()
{
o.remove("ext");
}
}