use super::*;
use crate::auth::tests::store;
use crate::auth::{AuthStore, User};
const PW: &str = "correct horse battery";
fn org(s: &str) -> OrgId {
OrgId::new(s).unwrap()
}
fn as_kind(s: &AuthStore, u: &User, kind: PrincipalKind) -> Principal {
let orgs: Vec<(OrgId, Role)> = s
.memberships(u.id)
.unwrap()
.into_iter()
.map(|m| (m.org, m.role))
.collect();
let orgs = match &kind {
PrincipalKind::ApiToken { org: Some(o), .. } => {
orgs.into_iter().filter(|(x, _)| x == o).collect()
}
_ => orgs,
};
let confined = matches!(&kind, PrincipalKind::ApiToken { org: Some(_), .. });
Principal {
user: u.clone(),
kind,
orgs,
platform_admin: u.platform_admin && !confined,
downscoped: None,
}
}
fn session(s: &AuthStore, u: &User) -> Principal {
as_kind(s, u, PrincipalKind::Session { id: 1 })
}
fn org_token(s: &AuthStore, u: &User, o: &str, scopes: &[&str]) -> Principal {
as_kind(
s,
u,
PrincipalKind::ApiToken {
id: 9,
org: Some(org(o)),
name: "t".into(),
scopes: scopes.iter().map(|x| x.to_string()).collect(),
},
)
}
struct World {
s: AuthStore,
root: User,
owner: User,
admin: User,
member: User,
viewer: User,
outsider: User,
}
fn world() -> World {
let (s, _) = store();
let root = s.create_user("root@x.io", "Root", Some(PW), true).unwrap();
let mk = |e: &str, r: Option<Role>| {
let u = s.create_user(e, e, Some(PW), false).unwrap();
if let Some(r) = r {
s.ensure_org(&org("acme")).unwrap();
s.set_member(&org("acme"), u.id, r).unwrap();
}
u
};
let owner = mk("owner@x.io", Some(Role::Owner));
let admin = mk("admin@x.io", Some(Role::Admin));
let member = mk("member@x.io", Some(Role::Member));
let viewer = mk("viewer@x.io", Some(Role::Viewer));
let outsider = mk("out@x.io", None);
World {
s,
root,
owner,
admin,
member,
viewer,
outsider,
}
}
fn forbidden<T: std::fmt::Debug>(r: R<T>) -> bool {
matches!(r, Err(AuthError::Forbidden(_)))
}
fn hidden<T: std::fmt::Debug>(r: R<T>) -> bool {
matches!(r, Err(AuthError::NotFound(_)))
}
#[test]
fn members_follow_the_role_matrix() {
let w = world();
let (s, acme) = (&w.s, org("acme"));
for u in [&w.owner, &w.admin, &w.member, &w.viewer, &w.root] {
let v = members(s, &session(s, u), &acme).unwrap();
assert_eq!(v["members"].as_array().unwrap().len(), 4);
}
assert!(hidden(members(s, &session(s, &w.outsider), &acme)));
assert!(hidden(invitations(s, &session(s, &w.outsider), &acme)));
assert!(invitations(s, &session(s, &w.admin), &acme).is_ok());
assert!(org_tokens(s, &session(s, &w.owner), &acme).is_ok());
for u in [&w.member, &w.viewer] {
let p = session(s, u);
assert!(forbidden(invitations(s, &p, &acme)));
assert!(forbidden(org_tokens(s, &p, &acme)));
assert!(forbidden(invite(s, &p, &acme, "n@x.io", None, None)));
assert!(forbidden(set_role(s, &p, &acme, w.viewer.id, Role::Member)));
}
let admin = session(s, &w.admin);
assert!(forbidden(invite(
s,
&admin,
&acme,
"n@x.io",
Some(Role::Owner),
None
)));
assert!(forbidden(set_role(
s,
&admin,
&acme,
w.owner.id,
Role::Member
)));
assert!(forbidden(set_role(
s,
&admin,
&acme,
w.member.id,
Role::Owner
)));
assert!(forbidden(remove_member(s, &admin, &acme, w.owner.id)));
set_role(s, &admin, &acme, w.viewer.id, Role::Member).unwrap();
let v = invite(
s,
&admin,
&acme,
"n@x.io",
Some(Role::Admin),
Some("https://isb.x.io/"),
)
.unwrap();
let token = v["token"].as_str().unwrap();
assert_eq!(v["link"], format!("https://isb.x.io/invite#{token}"));
let id = v["invitation"]["id"].as_i64().unwrap();
revoke_invitation(s, &admin, &acme, id).unwrap();
assert!(hidden(revoke_invitation(s, &admin, &acme, id)));
set_role(s, &session(s, &w.owner), &acme, w.admin.id, Role::Owner).unwrap();
remove_member(s, &session(s, &w.viewer), &acme, w.viewer.id).unwrap();
assert!(forbidden(remove_member(
s,
&session(s, &w.member),
&acme,
w.admin.id
)));
}
#[test]
fn scoped_tokens_only_read_and_workspaces_reach_nothing() {
let w = world();
let (s, acme) = (&w.s, org("acme"));
let read = org_token(s, &w.owner, "acme", &["read"]);
assert!(members(s, &read, &acme).is_ok());
assert!(tokens(s, &read, None).is_ok());
assert!(forbidden(invite(s, &read, &acme, "n@x.io", None, None)));
assert!(forbidden(add_ssh_key(s, &read, "ssh-ed25519 AAAA", None)));
assert!(forbidden(revoke_token(s, &read, 1)));
assert!(forbidden(revoke_session(s, &read, 1)));
let admin = org_token(s, &w.owner, "acme", &["admin"]);
assert!(invite(s, &admin, &acme, "n@x.io", None, None).is_ok());
let ws = Principal::workspace(&acme, "dev", Role::Admin);
assert!(forbidden(members(s, &ws, &acme)));
assert!(forbidden(invitations(s, &ws, &acme)));
assert!(forbidden(invite(s, &ws, &acme, "n@x.io", None, None)));
assert!(forbidden(tokens(s, &ws, None)));
assert!(forbidden(org_tokens(s, &ws, &acme)));
assert!(forbidden(ssh_keys(s, &ws)));
assert!(forbidden(sessions(s, &ws)));
assert!(forbidden(users(s, &ws)));
let t = NewToken {
name: "x".into(),
org: Some(acme.clone()),
expires: None,
scopes: vec![],
superadmin: false,
};
assert!(forbidden(create_token(s, &ws, t)));
assert_eq!(me(s, &ws, None).unwrap()["auth"]["kind"], "workspace");
}
fn new_token(o: &str) -> NewToken {
NewToken {
name: "child".into(),
org: Some(org(o)),
expires: Some("1h".into()),
scopes: vec!["read".into()],
superadmin: false,
}
}
#[test]
fn tokens_never_mint_tokens() {
let w = world();
let s = &w.s;
let v = create_token(s, &session(s, &w.member), new_token("acme")).unwrap();
assert!(v["token"].as_str().unwrap().starts_with("isb_tok_"));
assert_eq!(v["info"]["scopes"], json!(["read"]));
assert!(forbidden(create_token(
s,
&session(s, &w.outsider),
new_token("acme")
)));
let access = as_kind(s, &w.member, PrincipalKind::Access);
assert!(create_token(s, &access, new_token("acme")).is_ok());
for scopes in [&[][..], &["admin"][..]] {
let t = org_token(s, &w.owner, "acme", scopes);
let e = create_token(s, &t, new_token("acme")).unwrap_err();
assert!(e.to_string().contains("cannot mint"), "{e}");
}
let sa_token = Superadmin::as_user(
SuperadminSource::Token {
id: 1,
name: "ops".into(),
},
w.root.clone(),
s,
)
.unwrap();
assert!(forbidden(create_token(
s,
&sa_token.principal,
new_token("acme")
)));
let tailnet = Superadmin::as_user(
SuperadminSource::Tailnet {
login: "root@x.io".into(),
node: "laptop".into(),
tags: vec![],
},
w.root.clone(),
s,
)
.unwrap();
assert!(create_token(s, &tailnet.principal, new_token("acme")).is_ok());
let mut t = new_token("acme");
t.superadmin = true;
assert!(forbidden(create_token(s, &session(s, &w.root), t)));
}
#[test]
fn revoking_tokens_and_keys() {
let w = world();
let s = &w.s;
let member = session(s, &w.member);
let v = create_token(s, &member, new_token("acme")).unwrap();
let id = v["info"]["id"].as_i64().unwrap();
assert!(hidden(revoke_token(s, &session(s, &w.viewer), id)));
assert!(hidden(revoke_token(s, &session(s, &w.outsider), id)));
revoke_token(s, &session(s, &w.admin), id).unwrap();
assert!(hidden(revoke_token(s, &member, id)));
let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK85M+Nlyes6IrHWrRVqw80hYRdvPHO+GwqREPk1qxkh alice@laptop";
let k = add_ssh_key(s, &member, key, Some("laptop")).unwrap();
let kid = k["ssh_key"]["id"].as_i64().unwrap();
assert_eq!(
ssh_keys(s, &member).unwrap()["ssh_keys"]
.as_array()
.unwrap()
.len(),
1
);
assert!(
ssh_keys(s, &session(s, &w.viewer)).unwrap()["ssh_keys"]
.as_array()
.unwrap()
.is_empty()
);
assert!(hidden(delete_ssh_key(s, &session(s, &w.viewer), kid)));
delete_ssh_key(s, &member, kid).unwrap();
}
#[test]
fn users_are_for_platform_admins() {
let w = world();
let s = &w.s;
assert!(forbidden(users(s, &session(s, &w.owner))));
let change = UserChange {
disabled: Some(true),
platform_admin: None,
};
assert!(forbidden(update_user(
s,
&session(s, &w.owner),
w.member.id,
&change
)));
let root = session(s, &w.root);
assert_eq!(
users(s, &root).unwrap()["users"].as_array().unwrap().len(),
6
);
assert!(forbidden(update_user(s, &root, w.root.id, &change)));
let v = update_user(s, &root, w.member.id, &change).unwrap();
assert_eq!(v["user"]["disabled"], true);
let t = as_kind(
s,
&w.root,
PrincipalKind::ApiToken {
id: 1,
org: Some(org("acme")),
name: "t".into(),
scopes: vec![],
},
);
assert!(forbidden(users(s, &t)));
}
#[test]
fn platform_admins_act_on_other_accounts() {
let w = world();
let s = &w.s;
let root = session(s, &w.root);
let owner = session(s, &w.owner);
create_token(s, &session(s, &w.member), new_token("acme")).unwrap();
assert!(forbidden(all_tokens(s, &owner)));
let all = all_tokens(s, &root).unwrap();
assert_eq!(all["tokens"][0]["user"]["email"], "member@x.io");
let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK85M+Nlyes6IrHWrRVqw80hYRdvPHO+GwqREPk1qxkh alice@laptop";
let k = add_ssh_key(s, &session(s, &w.member), key, Some("laptop")).unwrap();
let kid = k["ssh_key"]["id"].as_i64().unwrap();
let mine = ssh_keys(s, &session(s, &w.member)).unwrap();
assert!(forbidden(user_ssh_keys(s, &owner, w.member.id)));
assert_eq!(user_ssh_keys(s, &root, w.member.id).unwrap(), mine);
assert!(hidden(user_ssh_keys(s, &root, 999)));
assert!(hidden(delete_user_ssh_key(s, &root, w.viewer.id, kid)));
delete_user_ssh_key(s, &root, w.member.id, kid).unwrap();
}
fn listed(s: &AuthStore, p: &Principal, existing: &[&str]) -> (Vec<String>, Vec<String>) {
let real: Vec<OrgId> = existing.iter().map(|o| org(o)).collect();
let f: OrgsFn = std::sync::Arc::new(move || Ok(real.clone()));
let v = me(s, p, Some(&f)).unwrap();
let names = |a: &Value, k: Option<&str>| -> Vec<String> {
a.as_array()
.unwrap()
.iter()
.map(|x| k.map_or(x, |k| &x[k]).as_str().unwrap().to_string())
.collect()
};
(
names(&v["orgs"], None),
names(&v["memberships"], Some("org")),
)
}
#[test]
fn whoami_lists_the_orgs_that_exist() {
let w = world();
let s = &w.s;
s.ensure_org(&org("stale")).unwrap();
s.set_member(&org("stale"), w.member.id, Role::Admin)
.unwrap();
let (orgs, _) = listed(s, &session(s, &w.root), &["acme", "default", "made-by-cli"]);
assert_eq!(orgs, ["acme", "default", "made-by-cli"]);
let sa = Superadmin::synthetic(SuperadminSource::Token {
id: 1,
name: "ci".into(),
})
.principal;
let (orgs, _) = listed(s, &sa, &["acme", "default"]);
assert_eq!(orgs, ["acme", "default"]);
let (orgs, ms) = listed(s, &session(s, &w.member), &["acme", "default"]);
assert_eq!(orgs, ["acme"]);
assert_eq!(ms, ["acme"]);
let t = org_token(s, &w.root, "acme", &[]);
assert!(listed(s, &t, &["acme", "default"]).0.is_empty());
let v = me(s, &session(s, &w.member), None).unwrap();
assert_eq!(v["orgs"], json!(["acme", "stale"]));
let broken: OrgsFn = std::sync::Arc::new(|| Err("incus is down".into()));
let v = me(s, &session(s, &w.member), Some(&broken)).unwrap();
assert_eq!(v["orgs"], json!(["acme", "stale"]));
}
#[test]
fn deleting_an_org_takes_its_memberships_invitations_and_tokens() {
let w = world();
let s = &w.s;
let acme = org("acme");
s.create_invitation(Some(w.owner.id), &acme, "n@x.io", Role::Member)
.unwrap();
create_token(s, &session(s, &w.member), new_token("acme")).unwrap();
assert!(s.delete_org(&acme).unwrap());
assert!(s.memberships(w.member.id).unwrap().is_empty());
assert!(s.list_members(&acme).unwrap().is_empty());
assert!(s.list_invitations(&acme).unwrap().is_empty());
let v = tokens(s, &session(s, &w.member), None).unwrap();
assert!(v["tokens"].as_array().unwrap().is_empty(), "{v}");
}