From 9de8cdab4704971fb90298c27b2af2408c6cfdf5 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 12 Sep 2024 13:47:37 +0200 Subject: [PATCH 01/24] Remove oso and scaffold new auth policy structure. --- Cargo.lock | 236 +-------- Cargo.toml | 4 - src/cli/ta/signer.rs | 2 +- src/commons/actor.rs | 294 +++++------ .../crypto/signing/dispatch/signerinfo.rs | 16 +- src/commons/eventsourcing/cmd.rs | 8 +- src/commons/eventsourcing/mod.rs | 10 +- src/constants.rs | 23 +- src/daemon/auth/authorizer.rs | 184 ++++--- src/daemon/auth/common/mod.rs | 2 - src/daemon/auth/common/permissions.rs | 81 --- src/daemon/auth/common/session.rs | 50 +- src/daemon/auth/mod.rs | 5 +- src/daemon/auth/policy.rs | 489 ++++++++---------- src/daemon/auth/providers/admin_token.rs | 21 +- .../auth/providers/config_file/config.rs | 2 +- .../auth/providers/config_file/provider.rs | 28 +- .../auth/providers/openid_connect/config.rs | 66 +-- .../providers/openid_connect/jmespathext.rs | 173 ------- .../auth/providers/openid_connect/mod.rs | 1 - .../auth/providers/openid_connect/provider.rs | 159 +++--- src/daemon/ca/manager.rs | 30 +- src/daemon/http/auth.rs | 22 +- src/daemon/http/mod.rs | 55 +- src/daemon/http/server.rs | 80 ++- src/daemon/krillserver.rs | 58 ++- src/daemon/properties/mod.rs | 4 +- src/daemon/scheduler.rs | 10 +- src/pubd/manager.rs | 10 +- src/pubd/repository.rs | 4 +- src/ta/mod.rs | 4 +- src/upgrades/mod.rs | 7 +- 32 files changed, 767 insertions(+), 1371 deletions(-) delete mode 100644 src/daemon/auth/common/permissions.rs delete mode 100644 src/daemon/auth/providers/openid_connect/jmespathext.rs diff --git a/Cargo.lock b/Cargo.lock index 18fc55d3e..fcd69283b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,15 +65,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "0.6.15" @@ -199,8 +190,8 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" dependencies = [ - "lalrpop 0.20.2", - "lalrpop-util 0.20.2", + "lalrpop", + "lalrpop-util", "regex", ] @@ -487,18 +478,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "deunicode" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -1013,17 +992,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "indexmap" version = "2.4.0" @@ -1108,18 +1076,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jmespatch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acf91a732ade34d8eda2dee9500a051833f14f0d3d10d77c149845d6ac6a5f0" -dependencies = [ - "lazy_static", - "serde", - "serde_json", - "slug", -] - [[package]] name = "js-sys" version = "0.3.70" @@ -1182,7 +1138,6 @@ dependencies = [ "hyper", "hyper-util", "intervaltree", - "jmespatch", "kmip-protocol", "kvx", "libflate", @@ -1190,7 +1145,6 @@ dependencies = [ "once_cell", "openidconnect", "openssl", - "oso", "percent-encoding", "pin-project-lite", "r2d2", @@ -1260,28 +1214,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "lalrpop" -version = "0.19.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" -dependencies = [ - "ascii-canvas", - "bit-set", - "diff", - "ena", - "is-terminal", - "itertools 0.10.5", - "lalrpop-util 0.19.12", - "petgraph", - "regex", - "regex-syntax 0.6.29", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", -] - [[package]] name = "lalrpop" version = "0.20.2" @@ -1292,11 +1224,11 @@ dependencies = [ "bit-set", "ena", "itertools 0.11.0", - "lalrpop-util 0.20.2", + "lalrpop-util", "petgraph", "pico-args", "regex", - "regex-syntax 0.8.4", + "regex-syntax", "string_cache", "term", "tiny-keccak", @@ -1304,22 +1236,13 @@ dependencies = [ "walkdir", ] -[[package]] -name = "lalrpop-util" -version = "0.19.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" -dependencies = [ - "regex", -] - [[package]] name = "lalrpop-util" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.7", + "regex-automata", ] [[package]] @@ -1400,27 +1323,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matchers" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "maybe-async" version = "0.2.10" @@ -1665,21 +1573,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "oso" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec41e2da1ce3a82eb807396f802c172f08aa03e1be31e5df49592a04e12c8c7" -dependencies = [ - "impl-trait-for-tuples", - "lazy_static", - "maplit", - "polar-core", - "thiserror", - "tracing", - "tracing-subscriber", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -1806,22 +1699,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "polar-core" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d2b6ee5b5ff6312ca55e2ba75fbd438c72bc041c799055388d815726eca69b" -dependencies = [ - "js-sys", - "lalrpop 0.19.12", - "lalrpop-util 0.19.12", - "regex", - "serde", - "serde_derive", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "postgres" version = "0.19.8" @@ -2040,17 +1917,8 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2061,15 +1929,9 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.4" @@ -2491,15 +2353,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2530,16 +2383,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slug" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" -dependencies = [ - "deunicode", - "wasm-bindgen", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -2973,23 +2816,10 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.75", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -2997,50 +2827,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" -dependencies = [ - "ansi_term", - "chrono", - "lazy_static", - "matchers", - "regex", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -3144,12 +2930,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index a490789a4..1906c5bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ http-body-util = "0.1" hyper = { version = "1.3.1", features = ["server"] } hyper-util = { version = "0.1", features = [ "server" ] } intervaltree = "0.2.6" -jmespatch = { version = "0.3", features = ["sync"], optional = true } kmip = { version = "0.4.2", package = "kmip-protocol", features = [ "tls-with-openssl" ], optional = true } kvx = { version = "0.9.3", features = ["macros"] } libflate = "2.1.0" @@ -44,7 +43,6 @@ log = "0.4" once_cell = { version = "1.7.2", optional = true } openidconnect = { version = "2.0.0", optional = true, default-features = false } openssl = { version = "0.10", features = ["v110"] } -oso = { version = "0.12", optional = true, default-features = false } percent-encoding = "2.3.1" pin-project-lite = "0.2.4" r2d2 = { version = "0.8.9", optional = true } @@ -81,9 +79,7 @@ default = ["multi-user", "hsm"] hsm = ["backoff", "kmip", "once_cell", "cryptoki", "r2d2"] multi-user = [ "basic-cookies", - "jmespatch/sync", "regex", - "oso", "openidconnect", "rpassword", "scrypt", diff --git a/src/cli/ta/signer.rs b/src/cli/ta/signer.rs index fb244a09a..436feea34 100644 --- a/src/cli/ta/signer.rs +++ b/src/cli/ta/signer.rs @@ -116,7 +116,7 @@ impl TrustAnchorSignerManager { .map_err(KrillError::AggregateStoreError)?; let ta_handle = TrustAnchorHandle::new("ta".into()); let signer = config.signer()?; - let actor = Actor::krillta(); + let actor = crate::constants::ACTOR_DEF_KRILLTA; Ok(TrustAnchorSignerManager { store, diff --git a/src/commons/actor.rs b/src/commons/actor.rs index eaf3207f2..cb7188d81 100644 --- a/src/commons/actor.rs +++ b/src/commons/actor.rs @@ -16,63 +16,138 @@ //! to define the Actor that should be created without needing any knowledge //! of the Authorizer. -#[cfg(feature = "multi-user")] -use oso::ToPolar; -#[cfg(feature = "multi-user")] -use std::fmt::Display; +use std::fmt; +use std::sync::Arc; -use std::{collections::HashMap, fmt, fmt::Debug}; + +//------------ Actor --------------------------------------------------------- + +#[derive(Clone, Debug)] +pub struct Actor(ActorName); + +#[derive(Clone, Debug)] +enum ActorName { + /// A system actor for the given component. + System(&'static str), + + /// A user actor that has not been authenticated. + Anonymous, + + /// A user actor with the provided user ID. + User(Arc) +} + +impl Actor { + /// Creates a system actor for the given component. + pub const fn system(component: &'static str) -> Self { + Self(ActorName::System(component)) + } + + /// Creates the anonymous actor. + pub const fn anonymous() -> Self { + Self(ActorName::Anonymous) + } + + /// Creates a user actor with the given user ID. + pub fn user(user_id: impl Into>) -> Self { + Self(ActorName::User(user_id.into())) + } + + /// Returns whether the actor is a system actor. + pub fn is_system(&self) -> bool { + matches!(self.0, ActorName::System(_)) + } + + /// Returns whether the actor is the anonymous actor. + pub fn is_anonymous(&self) -> bool { + matches!(self.0, ActorName::Anonymous) + } + + /// Returns whether the actor is a user actor. + pub fn is_user(&self) -> bool { + matches!(self.0, ActorName::User(_)) + } + + /// Returns the simple name of the actor. + /// + /// For system actors, this is the component name. For the anonymous + /// actor, this is the string `"anonymous"`. For user actors, it is their + /// user ID. + pub fn name(&self) -> &str { + match self.0 { + ActorName::System(ref component) => component, + ActorName::Anonymous => "anonymous", + ActorName::User(ref user_id) => user_id.as_ref(), + } + } + + /// Returns the audit name of the actor. + /// + /// This is the name stored with each command. For system actors, this + /// is the component name. For the anonymous actor, this is the string + /// `"anonymous"`. For user actors, it is the user ID prefixed with + /// `user:`. + pub fn audit_name(&self) -> String { + match self.0 { + ActorName::System(ref component) => component.to_string(), + ActorName::Anonymous => "anonymous".to_string(), + ActorName::User(ref user_id) => { + format!("user:{}", user_id.as_ref()) + } + } + } +} + +impl fmt::Display for Actor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.name()) + } +} + + + + +/* +use std::fmt; +use std::sync::Arc; use crate::{ commons::{ error::{ApiAuthError, Error}, KrillResult, }, - constants::ACTOR_DEF_ANON, - daemon::auth::{policy::AuthPolicy, Auth}, + daemon::auth::{policy::{AuthPolicy, Permission}, Auth, Handle}, }; -#[derive(Clone, Eq, PartialEq, Debug)] +#[derive(Clone, Deserialize, Eq, PartialEq, Debug, Serialize)] pub enum ActorName { - AsStaticStr(&'static str), AsString(String), } impl ActorName { pub fn as_str(&self) -> &str { match &self { - ActorName::AsStaticStr(s) => s, ActorName::AsString(s) => s, } } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Attributes { - None, - RoleOnly(&'static str), - UserDefined(HashMap), +impl From for ActorName { + fn from(src: String) -> Self { + Self::AsString(src) + } } -impl Attributes { - pub fn as_map(&self) -> HashMap { - match &self { - Attributes::UserDefined(map) => map.clone(), - Attributes::RoleOnly(role) => { - let mut map = HashMap::new(); - map.insert("role".to_string(), role.to_string()); - map - } - Attributes::None => HashMap::new(), - } +impl fmt::Display for ActorName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.as_str().fmt(f) } } + #[derive(Clone, Debug)] pub struct ActorDef { pub name: ActorName, - pub is_user: bool, - pub attributes: Attributes, pub new_auth: Option, pub auth_error: Option, } @@ -81,32 +156,25 @@ impl ActorDef { pub const fn anonymous() -> ActorDef { ActorDef { name: ActorName::AsStaticStr("anonymous"), - is_user: false, - attributes: Attributes::None, new_auth: None, auth_error: None, } } - pub const fn system(name: &'static str, role: &'static str) -> ActorDef { + pub const fn system(name: &'static str) -> ActorDef { ActorDef { name: ActorName::AsStaticStr(name), - attributes: Attributes::RoleOnly(role), - is_user: false, new_auth: None, auth_error: None, } } pub fn user( - name: String, - attributes: HashMap, + name: ActorName, new_auth: Option, ) -> ActorDef { ActorDef { - name: ActorName::AsString(name), - is_user: true, - attributes: Attributes::UserDefined(attributes), + name, new_auth, auth_error: None, } @@ -122,12 +190,8 @@ impl ActorDef { #[derive(Clone)] pub struct Actor { name: ActorName, - is_user: bool, - attributes: Attributes, new_auth: Option, - - #[cfg_attr(not(feature = "multi-user"), allow(dead_code))] - policy: Option, + policy: Arc, #[cfg_attr(not(feature = "multi-user"), allow(dead_code))] auth_error: Option, @@ -136,16 +200,12 @@ pub struct Actor { impl PartialEq for Actor { fn eq(&self, other: &Self) -> bool { self.name == other.name - && self.is_user == other.is_user - && self.attributes == other.attributes } } impl PartialEq for Actor { fn eq(&self, other: &ActorDef) -> bool { self.name == other.name - && self.is_user == other.is_user - && self.attributes == other.attributes } } @@ -169,17 +229,19 @@ impl Actor { /// Should only be used for system users, i.e. not for mapping /// logged in users. - pub fn actor_from_def(actor_def: ActorDef) -> Actor { + pub fn actor_from_def(_actor_def: ActorDef) -> Actor { + unimplemented!() + /* Actor { name: actor_def.name.clone(), - is_user: actor_def.is_user, - attributes: actor_def.attributes, new_auth: None, auth_error: None, policy: None, } + */ } + /* /// Only for use in testing pub fn test_from_details( name: String, @@ -194,132 +256,65 @@ impl Actor { policy: None, } } + */ - pub fn new(actor_def: ActorDef, policy: AuthPolicy) -> Actor { + pub fn new(actor_def: ActorDef, policy: Arc) -> Actor { Actor { name: actor_def.name.clone(), - is_user: actor_def.is_user, - attributes: actor_def.attributes.clone(), new_auth: actor_def.new_auth.clone(), auth_error: actor_def.auth_error, - policy: Some(policy), + policy, } } pub fn is_user(&self) -> bool { - self.is_user + unimplemented!() } pub fn is_anonymous(&self) -> bool { - self == &ACTOR_DEF_ANON + unimplemented!() } pub fn new_auth(&self) -> Option { self.new_auth.clone() } - pub fn attributes(&self) -> HashMap { - self.attributes.as_map() - } - - pub fn attribute(&self, attr_name: String) -> Option { - match &self.attributes { - Attributes::UserDefined(map) => map.get(&attr_name).cloned(), - Attributes::RoleOnly(role) if &attr_name == "role" => { - Some(role.to_string()) - } - Attributes::RoleOnly(_) => None, - Attributes::None => None, - } - } - pub fn name(&self) -> &str { self.name.as_str() } - #[cfg(not(feature = "multi-user"))] - pub fn is_allowed(&self, _: A, _: R) -> KrillResult { - // When not in multi-user mode we only have two states: authenticated - // or not authenticated (aka anonymous). Only authenticated - // (i.e. not anonymous) actors are permitted to perform restricted - // actions, i.e. those for which this fn is invoked. - Ok(!self.is_anonymous()) - } - - #[cfg(feature = "multi-user")] - pub fn is_allowed( + pub fn is_allowed( &self, - action: A, - resource: R, - ) -> KrillResult - where - A: ToPolar + Display + Debug + Clone, - R: ToPolar + Display + Debug + Clone, - { - if log_enabled!(log::Level::Trace) { - trace!( - "Access check: actor={}, action={}, resource={}", - self.name(), - &action, - &resource - ); - } + permission: Permission, + resource: Option<&Handle>, + ) -> KrillResult { + trace!( + "Access check: actor={}, permission={}, resource={:?}", + self.name(), permission, resource + ); if let Some(api_error) = &self.auth_error { trace!( - "Authentication denied: actor={}, action={}, resource={}: {}", + "Authentication denied: \ + actor={}, permission={}, resource={:?}: {}", self.name(), - &action, - &resource, - &api_error + permission, + resource, + api_error ); return Err(Error::from(api_error.clone())); } - match &self.policy { - Some(policy) => match policy.is_allowed( - self.clone(), - action.clone(), - resource.clone(), - ) { - Ok(allowed) => { - if log_enabled!(log::Level::Trace) { - trace!( - "Access {}: actor={:?}, action={:?}, resource={:?}", - if allowed { "granted" } else { "denied" }, - self, - &action, - &resource - ); - } - Ok(allowed) - } - Err(err) => { - error!( - "Access denied: actor={}, action={}, resource={}: {}", - self.name(), - &action, - &resource, - err - ); - Ok(false) - } - }, - None => { - // Auth policy is required, can only be omitted for use by - // test rules inside an Oso policy. We should - // never get here, but we don't want to crash - // Krill by calling unreachable!(). - error!( - "Unable to check access: actor={}, action={}, resource={}: {}", - self.name(), - &action, - &resource, - "Internal error: missing policy" - ); - Ok(false) - } - } + let allowed = self.policy.is_allowed(permission, resource); + trace!( + "Access {}: actor={:?}, permission={:?}, \ + resource={:?}", + if allowed { "granted" } else { "denied" }, + self, + permission, + resource + ); + Ok(allowed) } } @@ -331,12 +326,7 @@ impl fmt::Display for Actor { impl fmt::Debug for Actor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Actor(name={:?}, is_user={}, attr={:?})", - self.name(), - self.is_user, - self.attributes - ) + write!(f, "Actor(name={:?})", self.name()) } } +*/ diff --git a/src/commons/crypto/signing/dispatch/signerinfo.rs b/src/commons/crypto/signing/dispatch/signerinfo.rs index ef78744d8..a63ad003d 100644 --- a/src/commons/crypto/signing/dispatch/signerinfo.rs +++ b/src/commons/crypto/signing/dispatch/signerinfo.rs @@ -11,7 +11,6 @@ use url::Url; use crate::{ commons::{ - actor::Actor, api::CommandSummary, crypto::SignerHandle, error::Error, @@ -237,8 +236,7 @@ impl SignerInfoCommand { *key_id, internal_key_id.to_string(), ); - let actor = Actor::actor_from_def(ACTOR_DEF_KRILL); - Self::new(id, version, details, &actor) + Self::new(id, version, details, &ACTOR_DEF_KRILL) } pub fn remove_key( @@ -247,8 +245,7 @@ impl SignerInfoCommand { key_id: &KeyIdentifier, ) -> Self { let details = SignerInfoCommandDetails::RemoveKey(*key_id); - let actor = Actor::actor_from_def(ACTOR_DEF_KRILL); - Self::new(id, version, details, &actor) + Self::new(id, version, details, &ACTOR_DEF_KRILL) } pub fn change_signer_name( @@ -259,8 +256,7 @@ impl SignerInfoCommand { let details = SignerInfoCommandDetails::ChangeSignerName( signer_name.to_string(), ); - let actor = Actor::actor_from_def(ACTOR_DEF_KRILL); - Self::new(id, version, details, &actor) + Self::new(id, version, details, &ACTOR_DEF_KRILL) } pub fn change_signer_info( @@ -271,8 +267,7 @@ impl SignerInfoCommand { let details = SignerInfoCommandDetails::ChangeSignerInfo( signer_info.to_string(), ); - let actor = Actor::actor_from_def(ACTOR_DEF_KRILL); - Self::new(id, version, details, &actor) + Self::new(id, version, details, &ACTOR_DEF_KRILL) } } @@ -494,7 +489,6 @@ impl SignerMapper { )) })?; - let actor = Actor::system_actor(); let cmd = SignerInfoInitCommand::new( &signer_handle, SignerInfoInitCommandDetails { @@ -504,7 +498,7 @@ impl SignerMapper { public_key: public_key.clone(), private_key_internal_id: private_key_internal_id.to_string(), }, - &actor, + &ACTOR_DEF_KRILL, ); self.store.add(cmd)?; diff --git a/src/commons/eventsourcing/cmd.rs b/src/commons/eventsourcing/cmd.rs index bd213a43c..efcd5eaaf 100644 --- a/src/commons/eventsourcing/cmd.rs +++ b/src/commons/eventsourcing/cmd.rs @@ -183,17 +183,11 @@ impl SentCommand { details: C, actor: &Actor, ) -> Self { - let actor_name = if actor.is_user() { - format!("user:{}", actor.name()) - } else { - actor.name().to_string() - }; - SentCommand { handle: id.clone(), version, details, - actor: actor_name, + actor: actor.audit_name(), } } diff --git a/src/commons/eventsourcing/mod.rs b/src/commons/eventsourcing/mod.rs index c3e6d2d3a..f606af8a2 100644 --- a/src/commons/eventsourcing/mod.rs +++ b/src/commons/eventsourcing/mod.rs @@ -41,7 +41,6 @@ mod tests { use crate::{ commons::{ - actor::Actor, api::{CommandHistoryCriteria, CommandSummary}, }, constants::ACTOR_DEF_TEST, @@ -74,11 +73,10 @@ mod tests { impl PersonInitCommand { fn make(id: &MyHandle, name: String) -> Self { - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); PersonInitCommand::new( id, PersonInitCommandDetails { name }, - &actor, + &ACTOR_DEF_TEST, ) } } @@ -194,12 +192,11 @@ mod tests { impl PersonCommand { pub fn go_around_sun(id: &MyHandle, version: Option) -> Self { - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); Self::new( id, version, PersonCommandDetails::GoAroundTheSun, - &actor, + &ACTOR_DEF_TEST, ) } @@ -209,8 +206,7 @@ mod tests { s: &str, ) -> Self { let details = PersonCommandDetails::ChangeName(s.to_string()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); - Self::new(id, version, details, &actor) + Self::new(id, version, details, &ACTOR_DEF_TEST) } } diff --git a/src/constants.rs b/src/constants.rs index 4b4f6ec71..b7f721c74 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,9 +1,6 @@ use kvx::Namespace; - -use crate::{ - commons::{actor::ActorDef, eventsourcing::namespace}, - daemon::auth::common::NoResourceType, -}; +use crate::commons::actor::Actor; +use crate::commons::eventsourcing::namespace; pub const KRILL_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const KRILL_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); @@ -103,18 +100,14 @@ pub const HTTP_CLIENT_TIMEOUT_SECS: u64 = 120; pub const HTTP_USER_AGENT_TRUNCATE: usize = 256; // Will truncate received user-agent values at this size. pub const OPENID_CONNECT_HTTP_CLIENT_TIMEOUT_SECS: u64 = 30; -pub const NO_RESOURCE: NoResourceType = NoResourceType; - -pub const ACTOR_DEF_KRILL: ActorDef = ActorDef::system("krill", "admin"); -pub const ACTOR_DEF_KRILLTA: ActorDef = ActorDef::system("krillta", "admin"); -pub const ACTOR_DEF_ANON: ActorDef = ActorDef::anonymous(); -pub const ACTOR_DEF_ADMIN_TOKEN: ActorDef = - ActorDef::system("admin-token", "admin"); -pub const ACTOR_DEF_TESTBED: ActorDef = - ActorDef::system("testbed", "testbed"); +pub const ACTOR_DEF_KRILL: Actor = Actor::system("krill"); +pub const ACTOR_DEF_KRILLTA: Actor = Actor::system("krillta"); +pub const ACTOR_DEF_ANON: Actor = Actor::anonymous(); +pub const ACTOR_DEF_ADMIN_TOKEN: Actor = Actor::system("admin-token"); +pub const ACTOR_DEF_TESTBED: Actor = Actor::system("testbed"); #[cfg(test)] -pub const ACTOR_DEF_TEST: ActorDef = ActorDef::system("test", "admin"); +pub const ACTOR_DEF_TEST: Actor = Actor::system("test"); // Note: These must match the values used by Lagosta. #[cfg(feature = "multi-user")] diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 976b0f42d..836869965 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -1,20 +1,22 @@ //! Authorization for the API -use std::{any::Any, collections::HashMap, fmt, str::FromStr, sync::Arc}; - +use std::fmt; +use std::any::Any; +use std::str::FromStr; +use std::sync::Arc; use rpki::ca::idexchange::{InvalidHandle, MyHandle}; +use crate::commons::actor::Actor; +use crate::commons::error::ApiAuthError; use crate::{ commons::{ - actor::{Actor, ActorDef}, api::Token, error::Error, KrillResult, }, - constants::{ACTOR_DEF_ANON, NO_RESOURCE}, daemon::{ auth::{ - common::permissions::Permission, policy::AuthPolicy, + policy::{AuthPolicy, AuthPolicyMap, Permission}, providers::AdminTokenAuthProvider, }, config::Config, @@ -27,7 +29,7 @@ use crate::daemon::auth::providers::{ ConfigFileAuthProvider, OpenIDConnectAuthProvider, }; -//------------ Authorizer ---------------------------------------------------- +//------------ AuthProvider -------------------------------------------------- /// An AuthProvider authenticates and authorizes a given token. /// @@ -77,7 +79,7 @@ impl AuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> KrillResult> { match &self { AuthProvider::Token(provider) => provider.authenticate(request), #[cfg(feature = "multi-user")] @@ -134,13 +136,15 @@ impl AuthProvider { } } + +//------------ Authorizer ---------------------------------------------------- + /// This type is responsible for checking authorizations when the API is /// accessed. pub struct Authorizer { primary_provider: AuthProvider, legacy_provider: Option, - policy: AuthPolicy, - private_attributes: Vec, + policy: AuthPolicyMap, } impl Authorizer { @@ -178,58 +182,47 @@ impl Authorizer { Some(AdminTokenAuthProvider::new(config.clone())) }; - #[cfg(feature = "multi-user")] - let private_attributes = config.auth_private_attributes.clone(); - #[cfg(not(feature = "multi-user"))] - let private_attributes = vec!["role".to_string()]; - Ok(Authorizer { primary_provider, legacy_provider, - policy: AuthPolicy::new(config)?, - private_attributes, + policy: AuthPolicyMap::new(&config)?, }) } - pub async fn actor_from_request(&self, request: &HyperRequest) -> Actor { + /// Authenticates an HTTP request. + pub async fn authenticate_request( + &self, request: &HyperRequest + ) -> AuthInfo { trace!("Determining actor for request {:?}", &request); // Try the legacy provider first, if any - let mut authenticate_res = match &self.legacy_provider { + let authenticate_res = match &self.legacy_provider { Some(provider) => provider.authenticate(request), None => Ok(None), }; // Try the real provider if we did not already successfully // authenticate - authenticate_res = match authenticate_res { + let authenticate_res = match authenticate_res { Ok(Some(res)) => Ok(Some(res)), _ => self.primary_provider.authenticate(request).await, }; // Create an actor based on the authentication result - let actor = match authenticate_res { + let res = match authenticate_res { // authentication success - Ok(Some(actor_def)) => self.actor_from_def(actor_def), + Ok(Some(res)) => res, // authentication failure - Ok(None) => self.actor_from_def(ACTOR_DEF_ANON), + Ok(None) => AuthInfo::anonymous(), // error during authentication - Err(err) => { - // receives a commons::error::Error, but we need an - // ApiAuthError - self.actor_from_def(ACTOR_DEF_ANON.with_auth_error(err)) - } + Err(err) => AuthInfo::error(err), }; - trace!("Actor determination result: {:?}", &actor); + trace!("Actor determination result: {:?}", res); - actor - } - - pub fn actor_from_def(&self, def: ActorDef) -> Actor { - Actor::new(def, self.policy.clone()) + res } /// Return the URL at which an end-user should be directed to login with @@ -238,39 +231,30 @@ impl Authorizer { self.primary_provider.get_login_url().await } - /// Submit credentials directly to the configured provider to establish a - /// login session, if supported by the configured provider. + /// Establish an authenticated session from credentials in an HTTP request. pub async fn login( - &self, - request: &HyperRequest, + &self, request: &HyperRequest ) -> KrillResult { let user = self.primary_provider.login(request).await?; // The user has passed authentication, but may still not be // authorized to login as that requires a check against the policy // which cannot be done by the AuthProvider. Check that now. - let actor_def = - ActorDef::user(user.id.clone(), user.attributes.clone(), None); - let actor = self.actor_from_def(actor_def); - if !actor.is_allowed(Permission::LOGIN, NO_RESOURCE)? { - let reason = format!("Login denied for user '{}': User is not permitted to 'LOGIN'", user.id); + + if !self.policy.is_user_allowed( + &user.id, Permission::LOGIN, None + ) { + let reason = format!( + "Login denied for user '{}': \ + User is not permitted to 'LOGIN'", + user.id + ); warn!("{}", reason); return Err(Error::ApiInsufficientRights(reason)); } - - // Exclude private attributes before passing them to Lagosta to be - // shown in the web UI. - let visible_attributes = user - .attributes - .clone() - .into_iter() - .filter(|(k, _)| !self.private_attributes.contains(k)) - .collect::>(); - let filtered_user = LoggedInUser { token: user.token, id: user.id, - attributes: visible_attributes, }; if log_enabled!(log::Level::Trace) { @@ -290,15 +274,93 @@ impl Authorizer { ) -> KrillResult { self.primary_provider.logout(request).await } + + pub fn get_policy(&self, actor: &Actor) -> Arc { + self.policy.get_policy(actor) + } + + pub fn is_allowed( + &self, + actor: &Actor, + permission: Permission, + resource: Option<&Handle> + ) -> bool { + self.policy.is_allowed(actor, permission, resource) + } } + +//------------ LoggedInUser -------------------------------------------------- + +/// Information to be returned to the caller after login. +/// +/// This may be serialized into a JSON response. #[derive(Serialize, Debug)] pub struct LoggedInUser { + /// The API token to use in subsequent calls. pub token: Token, + + /// The user ID. + // XXX Swith to using Arc. May require Serialize shenanigans. pub id: String, - pub attributes: HashMap, } + +//------------ AuthInfo ------------------------------------------------------ + +/// Information about the result of trying to authenticate a request. +#[derive(Clone, Debug)] +pub struct AuthInfo { + /// The actor for the authenticated user. + pub actor: Actor, + + /// Optional error information if authentication failed. + pub auth_error: Option, + + /// Optional authentication information to be included in a response. + pub new_auth: Option, +} + +impl AuthInfo { + pub fn user(user_id: impl Into>) -> Self { + Self { + actor: Actor::user(user_id), + auth_error: None, + new_auth: None, + } + } + + fn anonymous() -> Self { + Self { + actor: Actor::anonymous(), + auth_error: None, + new_auth: None, + } + } + + fn error(err: impl Into) -> Self { + Self { + actor: Actor::anonymous(), + auth_error: Some(err.into()), + new_auth: None + } + } + + pub fn with_new_auth( + user_id: impl Into>, + new_auth: Auth + ) -> Self { + Self { + actor: Actor::user(user_id), + auth_error: None, + new_auth: Some(new_auth) + } + } +} + + +//------------ Auth ---------------------------------------------------------- + #[derive(Clone, Debug)] pub enum Auth { Bearer(Token), @@ -376,17 +438,3 @@ impl AsRef for Handle { } } -#[cfg(feature = "multi-user")] -impl oso::PolarClass for Handle { - fn get_polar_class() -> oso::Class { - Self::get_polar_class_builder() - .set_constructor(|name: String| Handle::from_str(&name).unwrap()) - .set_equality_check(|left: &Handle, right: &Handle| left == right) - .add_attribute_getter("name", |instance| instance.to_string()) - .build() - } - - fn get_polar_class_builder() -> oso::ClassBuilder { - oso::Class::builder() - } -} diff --git a/src/daemon/auth/common/mod.rs b/src/daemon/auth/common/mod.rs index ff8a7f68c..6d96e7754 100644 --- a/src/daemon/auth/common/mod.rs +++ b/src/daemon/auth/common/mod.rs @@ -1,8 +1,6 @@ #[cfg(feature = "multi-user")] pub mod crypt; -pub mod permissions; - #[derive(Debug, Clone)] pub struct NoResourceType; impl std::fmt::Display for NoResourceType { diff --git a/src/daemon/auth/common/permissions.rs b/src/daemon/auth/common/permissions.rs deleted file mode 100644 index 7c5a4224c..000000000 --- a/src/daemon/auth/common/permissions.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Based on https://github.com/rust-lang/rfcs/issues/284#issuecomment-277871931 -// Use a macro to build the Permission enum so that we can iterate over the -// enum variants when adding them as Polar constants in struct AuthPolicy. -// This ensures that we don't accidentally miss one. We can also implement the -// Display trait that we need Actor::is_allowed() and the FromStr trait and -// avoid labour intensive and error prone duplication of the enum variants -// that would be needed when implementing the traits manually. -macro_rules! iterable_enum { - ($name:ident { $($variant:ident),* }) => ( - #[allow(non_camel_case_types)] - #[derive(Clone, Debug, Eq, PartialEq)] - pub enum $name { $($variant),* } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - $( Self::$variant => write!(f, stringify!($variant)) ),+ - } - } - } - - impl std::str::FromStr for $name { - type Err = String; - - fn from_str(input: &str) -> Result { - match input { - $( stringify!($variant) => { Ok($name::$variant) } - ),+ - _ => Err(format!("Unknown {} '{}'", stringify!($name), input)) - } - } - } - - impl $name { - pub fn iter() -> Iter { - Iter(None) - } - } - - pub struct Iter(Option<$name>); - - impl Iterator for Iter { - type Item = $name; - - fn next(&mut self) -> Option { - match self.0 { - None => $( { self.0 = Some($name::$variant); Some($name::$variant) }, - Some($name::$variant) => )* None, - } - } - } - ); -} - -iterable_enum! { - Permission { - LOGIN, - PUB_ADMIN, - PUB_LIST, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - CA_LIST, - CA_READ, - CA_CREATE, - CA_UPDATE, - CA_ADMIN, - CA_DELETE, - ROUTES_READ, - ROUTES_UPDATE, - ROUTES_ANALYSIS, - ASPAS_READ, - ASPAS_UPDATE, - ASPAS_ANALYSIS, - BGPSEC_READ, - BGPSEC_UPDATE, - RTA_LIST, - RTA_READ, - RTA_UPDATE - } -} diff --git a/src/daemon/auth/common/session.rs b/src/daemon/auth/common/session.rs index 1a209cc90..110ea9013 100644 --- a/src/daemon/auth/common/session.rs +++ b/src/daemon/auth/common/session.rs @@ -1,16 +1,14 @@ -use std::{ - collections::HashMap, - sync::RwLock, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::engine::Engine as _; +use crate::commons::api::Token; +use crate::commons::error::Error; +use crate::commons::KrillResult; +use crate::daemon::auth::common::crypt; +use crate::daemon::auth::common::crypt::{CryptState, NonceState}; -use crate::{ - commons::{api::Token, error::Error, KrillResult}, - daemon::auth::common::crypt::{self, CryptState, NonceState}, -}; const MAX_CACHE_SECS: u64 = 30; @@ -18,8 +16,7 @@ const MAX_CACHE_SECS: u64 = 30; pub struct ClientSession { pub start_time: u64, pub expires_in: Option, - pub id: String, - pub attributes: HashMap, + pub user_id: Arc, pub secrets: HashMap, } @@ -49,8 +46,8 @@ impl ClientSession { }; trace!( - "Login session status check: id={}, status={:?}, max age={} secs, cur age={} secs", - &self.id, + "Login session status check: user_id={}, status={:?}, max age={} secs, cur age={} secs", + &self.user_id, &status, max_age_secs, cur_age_secs @@ -190,8 +187,7 @@ impl LoginSessionCache { pub fn encode( &self, - id: &str, - attributes: &HashMap, + user_id: Arc, secrets: HashMap, crypt_state: &CryptState, expires_in: Option, @@ -199,8 +195,7 @@ impl LoginSessionCache { let session = ClientSession { start_time: Self::time_now_secs_since_epoch()?, expires_in, - id: id.to_string(), - attributes: attributes.clone(), + user_id, secrets, }; @@ -233,7 +228,7 @@ impl LoginSessionCache { add_to_cache: bool, ) -> KrillResult { if let Some(session) = self.lookup_session(&token) { - trace!("Session cache hit for session id {}", &session.id); + trace!("Session cache hit for session id {}", &session.user_id); return Ok(session); } else { trace!("Session cache miss, deserializing..."); @@ -264,7 +259,7 @@ impl LoginSessionCache { trace!( "Session cache miss, deserialized session id {}", - &session.id + &session.user_id ); if add_to_cache { @@ -341,13 +336,12 @@ mod tests { // Add an item to the cache and verify that the cache now has 1 item let item1_token = cache - .encode("some id", &HashMap::new(), HashMap::new(), &key, None) + .encode("some id".into(), HashMap::new(), &key, None) .unwrap(); assert_eq!(cache.size(), 1); let item1 = cache.decode(item1_token, &key, true).unwrap(); - assert_eq!(item1.id, "some id"); - assert_eq!(item1.attributes, HashMap::new()); + assert_eq!(item1.user_id.as_ref(), "some id"); assert_eq!(item1.expires_in, None); assert_eq!(item1.secrets, HashMap::new()); @@ -358,12 +352,10 @@ mod tests { assert_eq!(cache.size(), 1); // Add another item to the cache - let some_attrs = one_attr_map("some attr key", "some attr val"); let some_secrets = one_attr_map("some secret key", "some secret val"); let item2_token = cache .encode( - "other id", - &some_attrs, + "other id".into(), some_secrets, &key, Some(Duration::from_secs(10)), @@ -383,11 +375,7 @@ mod tests { assert_eq!(cache.size(), 1); let item2 = cache.decode(item2_token, &key, true).unwrap(); - assert_eq!(item2.id, "other id"); - assert_eq!( - item2.attributes, - one_attr_map("some attr key", "some attr val") - ); + assert_eq!(item2.user_id.as_ref(), "other id"); assert_eq!(item2.expires_in, Some(Duration::from_secs(10))); assert_eq!( item2.secrets, diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index c7efec63a..492a51594 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -20,4 +20,7 @@ pub mod policy { } } -pub use authorizer::{Auth, AuthProvider, Authorizer, Handle, LoggedInUser}; +pub use authorizer::{ + Auth, AuthInfo, AuthProvider, Authorizer, Handle, LoggedInUser +}; + diff --git a/src/daemon/auth/policy.rs b/src/daemon/auth/policy.rs index 28e5438f3..3290c7349 100644 --- a/src/daemon/auth/policy.rs +++ b/src/daemon/auth/policy.rs @@ -1,299 +1,248 @@ -use std::{io::Read, str::FromStr, sync::Arc}; - -use oso::{Oso, PolarClass, PolarValue, ToPolar}; - -use crate::{ - commons::{ - actor::Actor, - error::{Error, KrillIoError}, - KrillResult, - }, - constants::{ - ACTOR_DEF_ADMIN_TOKEN, ACTOR_DEF_ANON, ACTOR_DEF_KRILL, - ACTOR_DEF_TESTBED, - }, - daemon::{ - auth::{ - common::{permissions::Permission, NoResourceType}, - Handle, - }, - config::Config, - }, -}; - -#[derive(Clone)] -pub struct AuthPolicy { - oso: Arc, +use std::fmt; +use std::collections::HashMap; +use std::sync::Arc; +use crate::commons::KrillResult; +use crate::commons::actor::Actor; +use crate::daemon::auth::Handle; +use crate::daemon::config::Config; + + +//------------ Role ---------------------------------------------------------- + +/// The role of actor has. +/// +/// Permissions aren’t assigned to actors directly but rather to roles to +/// which actors are assigned in turn. +/// +/// Most roles are defined through a string in configuration. However, there +/// are two special roles for anonymous actors and system actors. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Role(RoleEnum); + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum RoleEnum { + /// The role used by system actors. + System, + + /// The role used for anonymous users. + Anonymous, + + /// A user role. + User(Arc) } -impl std::ops::Deref for AuthPolicy { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.oso +impl Role { + /// Creates a new user role with the given name. + pub fn user(name: impl Into>) -> Self { + Self(RoleEnum::User(name.into().into())) } -} -impl AuthPolicy { - pub fn new(config: Arc) -> KrillResult { - let mut oso = Oso::new(); - oso.register_class(Actor::get_polar_class()).unwrap(); - oso.register_class(Handle::get_polar_class()).unwrap(); - - // Register both the Permission enum as a Polar class and its variants - // as Polar constants. The former is useful for writing Polar - // rules that only match on actual Krill Permissions, not on arbitrary - // strings, e.g. `allow(actor, action: Permission, resource)`. - // The latter is useful when writing rules that depend on a - // specific permission, e.g. `if action = CA_READ`. Without the - // variants as constants we would have to create a - // new Permission each time, converting from a string to the - // Permission type, e.g. `action = new Permission("CA_READ")`. - oso.register_class(Permission::get_polar_class()).unwrap(); - for permission in Permission::iter() { - let name = format!("{}", permission); - oso.register_constant(permission, &name).unwrap(); - } + /// Creates a new system role. + pub const fn system() -> Self { + Self(RoleEnum::System) + } - // Load built-in Polar authorization policy rules from embedded - // strings - Self::load_internal_policy( - &mut oso, - include_bytes!("../../../defaults/roles.polar"), - "roles", - )?; - Self::load_internal_policy( - &mut oso, - include_bytes!("../../../defaults/rules.polar"), - "rules", - )?; - Self::load_internal_policy( - &mut oso, - include_bytes!("../../../defaults/aliases.polar"), - "aliases", - )?; - Self::load_internal_policy( - &mut oso, - include_bytes!("../../../defaults/rbac.polar"), - "rbac", - )?; - Self::load_internal_policy( - &mut oso, - include_bytes!("../../../defaults/abac.polar"), - "abac", - )?; - - // Load additional policy rules from files optionally provided by the - // customer - Self::load_user_policy(config, &mut oso)?; - - // Sanity check: Verify the roles assigned to the built-in actors are - // as expected. - debug!("Running Polar self checks"); - - // The "krill" built-in actor is used to attribute internal actions by - // Krill that were not directly triggered by a user. This user should - // have the "admin" role. - Self::exec_query( - &mut oso, - r#"actor_has_role(Actor.builtin("krill"), "admin")"#, - )?; - - // The "admin-token" built-in actor is used for logins using the admin - // token (aka the "admin_token" set in the config file or via env - // var). This actor should have the "admin" role. - Self::exec_query( - &mut oso, - r#"actor_has_role(Actor.builtin("admin-token"), "admin")"#, - )?; - - // The built-in test actor "anon" represents a not-logged-in user and - // as such lacks a role. We should be able to test that it the actor - // does not have any role (represented by the _ placeholder in Oso - // Polar syntax). - Self::exec_query( - &mut oso, - r#"not actor_has_role(Actor.builtin("anon"), _)"#, - )?; - - // The built-in test actor "testbed" represents an anonymous user that - // is using the testbed UI/API and is temporarily upgraded with the - // necessary rights to perform the testbed related actions. These - // actions are grouped into a "testbed" role. The "testbed" actor - // should have the "testbed" role. - Self::exec_query( - &mut oso, - r#"actor_has_role(Actor.builtin("testbed"), "testbed")"#, - )?; - - Ok(AuthPolicy { oso: Arc::new(oso) }) + /// Creates a new anonymous role. + pub const fn anonymous() -> Self { + Self(RoleEnum::Anonymous) } - pub fn is_allowed( - &self, - actor: U, - action: A, - resource: R, - ) -> Result - where - U: ToPolar, - A: ToPolar, - R: ToPolar, - { - self.oso.is_allowed(actor, action, resource).map_err(|err| { - Error::custom(format!( - "Internal error while checking access against policy: {}", - err - )) - }) + /// Returns whether the role is a user role. + pub fn is_user(&self) -> bool { + matches!(self.0, RoleEnum::User(_)) } - fn load_internal_policy( - oso: &mut Oso, - bytes: &[u8], - fname: &str, - ) -> KrillResult<()> { - trace!("Loading Polar policy '{}'", fname); - oso.load_str(std::str::from_utf8(bytes).map_err(|err| { - Error::custom(format!( - "Internal Polar policy '{}' is not valid UTF-8: {}", - fname, err - )) - })?) - .map_err(|err| { - Error::custom(format!( - "Internal Polar policy '{}' is not valid Polar syntax: {}", - fname, err - )) - }) + /// Returns whether the role is the system role. + pub fn is_system(&self) -> bool { + matches!(self.0, RoleEnum::System) } - fn exec_query(oso: &mut Oso, query: &str) -> KrillResult<()> { - oso.query(query).map_err(|err| { - Error::custom(format!( - "The Polar self check query '{}' failed: {}", - query, err - )) - })?; - Ok(()) + /// Returns whether the role is the anonymous role. + pub fn is_anonymous(&self) -> bool { + matches!(self.0, RoleEnum::Anonymous) } +} - fn load_user_policy( - config: Arc, - oso: &mut Oso, - ) -> KrillResult<()> { - for policy in config.auth_policies.iter() { - info!( - "Loading user-defined authorization policy file {:?}", - policy - ); - let fname = policy.file_name().unwrap().to_str().unwrap(); - let mut buffer = Vec::new(); - std::fs::File::open(policy.as_path()) - .map_err(|e| { - KrillIoError::new( - format!( - "Could not open policy file '{}'", - policy.to_string_lossy() - ), - e, - ) - })? - .read_to_end(&mut buffer) - .map_err(|e| { - KrillIoError::new( - format!( - "Could not read policy file '{}'", - policy.to_string_lossy() - ), - e, - ) - })?; - AuthPolicy::load_internal_policy(oso, &buffer, fname)?; - } - Ok(()) +//------------ Permission ---------------------------------------------------- + +/// The set of available permissions. +/// +/// Each API request requires for the actor to have exactly one of these +/// permissions. +#[derive(Clone, Copy, Debug)] +#[allow(non_camel_case_types)] // XXX Fix this +#[repr(u32)] +pub enum Permission { + LOGIN = 0, + PUB_ADMIN, + PUB_LIST, + PUB_READ, + PUB_CREATE, + PUB_DELETE, + CA_LIST, + CA_READ, + CA_CREATE, + CA_UPDATE, + CA_ADMIN, + CA_DELETE, + ROUTES_READ, + ROUTES_UPDATE, + ROUTES_ANALYSIS, + ASPAS_READ, + ASPAS_UPDATE, + ASPAS_ANALYSIS, + BGPSEC_READ, + BGPSEC_UPDATE, + RTA_LIST, + RTA_READ, + RTA_UPDATE +} + +impl fmt::Display for Permission { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Permission::*; + + f.write_str( + match *self { + LOGIN => "LOGIN", + PUB_ADMIN => "PUB_ADMIN", + PUB_LIST => "PUB_LIST", + PUB_READ => "PUB_READ", + PUB_CREATE => "PUB_CREATE", + PUB_DELETE => "PUB_DELETE", + CA_LIST => "CA_LIST", + CA_READ => "CA_READ", + CA_CREATE => "CA_CREATE", + CA_UPDATE => "CA_UPDATE", + CA_ADMIN => "CA_ADMIN", + CA_DELETE => "CA_DELETE", + ROUTES_READ => "ROUTES_READ", + ROUTES_UPDATE => "ROUTES_UPDATE", + ROUTES_ANALYSIS => "ROUTES_ANALYSIS", + ASPAS_READ => "ASPAS_READ", + ASPAS_UPDATE => "ASPAS_UPDATE", + ASPAS_ANALYSIS => "APSAS_ANALYSIS", + BGPSEC_READ => "BGPSEC_READ", + BGPSEC_UPDATE => "BGPSEC_UPDATE", + RTA_LIST => "RTA_LIST", + RTA_READ => "RTA_READ", + RTA_UPDATE => "RTA_UPDATE", + } + ) } } -// Allow our "no resource" type to match the "nil" in Oso policy rules by -// making it convertible to the Rust type Oso uses when registering the nil -// constant. We can't use Option::::None directly as it doesn't -// implement the Display trait which we depend on in non-trace level logging -// in `fn Actor::is_allowed()`. -// -// Note: for now it is not possible to use 'nil' directly due to https://github.com/osohq/oso/issues/788. Instead you -// have to do something like this: -// -// allow(actor: Actor, action: Permission, _resource: Option) if -// _resource = nil and -// ... -// -// WHen the bug is fixed you should then be able to do this: -// -// allow(actor: Actor, action: Permission, nil) if -// ... -impl ToPolar for NoResourceType { - #[allow(clippy::wrong_self_convention)] - fn to_polar(self) -> oso::PolarValue { - Option::::None.to_polar() + +//------------ PermissionSet ------------------------------------------------- + +/// A set of permissions. +#[derive(Clone, Copy, Debug, Default)] +struct PermissionSet(u32); + +impl PermissionSet { + pub fn add(&mut self, permission: Permission) { + self.0 |= 1u32.checked_shl( + permission as u32 + ).expect("permission size overflow"); + } + + pub fn remove(&mut self, permission: Permission) { + self.0 &= !( + 1u32.checked_shl( + permission as u32 + ).expect("permission size overflow") + ); + } + + pub fn has(&self, permission: Permission) -> bool { + self.0 & ( + 1u32.checked_shl( + permission as u32 + ).expect("permission size overflow") + ) != 0 } } -impl PolarClass for Actor { - fn get_polar_class() -> oso::Class { - Self::get_polar_class_builder() - .set_constructor(Actor::test_from_details) - .set_equality_check(|left: &Actor, right: &Actor| { - left.name() == right.name() - }) - .add_attribute_getter("name", |instance| { - instance.name().to_string() - }) - .add_class_method("builtin", |name: String| -> Actor { - match name.as_str() { - "anon" => Actor::actor_from_def(ACTOR_DEF_ANON), - "krill" => Actor::actor_from_def(ACTOR_DEF_KRILL), - "admin-token" => { - Actor::actor_from_def(ACTOR_DEF_ADMIN_TOKEN) - } - "testbed" => Actor::actor_from_def(ACTOR_DEF_TESTBED), - _ => panic!("Unknown built-in actor name '{}'", name), - } - }) - // method to do a "contains" test, either get rid of this if the - // Oso Polar "in" operator will suffice or move this - // to a separate Polar Class called Util and name the - // method "contains". - .add_class_method( - "is_in", - |name: String, names: Vec| -> bool { - names.contains(&name) - }, - ) - .add_method("attr", Actor::attribute) - .add_method("attrs", Actor::attributes) - .build() + +//------------ AuthPolicy ---------------------------------------------------- + +/// The policy allows checking for a permission on a resoure. +#[derive(Clone, Default)] +pub struct AuthPolicy { + /// Permissions for requests without specific resources. + none: PermissionSet, + + /// Blanket permission for all resources. + /// + /// This is checked for any resource that isn’t included in + /// the `resources` field. + any: PermissionSet, + + /// Permissions for specific resources. + resources: HashMap, +} + +impl AuthPolicy { + pub fn new(_config: &Config) -> KrillResult { + unimplemented!() } - fn get_polar_class_builder() -> oso::ClassBuilder { - oso::Class::builder() + pub fn is_allowed( + &self, + permission: Permission, + resource: Option<&Handle> + ) -> bool { + match resource { + Some(resource) => { + match self.resources.get(resource) { + Some(permissions) => permissions.has(permission), + None => self.any.has(permission), + } + } + None => { + self.none.has(permission) + } + } } } -impl PolarClass for Permission { - fn get_polar_class() -> oso::Class { - Self::get_polar_class_builder() - .set_constructor(|perm_name: String| -> Permission { - Permission::from_str(&perm_name).unwrap() - }) - .set_equality_check(|left: &Permission, right: &Permission| { - *left == *right - }) - .build() + +//------------ AuthPolicyMap ------------------------------------------------- + +/// A map providing the policy for each known actor. +#[derive(Clone, Default)] +pub struct AuthPolicyMap { + map: HashMap, Arc>, + + default: Arc, +} + +impl AuthPolicyMap { + pub fn new(_config: &Config) -> KrillResult { + unimplemented!() + } + + pub fn get_policy(&self, _actor: &Actor) -> Arc { + unimplemented!() } - fn get_polar_class_builder() -> oso::ClassBuilder { - oso::Class::builder() + pub fn is_allowed( + &self, + _actor: &Actor, + _permission: Permission, + _resource: Option<&Handle>, + ) -> bool { + unimplemented!() + } + + pub fn is_user_allowed( + &self, + _user_id: &str, + _permission: Permission, + _resource: Option<&Handle>, + ) -> bool { + unimplemented!() } } + diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index 02af62091..1d25f026a 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -3,11 +3,10 @@ use std::sync::Arc; use crate::daemon::http::{HttpResponse, HyperRequest}; use crate::{ commons::{ - actor::ActorDef, api::Token, error::Error, util::httpclient, + api::Token, error::Error, util::httpclient, KrillResult, }, - constants::ACTOR_DEF_ADMIN_TOKEN, - daemon::{auth::LoggedInUser, config::Config}, + daemon::{auth::{AuthInfo, LoggedInUser}, config::Config}, }; // This is NOT an actual relative path to redirect to. Instead it is the path @@ -19,12 +18,15 @@ const LAGOSTA_LOGIN_ROUTE_PATH: &str = "/login"; pub struct AdminTokenAuthProvider { required_token: Token, + user_id: Arc, } impl AdminTokenAuthProvider { pub fn new(config: Arc) -> Self { AdminTokenAuthProvider { required_token: config.admin_token.clone(), + // XXX Get from config. + user_id: "admin".into(), } } } @@ -33,14 +35,14 @@ impl AdminTokenAuthProvider { pub fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> KrillResult> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } let res = match httpclient::get_bearer_token(request) { Some(token) if token == self.required_token => { - Ok(Some(ACTOR_DEF_ADMIN_TOKEN)) + Ok(Some(AuthInfo::user(self.user_id.clone()))) } Some(_) => Err(Error::ApiInvalidCredentials( "Invalid bearer token".to_string(), @@ -62,10 +64,9 @@ impl AdminTokenAuthProvider { pub fn login(&self, request: &HyperRequest) -> KrillResult { match self.authenticate(request)? { - Some(actor_def) => Ok(LoggedInUser { + Some(_actor) => Ok(LoggedInUser { token: self.required_token.clone(), - id: actor_def.name.as_str().to_string(), - attributes: actor_def.attributes.as_map(), + id: self.user_id.as_ref().into(), }), None => Err(Error::ApiInvalidCredentials( "Missing bearer token".to_string(), @@ -77,8 +78,8 @@ impl AdminTokenAuthProvider { &self, request: &HyperRequest, ) -> KrillResult { - if let Ok(Some(actor)) = self.authenticate(request) { - info!("User logged out: {}", actor.name.as_str()); + if let Ok(Some(info)) = self.authenticate(request) { + info!("User logged out: {}", info.actor.name()); } // Logout is complete, direct Lagosta to show the user the Lagosta diff --git a/src/daemon/auth/providers/config_file/config.rs b/src/daemon/auth/providers/config_file/config.rs index a1bff175f..dcfea90e0 100644 --- a/src/daemon/auth/providers/config_file/config.rs +++ b/src/daemon/auth/providers/config_file/config.rs @@ -5,7 +5,7 @@ pub type ConfigAuthUsers = HashMap; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ConfigUserDetails { #[serde(default)] - pub attributes: HashMap, + pub role: String, // optional so that OpenIDConnectAuthProvider can also use config file // user defined attributes without requiring a dummy password hash diff --git a/src/daemon/auth/providers/config_file/provider.rs b/src/daemon/auth/providers/config_file/provider.rs index f657ebc12..f9ea4365e 100644 --- a/src/daemon/auth/providers/config_file/provider.rs +++ b/src/daemon/auth/providers/config_file/provider.rs @@ -7,7 +7,7 @@ use unicode_normalization::UnicodeNormalization; use crate::daemon::http::{HttpResponse, HyperRequest}; use crate::{ commons::{ - actor::ActorDef, api::Token, error::Error, util::httpclient, + api::Token, error::Error, util::httpclient, KrillResult, }, constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}, @@ -17,7 +17,7 @@ use crate::{ session::*, }, auth::providers::config_file::config::ConfigUserDetails, - auth::{Auth, LoggedInUser}, + auth::{Auth, AuthInfo, LoggedInUser}, config::Config, }, }; @@ -27,7 +27,6 @@ const UI_LOGIN_ROUTE_PATH: &str = "/login?withId=true"; struct UserDetails { password_hash: Token, salt: String, - attributes: HashMap, } fn get_checked_config_user( @@ -59,7 +58,6 @@ fn get_checked_config_user( Ok(UserDetails { password_hash: Token::from(password_hash), salt, - attributes: user.attributes.clone(), }) } @@ -124,7 +122,7 @@ impl ConfigFileAuthProvider { pub fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> KrillResult> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } @@ -139,13 +137,9 @@ impl ConfigFileAuthProvider { true, )?; - trace!( - "id={}, attributes={:?}", - &session.id, - &session.attributes - ); + trace!("user_id={}", session.user_id); - Ok(Some(ActorDef::user(session.id, session.attributes, None))) + Ok(Some(AuthInfo::user(session.user_id))) } _ => Ok(None), }; @@ -225,10 +219,9 @@ impl ConfigFileAuthProvider { // and don't result in an obvious timing difference between // the two scenarios which could potentially // be used to discover user names. - if let Some(user) = self.users.get(&username) { + if let Some(_user) = self.users.get(username.as_str()) { let api_token = self.session_cache.encode( - &username, - &user.attributes, + username.clone().into(), HashMap::new(), &self.session_key, None, @@ -236,8 +229,7 @@ impl ConfigFileAuthProvider { Ok(LoggedInUser { token: api_token, - id: username.to_string(), - attributes: user.attributes.clone(), + id: username.clone(), }) } else { trace!("Incorrect password for user {}", username); @@ -267,8 +259,8 @@ impl ConfigFileAuthProvider { Some(token) => { self.session_cache.remove(&token); - if let Ok(Some(actor)) = self.authenticate(request) { - info!("User logged out: {}", actor.name.as_str()); + if let Ok(Some(info)) = self.authenticate(request) { + info!("User logged out: {}", info.actor.name()); } } _ => { diff --git a/src/daemon/auth/providers/openid_connect/config.rs b/src/daemon/auth/providers/openid_connect/config.rs index fc20adbbd..3295f78c3 100644 --- a/src/daemon/auth/providers/openid_connect/config.rs +++ b/src/daemon/auth/providers/openid_connect/config.rs @@ -1,9 +1,6 @@ use std::collections::HashMap; -use serde::{de, Deserialize, Deserializer}; - -pub type ConfigAuthOpenIDConnectClaims = - HashMap; +use serde::Deserialize; pub struct ConfigDefaults {} @@ -15,7 +12,7 @@ pub struct ConfigAuthOpenIDConnect { pub client_secret: String, - pub claims: Option, + pub id_claim: String, #[serde(default)] pub extra_login_scopes: Vec, @@ -38,62 +35,3 @@ fn default_prompt_for_login() -> bool { true } -#[derive(Clone, Debug, Deserialize)] -pub struct ConfigAuthOpenIDConnectClaim { - pub source: Option, - pub jmespath: Option, - pub dest: Option, -} - -#[derive(Clone, Debug)] -pub enum ConfigAuthOpenIDConnectClaimSource { - ConfigFile, - IdTokenStandardClaim, - IdTokenAdditionalClaim, - UserInfoStandardClaim, - UserInfoAdditionalClaim, -} - -impl std::fmt::Display for ConfigAuthOpenIDConnectClaimSource { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - ConfigAuthOpenIDConnectClaimSource::ConfigFile => { - write!(f, "config-file") - } - ConfigAuthOpenIDConnectClaimSource::IdTokenStandardClaim => { - write!(f, "id-token-standard-claim") - } - ConfigAuthOpenIDConnectClaimSource::IdTokenAdditionalClaim => { - write!(f, "id-token-additional-claim") - } - ConfigAuthOpenIDConnectClaimSource::UserInfoStandardClaim => { - write!(f, "user-info-standard-claim") - } - ConfigAuthOpenIDConnectClaimSource::UserInfoAdditionalClaim => { - write!(f, "user-info-additional-claim") - } - } - } -} - -impl<'de> Deserialize<'de> for ConfigAuthOpenIDConnectClaimSource { - fn deserialize( - d: D, - ) -> Result - where - D: Deserializer<'de>, - { - let string = String::deserialize(d)?; - match string.as_str() { - "config-file" => Ok(ConfigAuthOpenIDConnectClaimSource::ConfigFile), - "id-token-standard-claim" => Ok(ConfigAuthOpenIDConnectClaimSource::IdTokenStandardClaim), - "id-token-additional-claim" => Ok(ConfigAuthOpenIDConnectClaimSource::IdTokenAdditionalClaim), - "user-info-standard-claim" => Ok(ConfigAuthOpenIDConnectClaimSource::UserInfoStandardClaim), - "user-info-additional-claim" => Ok(ConfigAuthOpenIDConnectClaimSource::UserInfoAdditionalClaim), - _ => Err(de::Error::custom(format!( - "expected \"config-file\", \"id-token-additional-claim\", \"id-token-standard-claim\", \"user-info-standard-claim\", or \"user-info-additional-claim\", found : \"{}\"", - string - ))), - } - } -} diff --git a/src/daemon/auth/providers/openid_connect/jmespathext.rs b/src/daemon/auth/providers/openid_connect/jmespathext.rs deleted file mode 100644 index 93169d5b3..000000000 --- a/src/daemon/auth/providers/openid_connect/jmespathext.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::sync::Arc; - -use jmespatch as jmespath; - -use jmespath::{ - functions::{ArgumentType, CustomFunction, Signature}, - Context, ErrorReason, JmespathError, Rcvar, Runtime, -}; - -use regex::Regex; - -/// Create a customized instance of the JMESPath runtime with support for the -/// standard functions and two additional custom functions: recap and resub. -pub fn init_runtime() -> Runtime { - let mut runtime = Runtime::new(); - - runtime.register_builtin_functions(); - runtime.register_function("recap", make_recap_fn()); - runtime.register_function("resub", make_resub_fn()); - - runtime -} - -/// Custom JMESPath recap(haystack, regex) function that returns the value of -/// the first capture group of the first match in the haystack by the -/// specified regex. -/// -/// Returns an empty string if no match is found. -fn make_recap_fn() -> Box { - let fn_signature = - Signature::new(vec![ArgumentType::Any, ArgumentType::String], None); - - let fn_impl = Box::new(|args: &[Rcvar], _: &mut Context| { - trace!("jmespath recap() arguments: {:?}", args); - - let mut res = String::new(); - - if let jmespath::Variable::String(str) = &*args[0] { - if let jmespath::Variable::String(re_str) = &*args[1] { - match Regex::new(re_str) { - Ok(re) => { - let mut iter = re.captures_iter(str); - if let Some(captures) = iter.next() { - // captures[0] is the entire match - // captures[1] is the value of the first capture - // group match - res = captures[1].to_string(); - } - } - Err(err) => { - return Err(JmespathError::new( - re_str, - 0, - ErrorReason::Parse(format!( - "Invalid regular expression: {}", - err - )), - )); - } - } - } - } - - trace!("jmespath recap() result: {}", &res); - Ok(Arc::new(jmespath::Variable::String(res))) - }); - - Box::new(CustomFunction::new(fn_signature, fn_impl)) -} - -/// Custom JMESPath resub(haystack, needle regex, replacement value) function -/// that returns the result of replacing the first text in the haystack that -/// matches the needle regex with the given replacement value. -/// -/// Returns the given string unchanged if no match is found to replace. -fn make_resub_fn() -> Box { - let fn_signature = Signature::new( - vec![ - ArgumentType::Any, - ArgumentType::String, - ArgumentType::String, - ], - None, - ); - - let fn_impl = Box::new(|args: &[Rcvar], _: &mut Context| { - trace!("jmespath fn resub() arguments: {:?}", args); - - if let jmespath::Variable::String(str) = &*args[0] { - let mut res = String::new(); - if let jmespath::Variable::String(re_str) = &*args[1] { - if let jmespath::Variable::String(newval) = &*args[2] { - match Regex::new(re_str) { - Ok(re) => { - res = re - .replace(str.as_str(), newval.as_str()) - .to_string(); - } - Err(err) => { - return Err(JmespathError::new( - re_str, - 0, - ErrorReason::Parse(format!( - "Invalid regular expression: {}", - err - )), - )); - } - } - } - } - trace!("jmespath resub() result: {}", &res); - return Ok(Arc::new(jmespath::Variable::String(res))); - } - - Ok(Arc::new(jmespath::Variable::Null)) - }); - - Box::new(CustomFunction::new(fn_signature, fn_impl)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn resub_should_handle_null_input() { - let runtime = init_runtime(); - - // Parse some JSON data into a JMESPath variable - let json_str = r#" - { - "groups":["a", "b"] - } - "#; - let jmespath_var = jmespath::Variable::from_json(json_str).unwrap(); - - // Create an expression that should yield null when evaluated - let null_expr = "groups[?@ == 'idontexist'] | [0]"; - let should_yield_null = runtime.compile(null_expr).unwrap(); - let result = should_yield_null.search(&jmespath_var).unwrap(); - assert_eq!(jmespath::Variable::Null, *result); - - // Now use that expression as input to the resub() function and verify - // that it returns null too - let should_also_yield_null = runtime - .compile(&format!("resub({}, '^.+$', 'admin')", null_expr)) - .unwrap(); - let result = should_also_yield_null.search(&jmespath_var).unwrap(); - assert_eq!(jmespath::Variable::Null, *result); - } - - #[test] - fn resub_should_return_error_when_given_an_invalid_regex() { - let runtime = init_runtime(); - - // an opening square bracket without matching closing square bracket - // is an invalid regular expression - let should_also_yield_null = runtime - .compile("resub('dummy input', '[', 'admin')") - .unwrap(); - - // Parse some JSON data into a JMESPath variable - let json_str = r#" - { - "groups":["a", "b"] - } - "#; - let jmespath_var = jmespath::Variable::from_json(json_str).unwrap(); - - assert!(should_also_yield_null.search(&jmespath_var).is_err()); - } -} diff --git a/src/daemon/auth/providers/openid_connect/mod.rs b/src/daemon/auth/providers/openid_connect/mod.rs index 8212cfd71..5551434b5 100644 --- a/src/daemon/auth/providers/openid_connect/mod.rs +++ b/src/daemon/auth/providers/openid_connect/mod.rs @@ -3,7 +3,6 @@ pub mod util; pub mod config; pub mod httpclient; -pub mod jmespathext; pub mod provider; pub use config::ConfigAuthOpenIDConnect; diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index f335cfd20..8b7a2a6a3 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -28,10 +28,7 @@ //! [openid-connect-rpinitiated-1_0]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html use std::{ - collections::{ - hash_map::Entry::{Occupied, Vacant}, - HashMap, - }, + collections::HashMap, ops::Deref, sync::Arc, time::Instant, @@ -43,8 +40,6 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD as URL_BASE64_ENGINE; use base64::engine::Engine as _; use basic_cookies::Cookie; use hyper::header::{HeaderValue, SET_COOKIE}; -use jmespatch as jmespath; -use jmespath::ToJmespath; use openidconnect::{ core::{ @@ -63,7 +58,6 @@ use urlparse::{urlparse, GetQuery}; use crate::daemon::http::{HttpResponse, HyperRequest}; use crate::{ commons::{ - actor::ActorDef, api::Token, error::Error, util::{httpclient, sha256}, @@ -75,28 +69,25 @@ use crate::{ crypt::{self, CryptState}, session::*, }, - providers::config_file::config::ConfigUserDetails, providers::openid_connect::{ - config::{ - ConfigAuthOpenIDConnect, ConfigAuthOpenIDConnectClaim, - ConfigAuthOpenIDConnectClaimSource as ClaimSource, - ConfigAuthOpenIDConnectClaims, - }, + config::ConfigAuthOpenIDConnect, httpclient::logging_http_client, - jmespathext, util::{ FlexibleClient, FlexibleIdTokenClaims, FlexibleTokenResponse, FlexibleUserInfoClaims, LogOrFail, WantedMeta, }, }, - Auth, LoggedInUser, + Auth, AuthInfo, LoggedInUser, }, config::Config, http::auth::{url_encode, AUTH_CALLBACK_ENDPOINT}, }, }; + +//------------ Constants ----------------------------------------------------- + // On modern browsers (Chrome >= 51, Edge >= 16, Firefox >= 60 & Safari >= 12) // the "__Host" prefix is a defence-in-depth measure that causes the browser // to further restrict access to the cookie, permitting access only if the @@ -105,6 +96,9 @@ use crate::{ const NONCE_COOKIE_NAME: &str = "__Host-krill_login_nonce"; const CSRF_COOKIE_NAME: &str = "__Host-krill_login_csrf_hash"; + +//------------ TokenKind ----------------------------------------------------- + #[allow(clippy::enum_variant_names)] enum TokenKind { AccessToken, @@ -131,6 +125,10 @@ impl From for &'static str { } } } + + +//------------ LogoutMode ---------------------------------------------------- + enum LogoutMode { OAuth2TokenRevocation { revocation_url: String, @@ -148,6 +146,9 @@ enum LogoutMode { }, } + +//------------ ProivderConnectionProperties ---------------------------------- + pub struct ProviderConnectionProperties { client: FlexibleClient, email_scope_supported: bool, @@ -156,6 +157,9 @@ pub struct ProviderConnectionProperties { logout_mode: LogoutMode, } + +//------------ OpenIdConnectAuthProvider ------------------------------------- + pub struct OpenIDConnectAuthProvider { config: Arc, session_cache: Arc, @@ -565,7 +569,7 @@ impl OpenIDConnectAuthProvider { trace!( "OpenID Connect: Revoking token for user: \"{}\"", - &session.id + &session.user_id ); trace!("OpenID Connect: Submitting RFC-7009 section 2 Token Revocation request"); let lock_guard = self.get_connection().await.map_err(|err| { @@ -640,7 +644,7 @@ impl OpenIDConnectAuthProvider { debug!( "OpenID Connect: Refreshing token for user: \"{}\"", - &session.id + &session.user_id ); trace!("OpenID Connect: Submitting RFC-6749 section 6 Access Token Refresh request"); @@ -660,8 +664,7 @@ impl OpenIDConnectAuthProvider { match token_response { Ok(token_response) => { let new_token_res = self.session_cache.encode( - &session.id, - &session.attributes, + session.user_id.clone(), secrets_from_token_response(&token_response), &self.session_key, token_response.expires_in(), @@ -732,10 +735,12 @@ impl OpenIDConnectAuthProvider { fn extract_claim( &self, - claim_conf: &ConfigAuthOpenIDConnectClaim, - id_token_claims: &FlexibleIdTokenClaims, - user_info_claims: Option<&FlexibleUserInfoClaims>, + //_claim_conf: &ConfigAuthOpenIDConnectClaim, + _id_token_claims: &FlexibleIdTokenClaims, + _user_info_claims: Option<&FlexibleUserInfoClaims>, ) -> KrillResult> { + unimplemented!() + /* let searchable_claims = match &claim_conf.source { Some(ClaimSource::ConfigFile) => return Ok(None), Some(ClaimSource::IdTokenStandardClaim) => { @@ -878,6 +883,7 @@ impl OpenIDConnectAuthProvider { ); Ok(None) + */ } fn init_session_key(config: &Config) -> KrillResult { @@ -1242,6 +1248,7 @@ impl OpenIDConnectAuthProvider { Ok(user_info_claims) } + /* fn resolve_claims( &self, claims_conf: HashMap, @@ -1249,7 +1256,7 @@ impl OpenIDConnectAuthProvider { id_token_claims: &FlexibleIdTokenClaims, user_info_claims: Option, id: &str, - ) -> KrillResult> { + ) -> KrillResult { let mut attributes: HashMap = HashMap::new(); for (attr_name, claim_conf) in claims_conf { if attr_name == "id" { @@ -1257,15 +1264,15 @@ impl OpenIDConnectAuthProvider { } let attr_value = match &claim_conf.source { Some(ClaimSource::ConfigFile) if user.is_some() => { - // Lookup the claim value in the auth_users config file - // section - user.unwrap() - .attributes - .get(&attr_name.to_string()) - .cloned() + if attr_name == "role" { + Some(user.unwrap().role.clone()) + } + else { + None + } } _ => self.extract_claim( - &claim_conf, + //&claim_conf, id_token_claims, user_info_claims.as_ref(), )?, @@ -1315,8 +1322,18 @@ impl OpenIDConnectAuthProvider { info!("No '{}' claim found for user: {}", &attr_name, &id); } } - Ok(attributes) + + match attributes.get("role") { + Some(role) => Ok(role.clone()), + None => { + Err(OpenIDConnectAuthProvider::internal_error( + "no role for user".into(), + Some(format!("user ID: {}", id)) + )) + } + } } + */ } impl OpenIDConnectAuthProvider { @@ -1334,7 +1351,7 @@ impl OpenIDConnectAuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> KrillResult> { trace!("Attempting to authenticate the request.."); self.initialize_connection_if_needed().await.map_err(|err| { @@ -1359,11 +1376,7 @@ impl OpenIDConnectAuthProvider { // return match status { SessionStatus::Active => { - return Ok(Some(ActorDef::user( - session.id, - session.attributes, - None, - ))); + return Ok(Some(AuthInfo::user(session.user_id))) } SessionStatus::NeedsRefresh => { // If we have a refresh token try and extend the @@ -1373,11 +1386,7 @@ impl OpenIDConnectAuthProvider { .secrets .contains_key(TokenKind::RefreshToken.into()) { - return Ok(Some(ActorDef::user( - session.id, - session.attributes, - None, - ))); + return Ok(Some(AuthInfo::user(session.user_id))) } } SessionStatus::Expired => { @@ -1402,7 +1411,7 @@ impl OpenIDConnectAuthProvider { Ok(auth) => { trace!( "OpenID Connect: Successfully refreshed token for user \"{}\"", - &session.id + session.user_id ); auth } @@ -1410,7 +1419,7 @@ impl OpenIDConnectAuthProvider { trace!("OpenID Connect: RFC 6749 5.2 Error response returned..."); debug!( "OpenID Connect: Refreshing the token for user '{}' failed: {}", - &session.id, &err + session.user_id, err ); match err { // This is the Error returned by the OpenID @@ -1483,11 +1492,7 @@ impl OpenIDConnectAuthProvider { } }; - Ok(Some(ActorDef::user( - session.id, - session.attributes, - Some(new_auth), - ))) + Ok(Some(AuthInfo::with_new_auth(session.user_id, new_auth))) } _ => Ok(None), }; @@ -1874,20 +1879,9 @@ impl OpenIDConnectAuthProvider { // configuration without the "id" key :-) // ========================================================================================== - let claims_conf = - with_default_claims(&self.oidc_conf()?.claims); - - let id_claim_conf = - claims_conf.get("id").ok_or_else(|| { - OpenIDConnectAuthProvider::internal_error( - "Missing 'id' claim configuration", - None, - ) - })?; - let id = self .extract_claim( - id_claim_conf, + //id_claim_conf, id_token_claims, user_info_claims.as_ref(), )? @@ -1898,26 +1892,6 @@ impl OpenIDConnectAuthProvider { ) })?; - // Lookup the a user in the config file authentication - // provider configuration by the id value that - // we just obtained, if present. Any claim - // configurations that refer to attributes of - // users configured in the config file will be looked up on - // this user. - let user = self - .config - .auth_users - .as_ref() - .and_then(|users| users.get(&id)); - - let attributes = self.resolve_claims( - claims_conf, - user, - id_token_claims, - user_info_claims, - &id, - )?; - // ========================================================================================== // Step 5: Respond to the user: access granted, or access // denied TODO: Choose which data to store at @@ -1944,19 +1918,14 @@ impl OpenIDConnectAuthProvider { // so attempting to refresh an access token // after that much time would also fail. // ========================================================================================== - let api_token = self.session_cache.encode( - &id, - &attributes, + let token = self.session_cache.encode( + id.clone().into(), secrets_from_token_response(&token_response), &self.session_key, token_response.expires_in(), )?; - Ok(LoggedInUser { - token: api_token, - id, - attributes, - }) + Ok(LoggedInUser { token, id, }) } _ => Err(Error::ApiInvalidCredentials( @@ -2014,7 +1983,7 @@ impl OpenIDConnectAuthProvider { )?; // announce that the user requested to be logged out - info!("User logged out: {}", session.id); + info!("User logged out: {}", session.user_id); // perform the logout: @@ -2046,7 +2015,7 @@ impl OpenIDConnectAuthProvider { OpenIDConnectAuthProvider::internal_error( format!( "Error while revoking token for user '{}'", - session.id + session.user_id ), Some(err.to_string()), ); @@ -2076,7 +2045,7 @@ impl OpenIDConnectAuthProvider { OpenIDConnectAuthProvider::internal_error( format!( "Error while building OpenID Connect RP-Initiated Logout URL for user '{}'", - session.id + session.user_id ), Some(stringify_cause_chain(err)), ); @@ -2093,6 +2062,9 @@ impl OpenIDConnectAuthProvider { } } + +//------------ Helper Functions ---------------------------------------------- + fn secrets_from_token_response( token_response: &FlexibleTokenResponse, ) -> HashMap { @@ -2117,6 +2089,7 @@ fn secrets_from_token_response( secrets } +/* fn with_default_claims( claims: &Option, ) -> ConfigAuthOpenIDConnectClaims { @@ -2143,6 +2116,7 @@ fn with_default_claims( claims } +*/ // Based on: https://github.com/ramosbugs/openidconnect-rs/blob/main/examples/google.rs#L38 pub fn stringify_cause_chain(fail: F) -> String { @@ -2158,3 +2132,4 @@ pub fn stringify_cause_chain(fail: F) -> String { } cause_chain } + diff --git a/src/daemon/ca/manager.rs b/src/daemon/ca/manager.rs index e1cfc5b6b..972877e38 100644 --- a/src/daemon/ca/manager.rs +++ b/src/daemon/ca/manager.rs @@ -48,8 +48,8 @@ use crate::{ CASERVER_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, daemon::{ - auth::common::permissions::Permission, auth::Handle, + auth::policy::{AuthPolicy, Permission}, ca::{ CaObjectsStore, CaStatus, CertAuth, CertAuthCommand, CertAuthCommandDetails, DeprecatedRepository, @@ -611,19 +611,23 @@ impl CaManager { Ok(()) } - /// Get the CAs that the given actor is permitted to see. - pub fn ca_list(&self, actor: &Actor) -> KrillResult { + /// Returns all known CA handles. + pub fn ca_handles(&self) -> KrillResult> { + Ok(self.ca_store.list()?) + } + + /// Gets the CAs that the given policy allows read access to. + pub fn ca_list( + &self, auth: &AuthPolicy + ) -> KrillResult { Ok(CertAuthList::new( self.ca_store .list()? .into_iter() .filter(|handle| { - matches!( - actor.is_allowed( - Permission::CA_READ, - Handle::from(handle) - ), - Ok(true) + auth.is_allowed( + Permission::CA_READ, + Some(&Handle::from(handle)) ) }) .map(CertAuthSummary::new) @@ -2460,9 +2464,9 @@ impl CaManager { /// Schedule synchronizing all CAs with their repositories. pub fn cas_schedule_repo_sync_all( &self, - actor: &Actor, + auth: &AuthPolicy, ) -> KrillResult<()> { - for ca in self.ca_list(actor)?.cas() { + for ca in self.ca_list(auth)?.cas() { self.cas_schedule_repo_sync(ca.handle().clone())?; } Ok(()) @@ -3056,7 +3060,9 @@ impl CaManager { /// Note: this does not re-issue issued CA certificates, because child /// CAs are expected to note extended validity eligibility and request /// updated certificates themselves. - pub async fn renew_objects_all(&self, actor: &Actor) -> KrillResult<()> { + pub async fn renew_objects_all( + &self, actor: &Actor + ) -> KrillResult<()> { for ca in self.ca_store.list()? { let cmd = CertAuthCommand::new( &ca, diff --git a/src/daemon/http/auth.rs b/src/daemon/http/auth.rs index b3f123b3f..1da0b42ba 100644 --- a/src/daemon/http/auth.rs +++ b/src/daemon/http/auth.rs @@ -24,28 +24,10 @@ pub fn url_encode>(s: S) -> Result { #[cfg(feature = "multi-user")] fn build_auth_redirect_location(user: LoggedInUser) -> Result { - use std::collections::HashMap; - - fn b64_encode_attributes_with_mapped_error( - a: &HashMap, - ) -> Result { - use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; - use base64::engine::Engine as _; - - Ok(BASE64_ENGINE.encode( - serde_json::to_string(a) - .map_err(|err| Error::custom(err.to_string()))?, - )) - } - - let attributes = - b64_encode_attributes_with_mapped_error(&user.attributes)?; - Ok(format!( - "/ui/login?token={}&id={}&attributes={}", + "/ui/login?token={}&id={}", &url_encode(user.token)?, - &url_encode(user.id)?, - &url_encode(attributes)? + &url_encode(user.id.as_str())?, )) } diff --git a/src/daemon/http/mod.rs b/src/daemon/http/mod.rs index 94905a5a7..15d02de96 100644 --- a/src/daemon/http/mod.rs +++ b/src/daemon/http/mod.rs @@ -1,24 +1,27 @@ -use std::{io, str::from_utf8, str::FromStr}; - +use std::io; +use std::str::FromStr; +use std::str::from_utf8; +use std::sync::Arc; use bytes::Bytes; -use serde::{de::DeserializeOwned, Serialize}; - use http_body_util::{BodyExt, Either, Empty, Full, Limited}; use hyper::body::Body; use hyper::header::USER_AGENT; use hyper::http::uri::PathAndQuery; use hyper::{HeaderMap, Method, StatusCode}; - use rpki::ca::{provisioning, publication}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use crate::daemon::auth::{AuthInfo, Handle, LoggedInUser}; +use crate::daemon::auth::policy::{AuthPolicy, Permission}; use crate::{ commons::{ - actor::{Actor, ActorDef}, + actor::Actor, error::Error, KrillResult, }, constants::HTTP_USER_AGENT_TRUNCATE, - daemon::{auth::LoggedInUser, http::server::State}, + daemon::http::server::State, }; pub mod auth; @@ -351,19 +354,22 @@ pub struct Request { request: HyperRequest, path: RequestPath, state: State, - actor: Actor, + auth: AuthInfo, + auth_policy: Arc, } impl Request { pub async fn new(request: HyperRequest, state: State) -> Self { let path = RequestPath::from_request(&request); - let actor = state.actor_from_request(&request).await; + let auth = state.authenticate_request(&request).await; + let auth_policy = state.get_auth_policy(&auth.actor); Request { request, path, state, - actor, + auth, + auth_policy, } } @@ -387,18 +393,35 @@ impl Request { } } - pub async fn upgrade_from_anonymous(&mut self, actor_def: ActorDef) { - if self.actor.is_anonymous() { - self.actor = self.state.actor_from_def(actor_def); + pub async fn upgrade_from_anonymous(&mut self, actor: Actor) { + if self.auth.actor.is_anonymous() { + self.auth.actor = actor.into(); info!( - "Permitted anonymous actor to become actor '{}' for the duration of this request", - self.actor.name() + "Permitted anonymous actor to become actor '{}' \ + for the duration of this request", + self.auth.actor.name() ); } } + pub fn is_allowed( + &self, + permission: Permission, + resource: Option<&Handle> + ) -> bool { + self.state.is_allowed(&self.auth.actor, permission, resource) + } + pub fn actor(&self) -> Actor { - self.actor.clone() + self.auth.actor.clone() + } + + pub fn auth_policy(&self) -> &AuthPolicy { + &self.auth_policy + } + + pub fn auth_info_mut(&mut self) -> &mut AuthInfo { + &mut self.auth } /// Returns the complete path. diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index a6c73c305..3f04fc8db 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -41,11 +41,11 @@ use crate::{ }, constants::{ KRILL_ENV_HTTP_LOG_INFO, KRILL_ENV_UPGRADE_ONLY, KRILL_VERSION_MAJOR, - KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, NO_RESOURCE, + KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, }, daemon::{ - auth::common::permissions::Permission, auth::{Auth, Handle}, + auth::policy::Permission, ca::CaStatus, config::Config, http::{ @@ -346,11 +346,11 @@ async fn map_requests( ) -> Result { let logger = RequestLogger::begin(&req); - let req = Request::new(req, state).await; + let mut req = Request::new(req, state).await; // Save any updated auth details, e.g. if an OpenID Connect token needed // refreshing. - let new_auth = req.actor().new_auth(); + let new_auth = req.auth_info_mut().new_auth.take(); // We used to use .or_else() here but that causes a large recursive call // tree due to these calls being to async functions, large enough with the @@ -1197,43 +1197,38 @@ fn add_new_auth_to_response( // similar to how this macro is used in each function. macro_rules! aa { (no_warn $req:ident, $perm:expr, $action:expr) => {{ - aa!($req, $perm, NO_RESOURCE, $action, true) + aa!($req, $perm, Option::<&Handle>::None, $action, true) }}; ($req:ident, $perm:expr, $action:expr) => {{ - aa!($req, $perm, NO_RESOURCE, $action, false) + aa!($req, $perm, Option::<&Handle>::None, $action, false) }}; (no_warn $req:ident, $perm:expr, $resource:expr, $action:expr) => {{ - aa!($req, $perm, $resource, $action, true) + aa!($req, $perm, Some(&$resource), $action, true) }}; ($req:ident, $perm:expr, $resource:expr, $action:expr) => {{ - aa!($req, $perm, $resource, $action, false) + aa!($req, $perm, Some(&$resource), $action, false) }}; ($req:ident, $perm:expr, $resource:expr, $action:expr, $benign:expr) => {{ - match $req.actor().is_allowed($perm, $resource) { - Ok(true) => $action, - Ok(false) => { - let msg = format!( - "User '{}' does not have permission '{}' on resource '{}'", - $req.actor().name(), - $perm, - $resource - ); - Ok(HttpResponse::forbidden(msg).with_benign($benign)) - } - Err(err) => { - // Avoid an extra round of error -> string -> error conversion - // which causes the error message to nest, e.g. - // "Invalid credentials: Invalid credentials: Session expired" - match err { - Error::ApiInvalidCredentials(_) - | Error::ApiInsufficientRights(_) - | Error::ApiAuthPermanentError(_) - | Error::ApiAuthTransientError(_) - | Error::ApiAuthSessionExpired(_) - | Error::ApiLoginError(_) => Ok(HttpResponse::response_from_error(err).with_benign($benign)), - _ => Ok(HttpResponse::forbidden(format!("{}", err)).with_benign($benign)), + if $req.is_allowed($perm, $resource) { + $action + } + else { + let msg = match $resource { + Some(res) => { + format!( + "User '{}' does not have permission '{}' \ + on resource '{}'", + $req.actor().name(), $perm, res, + ) + }, + None => { + format!( + "User '{}' does not have permission '{}'", + $req.actor().name(), $perm, + ) } - } + }; + Ok(HttpResponse::forbidden(msg).with_benign($benign)) } }}; } @@ -1773,8 +1768,9 @@ async fn api_cas_import(req: Request) -> RoutingResult { async fn api_all_ca_issues(req: Request) -> RoutingResult { match *req.method() { Method::GET => aa!(req, Permission::CA_READ, { - let actor = req.actor(); - render_json_res(req.state().all_ca_issues(&actor).await) + render_json_res( + req.state().all_ca_issues(req.auth_policy()).await + ) }), _ => render_unknown_method(), } @@ -1795,8 +1791,7 @@ async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { async fn api_cas_list(req: Request) -> RoutingResult { aa!(req, Permission::CA_LIST, { - let actor = req.actor(); - render_json_res(req.state().ca_list(&actor)) + render_json_res(req.state().ca_list(req.auth_policy())) }) } @@ -2536,8 +2531,11 @@ async fn api_republish_all(req: Request, force: bool) -> RoutingResult { async fn api_resync_all(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { - let actor = req.actor(); - render_empty_res(req.state().cas_repo_sync_all(&actor)) + render_empty_res( + req.state().cas_repo_sync_all( + req.auth_policy() + ) + ) }), _ => render_unknown_method(), } @@ -2766,7 +2764,7 @@ async fn api_ta(req: Request, path: &mut RequestPath) -> RoutingResult { Method::POST => { let ta_handle = ta::ta_handle(); let server = req.state().clone(); - let actor = req.actor.clone(); + let actor = req.actor(); match req.api_bytes().await.map(|bytes| { extract_repository_contact(&ta_handle, bytes) @@ -2792,7 +2790,7 @@ async fn api_ta(req: Request, path: &mut RequestPath) -> RoutingResult { Some("signer") => match path.next() { Some("add") => { let server = req.state().clone(); - let actor = req.actor.clone(); + let actor = req.actor(); match req.json().await { Ok(ta_signer_info) => render_empty_res( server @@ -2816,7 +2814,7 @@ async fn api_ta(req: Request, path: &mut RequestPath) -> RoutingResult { Some("response") => match *req.method() { Method::POST => { let server = req.state().clone(); - let actor = req.actor.clone(); + let actor = req.actor(); match req.json().await { Ok(response) => render_empty_res( diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index 9d6c13191..ac1068d34 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -1,9 +1,10 @@ //! An RPKI publication protocol server. -use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; - +use std::collections::HashMap; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; use bytes::Bytes; use chrono::Duration; - use futures_util::future::try_join_all; use rpki::{ @@ -15,9 +16,11 @@ use rpki::{ uri, }; +use crate::daemon::auth::{AuthInfo, Handle}; +use crate::daemon::auth::policy::{AuthPolicy, Permission}; use crate::{ commons::{ - actor::{Actor, ActorDef}, + actor::Actor, api::{ self, import::{ExportChild, ImportChild}, @@ -158,7 +161,7 @@ impl KrillServer { .into(), )?, }; - let system_actor = authorizer.actor_from_def(ACTOR_DEF_KRILL); + let system_actor = ACTOR_DEF_KRILL; // Task queue Arc is shared between ca_manager, repo_manager and the // scheduler. @@ -337,12 +340,10 @@ impl KrillServer { &self.system_actor } - pub async fn actor_from_request(&self, request: &HyperRequest) -> Actor { - self.authorizer.actor_from_request(request).await - } - - pub fn actor_from_def(&self, actor_def: ActorDef) -> Actor { - self.authorizer.actor_from_def(actor_def) + pub async fn authenticate_request( + &self, request: &HyperRequest + ) -> AuthInfo { + self.authorizer.authenticate_request(request).await } pub async fn get_login_url(&self) -> KrillResult { @@ -371,6 +372,19 @@ impl KrillServer { pub fn login_session_cache_size(&self) -> usize { self.login_session_cache.size() } + + pub fn get_auth_policy(&self, actor: &Actor) -> Arc { + self.authorizer.get_policy(actor) + } + + pub fn is_allowed( + &self, + actor: &Actor, + permission: Permission, + resource: Option<&Handle> + ) -> bool { + self.authorizer.is_allowed(actor, permission, resource) + } } /// # Configure publishers @@ -708,9 +722,9 @@ impl KrillServer { ) -> KrillResult> { let mut res = HashMap::new(); - for ca in self.ca_list(&self.system_actor)?.cas() { + for handle in self.ca_manager.ca_handles()? { // can't fail really, but to be sure - if let Ok(ca) = self.ca_manager.get_ca(ca.handle()).await { + if let Ok(ca) = self.ca_manager.get_ca(&handle).await { let roas = ca.configured_roas(); let roa_count = roas.len(); let child_count = ca.children().count(); @@ -748,10 +762,10 @@ impl KrillServer { // We need to know which CAs already exist. They should not be // imported again, but can serve as parents. let mut existing_cas = HashMap::new(); - for ca in self.ca_list(&actor)?.cas() { - let parent_handle = ca.handle().convert(); + for handle in self.ca_manager.ca_handles()? { + let parent_handle = handle.convert(); let resources = - self.ca_manager.get_ca(ca.handle()).await?.all_resources(); + self.ca_manager.get_ca(&handle).await?.all_resources(); existing_cas.insert(parent_handle, resources); } structure.validate_ca_hierarchy(existing_cas)?; @@ -975,10 +989,10 @@ impl KrillServer { pub async fn all_ca_issues( &self, - actor: &Actor, + auth: &AuthPolicy, ) -> KrillResult { let mut all_issues = AllCertAuthIssues::default(); - for ca in self.ca_list(actor)?.cas() { + for ca in self.ca_list(auth)?.cas() { let issues = self.ca_issues(ca.handle()).await?; if !issues.is_empty() { all_issues.add(ca.handle().clone(), issues); @@ -1023,8 +1037,8 @@ impl KrillServer { } /// Re-sync all CAs with their repositories - pub fn cas_repo_sync_all(&self, actor: &Actor) -> KrillEmptyResult { - self.ca_manager.cas_schedule_repo_sync_all(actor) + pub fn cas_repo_sync_all(&self, auth: &AuthPolicy) -> KrillEmptyResult { + self.ca_manager.cas_schedule_repo_sync_all(auth) } /// Re-sync a specific CA with its repository @@ -1053,8 +1067,8 @@ impl KrillServer { /// # Admin CAS impl KrillServer { - pub fn ca_list(&self, actor: &Actor) -> KrillResult { - self.ca_manager.ca_list(actor) + pub fn ca_list(&self, auth: &AuthPolicy) -> KrillResult { + self.ca_manager.ca_list(auth) } /// Returns the public CA info for a CA, or NONE if the CA cannot be diff --git a/src/daemon/properties/mod.rs b/src/daemon/properties/mod.rs index e6b07b3be..945c5937e 100644 --- a/src/daemon/properties/mod.rs +++ b/src/daemon/properties/mod.rs @@ -32,7 +32,7 @@ use crate::{ util::KrillVersion, KrillResult, }, - constants::{PROPERTIES_DFLT_NAME, PROPERTIES_NS}, + constants::{ACTOR_DEF_KRILL, PROPERTIES_DFLT_NAME, PROPERTIES_NS}, }; //------------ PropertiesInitCommand --------------------------------------- @@ -282,7 +282,7 @@ impl PropertiesManager { .map(|store| PropertiesManager { store, main_key, - system_actor: Actor::system_actor(), + system_actor: ACTOR_DEF_KRILL, }) .map_err(Error::AggregateStoreError) } diff --git a/src/daemon/scheduler.rs b/src/daemon/scheduler.rs index f211c0545..727063525 100644 --- a/src/daemon/scheduler.rs +++ b/src/daemon/scheduler.rs @@ -237,11 +237,7 @@ impl Scheduler { // to avoid a thundering herd. Note that the operator can always // choose to run bulk operations manually if they know that they // cannot wait. - let ca_list = self - .ca_manager - .ca_list(&self.system_actor) - .map_err(FatalError)?; - let cas = ca_list.cas(); + let cas = self.ca_manager.ca_handles().map_err(FatalError)?; debug!("Adding missing tasks at start up"); // If we have many CAs then we need to apply some jitter @@ -250,10 +246,10 @@ impl Scheduler { let use_jitter = cas.len() >= SCHEDULER_USE_JITTER_CAS_THRESHOLD; - for summary in cas { + for handle in &cas { let ca = self .ca_manager - .get_ca(summary.handle()) + .get_ca(handle) .await .map_err(FatalError)?; let ca_handle = ca.handle(); diff --git a/src/pubd/manager.rs b/src/pubd/manager.rs index 5834b8869..1444abab2 100644 --- a/src/pubd/manager.rs +++ b/src/pubd/manager.rs @@ -471,7 +471,7 @@ mod tests { let publisher_req = make_publisher_req(alice_handle.as_str(), alice.id_cert()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); + let actor = ACTOR_DEF_TEST; server.create_publisher(publisher_req, &actor).unwrap(); let alice_found = @@ -493,7 +493,7 @@ mod tests { let publisher_req = make_publisher_req(alice_handle.as_str(), alice.id_cert()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); + let actor = ACTOR_DEF_TEST; server .create_publisher(publisher_req.clone(), &actor) .unwrap(); @@ -517,7 +517,7 @@ mod tests { let publisher_req = make_publisher_req(alice_handle.as_str(), alice.id_cert()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); + let actor = ACTOR_DEF_TEST; server.create_publisher(publisher_req, &actor).unwrap(); let list_reply = server.list(&alice_handle).unwrap(); @@ -544,7 +544,7 @@ mod tests { let publisher_req = make_publisher_req(alice_handle.as_str(), alice.id_cert()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); + let actor = ACTOR_DEF_TEST; server.create_publisher(publisher_req, &actor).unwrap(); // get the file out of a list_reply @@ -780,7 +780,7 @@ mod tests { let publisher_req = make_publisher_req(alice_handle.as_str(), alice.id_cert()); - let actor = Actor::actor_from_def(ACTOR_DEF_TEST); + let actor = ACTOR_DEF_TEST; server.create_publisher(publisher_req, &actor).unwrap(); // get the file out of a list_reply diff --git a/src/pubd/repository.rs b/src/pubd/repository.rs index 010fa20ea..8aba46a98 100644 --- a/src/pubd/repository.rs +++ b/src/pubd/repository.rs @@ -43,7 +43,7 @@ use crate::{ KrillResult, }, constants::{ - PUBSERVER_CONTENT_NS, PUBSERVER_DFLT, PUBSERVER_NS, + ACTOR_DEF_KRILL, PUBSERVER_CONTENT_NS, PUBSERVER_DFLT, PUBSERVER_NS, REPOSITORY_RRDP_ARCHIVE_DIR, REPOSITORY_RRDP_DIR, REPOSITORY_RSYNC_DIR, RRDP_FIRST_SERIAL, }, @@ -1882,7 +1882,7 @@ impl RepositoryAccessProxy { if self.initialized()? { Err(Error::RepositoryServerAlreadyInitialized) } else { - let actor = Actor::system_actor(); + let actor = ACTOR_DEF_KRILL; let (rrdp_base_uri, rsync_jail) = uris.unpack(); diff --git a/src/ta/mod.rs b/src/ta/mod.rs index e1010ad04..12148ce94 100644 --- a/src/ta/mod.rs +++ b/src/ta/mod.rs @@ -84,9 +84,7 @@ mod tests { let timing = TaTimingConfig::default(); - let actor = crate::commons::actor::Actor::actor_from_def( - crate::constants::ACTOR_DEF_KRILL, - ); + let actor = crate::constants::ACTOR_DEF_KRILL; let proxy_handle = TrustAnchorHandle::new("proxy".into()); let proxy_init = TrustAnchorProxyInitCommand::make( diff --git a/src/upgrades/mod.rs b/src/upgrades/mod.rs index 767246754..d3afb64d0 100644 --- a/src/upgrades/mod.rs +++ b/src/upgrades/mod.rs @@ -15,7 +15,6 @@ use rpki::{ use crate::{ commons::{ - actor::Actor, api::{ AspaDefinition, AspaDefinitionUpdates, CustomerAsn, ProviderAsn, }, @@ -30,7 +29,7 @@ use crate::{ KrillResult, }, constants::{ - CASERVER_NS, CA_OBJECTS_NS, KEYS_NS, KRILL_VERSION, + ACTOR_DEF_KRILL, CASERVER_NS, CA_OBJECTS_NS, KEYS_NS, KRILL_VERSION, PUBSERVER_CONTENT_NS, PUBSERVER_NS, SIGNERS_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, @@ -434,7 +433,7 @@ pub trait UpgradeAggregateStorePre0_14 { // From 0.14.x and up we will have command '0' for the init, // where beforehand we only had an event. We // will have to make up some values for the actor and time. - let actor = Actor::system_actor().to_string(); + let actor = ACTOR_DEF_KRILL; // The time is tricky.. our best guess is to set this to the // same value as the first command, if there @@ -454,7 +453,7 @@ pub trait UpgradeAggregateStorePre0_14 { let command = self.convert_init_event( old_init, handle.clone(), - actor, + actor.audit_name(), time, )?; From 089f0c981185c1a08b8beaea8d8352ed38067126 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 12 Sep 2024 14:20:48 +0200 Subject: [PATCH 02/24] Auth policies are now always available. --- src/daemon/auth/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index 492a51594..8e5be00e6 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -3,22 +3,7 @@ pub mod providers; pub mod common; -#[cfg(feature = "multi-user")] pub mod policy; -#[cfg(not(feature = "multi-user"))] -pub mod policy { - use std::sync::Arc; - - use crate::{commons::KrillResult, daemon::config::Config}; - - #[derive(Clone)] - pub struct AuthPolicy {} - impl AuthPolicy { - pub fn new(_: Arc) -> KrillResult { - Ok(AuthPolicy {}) - } - } -} pub use authorizer::{ Auth, AuthInfo, AuthProvider, Authorizer, Handle, LoggedInUser From c941bfbf30930d7c7c6bc015c3a598aacc60782a Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 30 Oct 2024 16:49:23 +0100 Subject: [PATCH 03/24] Implement new auth provider strategy. --- Cargo.lock | 447 +++++++++++++++--- Cargo.toml | 2 +- src/commons/actor.rs | 232 +-------- src/commons/error.rs | 26 + src/daemon/auth/authorizer.rs | 135 +++--- src/daemon/auth/common/session.rs | 62 ++- src/daemon/auth/mod.rs | 9 +- src/daemon/auth/permission.rs | 182 +++++++ src/daemon/auth/policy.rs | 248 ---------- src/daemon/auth/providers/admin_token.rs | 11 +- src/daemon/auth/providers/config_file.rs | 339 +++++++++++++ .../auth/providers/config_file/config.rs | 16 - src/daemon/auth/providers/config_file/mod.rs | 2 - .../auth/providers/config_file/provider.rs | 275 ----------- src/daemon/auth/providers/mod.rs | 2 +- .../auth/providers/openid_connect/claims.rs | 389 +++++++++++++++ .../auth/providers/openid_connect/config.rs | 38 +- .../auth/providers/openid_connect/mod.rs | 1 + .../auth/providers/openid_connect/provider.rs | 446 ++++------------- src/daemon/auth/roles.rs | 150 ++++++ src/daemon/ca/manager.rs | 11 +- src/daemon/config.rs | 17 +- src/daemon/http/mod.rs | 33 +- src/daemon/http/server.rs | 44 +- src/daemon/http/testbed.rs | 4 +- src/daemon/krillserver.rs | 52 +- src/daemon/scheduler.rs | 12 +- src/pubd/manager.rs | 2 +- tests/common/mod.rs | 2 + 29 files changed, 1818 insertions(+), 1371 deletions(-) create mode 100644 src/daemon/auth/permission.rs delete mode 100644 src/daemon/auth/policy.rs create mode 100644 src/daemon/auth/providers/config_file.rs delete mode 100644 src/daemon/auth/providers/config_file/config.rs delete mode 100644 src/daemon/auth/providers/config_file/mod.rs delete mode 100644 src/daemon/auth/providers/config_file/provider.rs create mode 100644 src/daemon/auth/providers/openid_connect/claims.rs create mode 100644 src/daemon/auth/roles.rs diff --git a/Cargo.lock b/Cargo.lock index fcd69283b..d7160fcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -184,6 +190,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "basic-cookies" version = "0.1.5" @@ -318,7 +330,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", "terminal_size", ] @@ -346,6 +358,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -395,6 +413,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -428,11 +458,38 @@ dependencies = [ "libloading", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + [[package]] name = "darling" -version = "0.13.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -440,27 +497,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 1.0.109", + "strsim", + "syn 2.0.75", ] [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 1.0.109", + "syn 2.0.75", ] [[package]] @@ -469,6 +526,17 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -476,6 +544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -485,6 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -510,12 +580,77 @@ dependencies = [ "winapi", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -614,6 +749,22 @@ dependencies = [ "syslog", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -722,6 +873,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -743,6 +895,17 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.4.6" @@ -755,13 +918,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -802,6 +971,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -992,6 +1170,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.4.0" @@ -999,7 +1188,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1250,6 +1440,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1277,7 +1470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" dependencies = [ "core2", - "hashbrown", + "hashbrown 0.14.5", "rle-decode-fast", ] @@ -1291,6 +1484,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" @@ -1407,13 +1606,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "num-bigint" -version = "0.4.6" +name = "num-bigint-dig" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ + "byteorder", + "lazy_static", + "libm", "num-integer", + "num-iter", "num-traits", + "rand", + "smallvec", + "zeroize", ] [[package]] @@ -1431,6 +1637,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1438,6 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1485,19 +1703,23 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openidconnect" -version = "2.5.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98dd5b7049bac4fdd2233b8c9767d42c05da8006fdb79cc903258556d2b18009" +checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590" dependencies = [ "base64 0.13.1", "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", "http 0.2.12", "itertools 0.10.5", "log", - "num-bigint", "oauth2", + "p256", + "p384", "rand", - "ring 0.16.20", + "rsa", "serde", "serde-value", "serde_derive", @@ -1505,6 +1727,7 @@ dependencies = [ "serde_path_to_error", "serde_plain", "serde_with", + "sha2", "subtle", "thiserror", "url", @@ -1573,6 +1796,30 @@ dependencies = [ "num-traits", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1612,6 +1859,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1625,7 +1881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.4.0", ] [[package]] @@ -1693,6 +1949,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1778,6 +2055,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1982,18 +2268,13 @@ dependencies = [ ] [[package]] -name = "ring" -version = "0.16.20" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", + "hmac", + "subtle", ] [[package]] @@ -2006,8 +2287,8 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] @@ -2039,12 +2320,32 @@ dependencies = [ "chrono", "log", "quick-xml", - "ring 0.17.8", + "ring", "serde", - "untrusted 0.9.0", + "untrusted", "uuid", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -2091,7 +2392,7 @@ checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "log", "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2120,9 +2421,9 @@ version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -2190,6 +2491,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -2322,24 +2637,32 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.14.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.4.0", "serde", + "serde_derive", + "serde_json", "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "1.5.2" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.75", ] [[package]] @@ -2368,6 +2691,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2401,15 +2734,19 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "spin" -version = "0.9.8" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] [[package]] name = "stderrlog" @@ -2448,12 +2785,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -2776,7 +3107,7 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", @@ -2885,12 +3216,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 1906c5bb0..f05663071 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ kvx = { version = "0.9.3", features = ["macros"] } libflate = "2.1.0" log = "0.4" once_cell = { version = "1.7.2", optional = true } -openidconnect = { version = "2.0.0", optional = true, default-features = false } +openidconnect = { version = "3.5.0", optional = true, default-features = false } openssl = { version = "0.10", features = ["v110"] } percent-encoding = "2.3.1" pin-project-lite = "0.2.4" diff --git a/src/commons/actor.rs b/src/commons/actor.rs index cb7188d81..4258dbe58 100644 --- a/src/commons/actor.rs +++ b/src/commons/actor.rs @@ -74,10 +74,10 @@ impl Actor { /// actor, this is the string `"anonymous"`. For user actors, it is their /// user ID. pub fn name(&self) -> &str { - match self.0 { - ActorName::System(ref component) => component, + match &self.0 { + ActorName::System(component) => component, ActorName::Anonymous => "anonymous", - ActorName::User(ref user_id) => user_id.as_ref(), + ActorName::User(user_id) => user_id.as_ref(), } } @@ -104,229 +104,3 @@ impl fmt::Display for Actor { } } - - - -/* -use std::fmt; -use std::sync::Arc; - -use crate::{ - commons::{ - error::{ApiAuthError, Error}, - KrillResult, - }, - daemon::auth::{policy::{AuthPolicy, Permission}, Auth, Handle}, -}; - -#[derive(Clone, Deserialize, Eq, PartialEq, Debug, Serialize)] -pub enum ActorName { - AsString(String), -} - -impl ActorName { - pub fn as_str(&self) -> &str { - match &self { - ActorName::AsString(s) => s, - } - } -} - -impl From for ActorName { - fn from(src: String) -> Self { - Self::AsString(src) - } -} - -impl fmt::Display for ActorName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.as_str().fmt(f) - } -} - - -#[derive(Clone, Debug)] -pub struct ActorDef { - pub name: ActorName, - pub new_auth: Option, - pub auth_error: Option, -} - -impl ActorDef { - pub const fn anonymous() -> ActorDef { - ActorDef { - name: ActorName::AsStaticStr("anonymous"), - new_auth: None, - auth_error: None, - } - } - - pub const fn system(name: &'static str) -> ActorDef { - ActorDef { - name: ActorName::AsStaticStr(name), - new_auth: None, - auth_error: None, - } - } - - pub fn user( - name: ActorName, - new_auth: Option, - ) -> ActorDef { - ActorDef { - name, - new_auth, - auth_error: None, - } - } - - // Takes either a ApiAuthError or a commons::error::Error - pub fn with_auth_error(mut self, api_error: Error) -> Self { - self.auth_error = Some(api_error.into()); - self - } -} - -#[derive(Clone)] -pub struct Actor { - name: ActorName, - new_auth: Option, - policy: Arc, - - #[cfg_attr(not(feature = "multi-user"), allow(dead_code))] - auth_error: Option, -} - -impl PartialEq for Actor { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} - -impl PartialEq for Actor { - fn eq(&self, other: &ActorDef) -> bool { - self.name == other.name - } -} - -impl Actor { - /// Only for krillta - /// - /// No authorizer framework exists for krillta. It is designed as a - /// CLI. Sysadmins should ensure that only trusted people can execute - /// the CLI (and/or read / write its data). - pub fn krillta() -> Actor { - Self::actor_from_def(crate::constants::ACTOR_DEF_KRILLTA) - } - - /// Setup a System Actor - /// - /// This is an admin user used by the system itself. Authorizer frameworks - /// are not relevant to it. - pub fn system_actor() -> Actor { - Self::actor_from_def(crate::constants::ACTOR_DEF_KRILL) - } - - /// Should only be used for system users, i.e. not for mapping - /// logged in users. - pub fn actor_from_def(_actor_def: ActorDef) -> Actor { - unimplemented!() - /* - Actor { - name: actor_def.name.clone(), - new_auth: None, - auth_error: None, - policy: None, - } - */ - } - - /* - /// Only for use in testing - pub fn test_from_details( - name: String, - attrs: HashMap, - ) -> Actor { - Actor { - name: ActorName::AsString(name), - attributes: Attributes::UserDefined(attrs), - is_user: false, - new_auth: None, - auth_error: None, - policy: None, - } - } - */ - - pub fn new(actor_def: ActorDef, policy: Arc) -> Actor { - Actor { - name: actor_def.name.clone(), - new_auth: actor_def.new_auth.clone(), - auth_error: actor_def.auth_error, - policy, - } - } - - pub fn is_user(&self) -> bool { - unimplemented!() - } - - pub fn is_anonymous(&self) -> bool { - unimplemented!() - } - - pub fn new_auth(&self) -> Option { - self.new_auth.clone() - } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - pub fn is_allowed( - &self, - permission: Permission, - resource: Option<&Handle>, - ) -> KrillResult { - trace!( - "Access check: actor={}, permission={}, resource={:?}", - self.name(), permission, resource - ); - - if let Some(api_error) = &self.auth_error { - trace!( - "Authentication denied: \ - actor={}, permission={}, resource={:?}: {}", - self.name(), - permission, - resource, - api_error - ); - return Err(Error::from(api_error.clone())); - } - - let allowed = self.policy.is_allowed(permission, resource); - trace!( - "Access {}: actor={:?}, permission={:?}, \ - resource={:?}", - if allowed { "granted" } else { "denied" }, - self, - permission, - resource - ); - Ok(allowed) - } -} - -impl fmt::Display for Actor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl fmt::Debug for Actor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Actor(name={:?})", self.name()) - } -} -*/ diff --git a/src/commons/error.rs b/src/commons/error.rs index 8f0ecbe6a..87e791caa 100644 --- a/src/commons/error.rs +++ b/src/commons/error.rs @@ -18,6 +18,7 @@ use rpki::{ use crate::{ commons::{ + actor::Actor, api::{ rrdp::PublicationDeltaError, CustomerAsn, ErrorResponse, RoaPayload, @@ -27,6 +28,7 @@ use crate::{ util::httpclient, }, daemon::{ca::RoaPayloadJsonMapKey, http::tls_keys}, + daemon::auth::{Handle, Permission}, ta, upgrades::UpgradeError, }; @@ -127,6 +129,30 @@ pub enum ApiAuthError { ApiInsufficientRights(String), } +impl ApiAuthError { + pub fn insufficient_rights( + actor: &Actor, perm: Permission, resource: Option<&Handle> + ) -> Self { + Self::ApiInsufficientRights( + match resource { + Some(res) => { + format!( + "User '{}' does not have permission '{}' \ + on resource '{}'", + actor, perm, res, + ) + }, + None => { + format!( + "User '{}' does not have permission '{}'", + actor, perm, + ) + } + } + ) + } +} + impl Display for ApiAuthError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 836869965..67ac76df1 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -5,18 +5,18 @@ use std::any::Any; use std::str::FromStr; use std::sync::Arc; use rpki::ca::idexchange::{InvalidHandle, MyHandle}; +use serde::{Deserialize, Serialize}; use crate::commons::actor::Actor; use crate::commons::error::ApiAuthError; use crate::{ commons::{ api::Token, - error::Error, KrillResult, }, daemon::{ auth::{ - policy::{AuthPolicy, AuthPolicyMap, Permission}, + Permission, Role, providers::AdminTokenAuthProvider, }, config::Config, @@ -134,6 +134,28 @@ impl AuthProvider { } } } + + /// Sweeps out session information. + pub fn sweep(&self) -> KrillResult<()> { + match self { + AuthProvider::Token(_) => Ok(()), + #[cfg(feature = "multi-user")] + AuthProvider::ConfigFile(provider) => provider.sweep(), + #[cfg(feature = "multi-user")] + AuthProvider::OpenIdConnect(provider) => provider.sweep(), + } + } + + /// Returns the size of the login session cache. + pub fn login_session_cache_size(&self) -> usize { + match self { + AuthProvider::Token(_) => 0, + #[cfg(feature = "multi-user")] + AuthProvider::ConfigFile(provider) => provider.cache_size(), + #[cfg(feature = "multi-user")] + AuthProvider::OpenIdConnect(provider) => provider.cache_size(), + } + } } @@ -144,7 +166,6 @@ impl AuthProvider { pub struct Authorizer { primary_provider: AuthProvider, legacy_provider: Option, - policy: AuthPolicyMap, } impl Authorizer { @@ -185,7 +206,6 @@ impl Authorizer { Ok(Authorizer { primary_provider, legacy_provider, - policy: AuthPolicyMap::new(&config)?, }) } @@ -237,33 +257,13 @@ impl Authorizer { ) -> KrillResult { let user = self.primary_provider.login(request).await?; - // The user has passed authentication, but may still not be - // authorized to login as that requires a check against the policy - // which cannot be done by the AuthProvider. Check that now. - - if !self.policy.is_user_allowed( - &user.id, Permission::LOGIN, None - ) { - let reason = format!( - "Login denied for user '{}': \ - User is not permitted to 'LOGIN'", - user.id - ); - warn!("{}", reason); - return Err(Error::ApiInsufficientRights(reason)); - } - let filtered_user = LoggedInUser { - token: user.token, - id: user.id, - }; - if log_enabled!(log::Level::Trace) { - trace!("User logged in: {:?}", &filtered_user); + trace!("User logged in: {:?}", &user); } else { - info!("User logged in: {}", &filtered_user.id); + info!("User logged in: {}", &user.id); } - Ok(filtered_user) + Ok(user) } /// Return the URL at which an end-user should be directed to logout with @@ -275,17 +275,14 @@ impl Authorizer { self.primary_provider.logout(request).await } - pub fn get_policy(&self, actor: &Actor) -> Arc { - self.policy.get_policy(actor) + /// Sweeps out session information. + pub fn sweep(&self) -> KrillResult<()> { + self.primary_provider.sweep() } - pub fn is_allowed( - &self, - actor: &Actor, - permission: Permission, - resource: Option<&Handle> - ) -> bool { - self.policy.is_allowed(actor, permission, resource) + /// Returns the size of the login session cache. + pub fn login_session_cache_size(&self) -> usize { + self.primary_provider.login_session_cache_size() } } @@ -312,48 +309,76 @@ pub struct LoggedInUser { #[derive(Clone, Debug)] pub struct AuthInfo { /// The actor for the authenticated user. - pub actor: Actor, - - /// Optional error information if authentication failed. - pub auth_error: Option, + actor: Actor, /// Optional authentication information to be included in a response. - pub new_auth: Option, + new_auth: Option, + + /// Access permissions. + /// + /// This is either a role which we consult to determine access + /// permissions or an authentication error to return instead. + permissions: Result, ApiAuthError>, } impl AuthInfo { - pub fn user(user_id: impl Into>) -> Self { + pub fn user( + user_id: impl Into>, + role: Arc, + ) -> Self { Self { actor: Actor::user(user_id), - auth_error: None, new_auth: None, + permissions: Ok(role), } } + pub fn testbed() -> Self { + Self::user("testbed", Role::testbed().into()) + } + fn anonymous() -> Self { Self { actor: Actor::anonymous(), - auth_error: None, new_auth: None, + permissions: Ok(Role::anonymous().into()), } } fn error(err: impl Into) -> Self { Self { actor: Actor::anonymous(), - auth_error: Some(err.into()), - new_auth: None + new_auth: None, + permissions: Err(err.into()) } } - pub fn with_new_auth( - user_id: impl Into>, - new_auth: Auth - ) -> Self { - Self { - actor: Actor::user(user_id), - auth_error: None, - new_auth: Some(new_auth) + pub fn set_new_auth(&mut self, new_auth: Auth) { + self.new_auth = Some(new_auth); + } + + pub fn actor(&self) -> &Actor { + &self.actor + } + + pub fn take_new_auth(&mut self) -> Option { + self.new_auth.take() + } + + pub fn check_permission( + &self, + permission: Permission, + resource: Option<&Handle> + ) -> Result<(), ApiAuthError> { + if self.permissions.as_ref().map_err(Clone::clone)? + .is_allowed(permission, resource) + { + Ok(()) + } + else { + Err(ApiAuthError::insufficient_rights( + &self.actor, permission, resource + )) } } } @@ -409,7 +434,7 @@ impl Auth { // is required when multi-user is enabled. We always need to pass the handle // into the authorization macro, even if multi-user is not enabled. So we need // this type even then. -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Handle(MyHandle); impl fmt::Display for Handle { diff --git a/src/daemon/auth/common/session.rs b/src/daemon/auth/common/session.rs index 110ea9013..5d286290f 100644 --- a/src/daemon/auth/common/session.rs +++ b/src/daemon/auth/common/session.rs @@ -1,8 +1,11 @@ use std::collections::HashMap; +use std::fmt::Debug; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::engine::Engine as _; +use serde::{Deserialize, Serialize}; +use serde::de::DeserializeOwned; use crate::commons::api::Token; use crate::commons::error::Error; use crate::commons::KrillResult; @@ -12,12 +15,12 @@ use crate::daemon::auth::common::crypt::{CryptState, NonceState}; const MAX_CACHE_SECS: u64 = 30; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ClientSession { +#[derive(Debug, Serialize, Deserialize)] +pub struct ClientSession { pub start_time: u64, pub expires_in: Option, pub user_id: Arc, - pub secrets: HashMap, + pub secrets: S, } #[derive(Debug, Eq, PartialEq)] @@ -27,7 +30,18 @@ pub enum SessionStatus { Expired, } -impl ClientSession { +impl Clone for ClientSession { + fn clone(&self) -> Self { + Self { + start_time: self.start_time, + expires_in: self.expires_in, + user_id: self.user_id.clone(), + secrets: self.secrets.clone(), + } + } +} + +impl ClientSession { pub fn status(&self) -> SessionStatus { if let Some(expires_in) = &self.expires_in { match SystemTime::now().duration_since(UNIX_EPOCH) { @@ -66,15 +80,11 @@ impl ClientSession { SessionStatus::Active } - - pub fn get_secret(&self, key: &str) -> Option<&String> { - self.secrets.get(key) - } } -struct CachedSession { +struct CachedSession { pub evict_after: u64, - pub session: ClientSession, + pub session: ClientSession, } pub type EncryptFn = fn(&[u8], &[u8], &NonceState) -> KrillResult>; @@ -85,20 +95,20 @@ pub type DecryptFn = fn(&[u8], &[u8]) -> KrillResult>; /// the Lagosta UI client) while keeping potentially sensitive data in-memory /// for as short as possible. This cache is NOT responsible for enforcing /// token expiration, that is handled separately by the AuthProvider. -pub struct LoginSessionCache { - cache: RwLock>, +pub struct LoginSessionCache { + cache: RwLock>>, encrypt_fn: EncryptFn, decrypt_fn: DecryptFn, ttl_secs: u64, } -impl Default for LoginSessionCache { +impl Default for LoginSessionCache { fn default() -> Self { Self::new() } } -impl LoginSessionCache { +impl LoginSessionCache { pub fn new() -> Self { LoginSessionCache { cache: RwLock::new(HashMap::new()), @@ -147,7 +157,8 @@ impl LoginSessionCache { .as_secs()) } - fn lookup_session(&self, token: &Token) -> Option { + fn lookup_session(&self, token: &Token) -> Option> + where S: Clone { match self.cache.read() { Ok(readable_cache) => { if let Some(cache_item) = readable_cache.get(token) { @@ -160,7 +171,7 @@ impl LoginSessionCache { None } - fn cache_session(&self, token: &Token, session: &ClientSession) { + fn cache_session(&self, token: &Token, session: ClientSession) { match self.cache.write() { Ok(mut writeable_cache) => { match Self::time_now_secs_since_epoch() { @@ -169,7 +180,7 @@ impl LoginSessionCache { token.clone(), CachedSession { evict_after: now + self.ttl_secs, - session: session.clone(), + session, }, ); } @@ -188,15 +199,16 @@ impl LoginSessionCache { pub fn encode( &self, user_id: Arc, - secrets: HashMap, + secrets: S, crypt_state: &CryptState, expires_in: Option, - ) -> KrillResult { + ) -> KrillResult + where S: Debug + Serialize { let session = ClientSession { start_time: Self::time_now_secs_since_epoch()?, expires_in, user_id, - secrets, + secrets }; debug!("Creating token for session: {:?}", &session); @@ -217,7 +229,7 @@ impl LoginSessionCache { )?; let token = Token::from(BASE64_ENGINE.encode(encrypted_bytes)); - self.cache_session(&token, &session); + self.cache_session(&token, session); Ok(token) } @@ -226,7 +238,8 @@ impl LoginSessionCache { token: Token, key: &CryptState, add_to_cache: bool, - ) -> KrillResult { + ) -> KrillResult> + where S: Clone + DeserializeOwned { if let Some(session) = self.lookup_session(&token) { trace!("Session cache hit for session id {}", &session.user_id); return Ok(session); @@ -246,7 +259,7 @@ impl LoginSessionCache { let unencrypted_bytes = (self.decrypt_fn)(&key.key, &bytes)?; let session = - serde_json::from_slice::(&unencrypted_bytes) + serde_json::from_slice::>(&unencrypted_bytes) .map_err(|err| { debug!( "Invalid bearer token: cannot deserialize: {}", @@ -263,7 +276,7 @@ impl LoginSessionCache { ); if add_to_cache { - self.cache_session(&token, &session); + self.cache_session(&token, session.clone()); } Ok(session) @@ -313,6 +326,7 @@ impl LoginSessionCache { } } + mod tests { #[test] fn basic_login_session_cache_test() { diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index 8e5be00e6..f8532caff 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -3,9 +3,12 @@ pub mod providers; pub mod common; -pub mod policy; - -pub use authorizer::{ +pub use self::authorizer::{ Auth, AuthInfo, AuthProvider, Authorizer, Handle, LoggedInUser }; +pub use self::permission::{Permission, PermissionSet}; +pub use self::roles::{Role, RoleMap}; + +mod permission; +mod roles; diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs new file mode 100644 index 000000000..7c2eec262 --- /dev/null +++ b/src/daemon/auth/permission.rs @@ -0,0 +1,182 @@ +use std::fmt; +use serde::{Deserialize, Serialize}; + + +//------------ Permission ---------------------------------------------------- + +macro_rules! define_permission { + ( $( $variant:ident, )* ) => { + /// The set of available permissions. + /// + /// Each API request requires for the actor to have exactly one of these + /// permissions. + #[derive(Clone, Copy, Debug, Deserialize, Serialize)] + #[allow(non_camel_case_types)] // XXX Fix this + #[repr(u32)] + pub enum Permission { + $( $variant, )* + } + + impl Permission { + pub fn iter() -> impl Iterator { + ALL_PERMISSIONS.iter().copied() + } + } + + impl fmt::Display for Permission { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str( + match *self { + $( + Self::$variant => stringify!($variant), + )* + } + ) + } + } + + const ALL_PERMISSIONS: &'static[Permission] = &[ + $( Permission::$variant, )* + ]; + } +} + +define_permission! { + LOGIN, + PUB_ADMIN, + PUB_LIST, + PUB_READ, + PUB_CREATE, + PUB_DELETE, + CA_LIST, + CA_READ, + CA_CREATE, + CA_UPDATE, + CA_ADMIN, + CA_DELETE, + ROUTES_READ, + ROUTES_UPDATE, + ROUTES_ANALYSIS, + ASPAS_READ, + ASPAS_UPDATE, + ASPAS_ANALYSIS, + BGPSEC_READ, + BGPSEC_UPDATE, + RTA_LIST, + RTA_READ, + RTA_UPDATE, +} + + +//------------ PermissionSet ------------------------------------------------- + +/// A set of permissions. +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(from = "Vec", into = "Vec")] +pub struct PermissionSet(u32); + +impl PermissionSet { + + const fn mask(permission: Permission) -> u32 { + 1u32 << (permission as u32) + } + + pub const fn add(self, permission: Permission) -> Self { + Self(self.0 | Self::mask(permission)) + } + + pub const fn remove(self, permission: Permission) -> Self { + Self(self.0 & !Self::mask(permission)) + } + + pub fn has(self, permission: Permission) -> bool { + self.0 & Self::mask(permission) != 0 + } + + pub fn iter(self) -> impl Iterator { + Permission::iter().filter(move |perm| self.has(*perm)) + } + + const fn from_permissions(mut slice: &[Permission]) -> Self { + let mut res = PermissionSet(0); + while let Some((head, tail)) = slice.split_first() { + res = res.add(*head); + slice = tail; + } + res + } +} + +impl From> for PermissionSet { + fn from(src: Vec) -> Self { + let mut res = Self(0); + for item in src { + res = res.add(item) + } + res + } +} + +impl From for Vec { + fn from(src: PermissionSet) -> Self { + src.iter().collect() + } +} + + +mod policy { + use super::PermissionSet; + use super::Permission::*; + + impl PermissionSet { + pub const ANY: Self = Self(u32::MAX); + + pub const NONE: Self = Self(0); + + pub const READONLY: Self = Self::from_permissions(&[ + CA_LIST, + CA_READ, + PUB_LIST, + PUB_READ, + ROUTES_READ, + ROUTES_ANALYSIS, + ASPAS_READ, + ASPAS_ANALYSIS, + BGPSEC_READ, + RTA_LIST, + RTA_READ + ]); + + pub const READWRITE: Self = Self::from_permissions(&[ + CA_LIST, + CA_READ, + CA_CREATE, + CA_UPDATE, + PUB_LIST, + PUB_READ, + PUB_CREATE, + PUB_DELETE, + ROUTES_READ, + ROUTES_ANALYSIS, + ROUTES_UPDATE, + ASPAS_READ, + ASPAS_UPDATE, + ASPAS_ANALYSIS, + BGPSEC_READ, + BGPSEC_UPDATE, + RTA_LIST, + RTA_READ, + RTA_UPDATE + ]); + + pub const TESTBED: Self = Self::from_permissions(&[ + CA_READ, + CA_UPDATE, + PUB_READ, + PUB_CREATE, + PUB_DELETE, + PUB_ADMIN + ]); + } +} + diff --git a/src/daemon/auth/policy.rs b/src/daemon/auth/policy.rs deleted file mode 100644 index 3290c7349..000000000 --- a/src/daemon/auth/policy.rs +++ /dev/null @@ -1,248 +0,0 @@ -use std::fmt; -use std::collections::HashMap; -use std::sync::Arc; -use crate::commons::KrillResult; -use crate::commons::actor::Actor; -use crate::daemon::auth::Handle; -use crate::daemon::config::Config; - - -//------------ Role ---------------------------------------------------------- - -/// The role of actor has. -/// -/// Permissions aren’t assigned to actors directly but rather to roles to -/// which actors are assigned in turn. -/// -/// Most roles are defined through a string in configuration. However, there -/// are two special roles for anonymous actors and system actors. -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct Role(RoleEnum); - -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub enum RoleEnum { - /// The role used by system actors. - System, - - /// The role used for anonymous users. - Anonymous, - - /// A user role. - User(Arc) -} - -impl Role { - /// Creates a new user role with the given name. - pub fn user(name: impl Into>) -> Self { - Self(RoleEnum::User(name.into().into())) - } - - /// Creates a new system role. - pub const fn system() -> Self { - Self(RoleEnum::System) - } - - /// Creates a new anonymous role. - pub const fn anonymous() -> Self { - Self(RoleEnum::Anonymous) - } - - /// Returns whether the role is a user role. - pub fn is_user(&self) -> bool { - matches!(self.0, RoleEnum::User(_)) - } - - /// Returns whether the role is the system role. - pub fn is_system(&self) -> bool { - matches!(self.0, RoleEnum::System) - } - - /// Returns whether the role is the anonymous role. - pub fn is_anonymous(&self) -> bool { - matches!(self.0, RoleEnum::Anonymous) - } -} - - -//------------ Permission ---------------------------------------------------- - -/// The set of available permissions. -/// -/// Each API request requires for the actor to have exactly one of these -/// permissions. -#[derive(Clone, Copy, Debug)] -#[allow(non_camel_case_types)] // XXX Fix this -#[repr(u32)] -pub enum Permission { - LOGIN = 0, - PUB_ADMIN, - PUB_LIST, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - CA_LIST, - CA_READ, - CA_CREATE, - CA_UPDATE, - CA_ADMIN, - CA_DELETE, - ROUTES_READ, - ROUTES_UPDATE, - ROUTES_ANALYSIS, - ASPAS_READ, - ASPAS_UPDATE, - ASPAS_ANALYSIS, - BGPSEC_READ, - BGPSEC_UPDATE, - RTA_LIST, - RTA_READ, - RTA_UPDATE -} - -impl fmt::Display for Permission { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Permission::*; - - f.write_str( - match *self { - LOGIN => "LOGIN", - PUB_ADMIN => "PUB_ADMIN", - PUB_LIST => "PUB_LIST", - PUB_READ => "PUB_READ", - PUB_CREATE => "PUB_CREATE", - PUB_DELETE => "PUB_DELETE", - CA_LIST => "CA_LIST", - CA_READ => "CA_READ", - CA_CREATE => "CA_CREATE", - CA_UPDATE => "CA_UPDATE", - CA_ADMIN => "CA_ADMIN", - CA_DELETE => "CA_DELETE", - ROUTES_READ => "ROUTES_READ", - ROUTES_UPDATE => "ROUTES_UPDATE", - ROUTES_ANALYSIS => "ROUTES_ANALYSIS", - ASPAS_READ => "ASPAS_READ", - ASPAS_UPDATE => "ASPAS_UPDATE", - ASPAS_ANALYSIS => "APSAS_ANALYSIS", - BGPSEC_READ => "BGPSEC_READ", - BGPSEC_UPDATE => "BGPSEC_UPDATE", - RTA_LIST => "RTA_LIST", - RTA_READ => "RTA_READ", - RTA_UPDATE => "RTA_UPDATE", - } - ) - } -} - - -//------------ PermissionSet ------------------------------------------------- - -/// A set of permissions. -#[derive(Clone, Copy, Debug, Default)] -struct PermissionSet(u32); - -impl PermissionSet { - pub fn add(&mut self, permission: Permission) { - self.0 |= 1u32.checked_shl( - permission as u32 - ).expect("permission size overflow"); - } - - pub fn remove(&mut self, permission: Permission) { - self.0 &= !( - 1u32.checked_shl( - permission as u32 - ).expect("permission size overflow") - ); - } - - pub fn has(&self, permission: Permission) -> bool { - self.0 & ( - 1u32.checked_shl( - permission as u32 - ).expect("permission size overflow") - ) != 0 - } -} - - -//------------ AuthPolicy ---------------------------------------------------- - -/// The policy allows checking for a permission on a resoure. -#[derive(Clone, Default)] -pub struct AuthPolicy { - /// Permissions for requests without specific resources. - none: PermissionSet, - - /// Blanket permission for all resources. - /// - /// This is checked for any resource that isn’t included in - /// the `resources` field. - any: PermissionSet, - - /// Permissions for specific resources. - resources: HashMap, -} - -impl AuthPolicy { - pub fn new(_config: &Config) -> KrillResult { - unimplemented!() - } - - pub fn is_allowed( - &self, - permission: Permission, - resource: Option<&Handle> - ) -> bool { - match resource { - Some(resource) => { - match self.resources.get(resource) { - Some(permissions) => permissions.has(permission), - None => self.any.has(permission), - } - } - None => { - self.none.has(permission) - } - } - } -} - - -//------------ AuthPolicyMap ------------------------------------------------- - -/// A map providing the policy for each known actor. -#[derive(Clone, Default)] -pub struct AuthPolicyMap { - map: HashMap, Arc>, - - default: Arc, -} - -impl AuthPolicyMap { - pub fn new(_config: &Config) -> KrillResult { - unimplemented!() - } - - pub fn get_policy(&self, _actor: &Actor) -> Arc { - unimplemented!() - } - - pub fn is_allowed( - &self, - _actor: &Actor, - _permission: Permission, - _resource: Option<&Handle>, - ) -> bool { - unimplemented!() - } - - pub fn is_user_allowed( - &self, - _user_id: &str, - _permission: Permission, - _resource: Option<&Handle>, - ) -> bool { - unimplemented!() - } -} - diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index 1d25f026a..bcd69578a 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -6,7 +6,7 @@ use crate::{ api::Token, error::Error, util::httpclient, KrillResult, }, - daemon::{auth::{AuthInfo, LoggedInUser}, config::Config}, + daemon::{auth::{AuthInfo, LoggedInUser, Role}, config::Config}, }; // This is NOT an actual relative path to redirect to. Instead it is the path @@ -19,6 +19,7 @@ const LAGOSTA_LOGIN_ROUTE_PATH: &str = "/login"; pub struct AdminTokenAuthProvider { required_token: Token, user_id: Arc, + role: Arc, } impl AdminTokenAuthProvider { @@ -27,6 +28,7 @@ impl AdminTokenAuthProvider { required_token: config.admin_token.clone(), // XXX Get from config. user_id: "admin".into(), + role: Role::admin().into(), } } } @@ -42,7 +44,9 @@ impl AdminTokenAuthProvider { let res = match httpclient::get_bearer_token(request) { Some(token) if token == self.required_token => { - Ok(Some(AuthInfo::user(self.user_id.clone()))) + Ok(Some(AuthInfo::user( + self.user_id.clone(), self.role.clone() + ))) } Some(_) => Err(Error::ApiInvalidCredentials( "Invalid bearer token".to_string(), @@ -79,7 +83,7 @@ impl AdminTokenAuthProvider { request: &HyperRequest, ) -> KrillResult { if let Ok(Some(info)) = self.authenticate(request) { - info!("User logged out: {}", info.actor.name()); + info!("User logged out: {}", info.actor().name()); } // Logout is complete, direct Lagosta to show the user the Lagosta @@ -87,3 +91,4 @@ impl AdminTokenAuthProvider { Ok(HttpResponse::text_no_cache(b"/".to_vec())) } } + diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs new file mode 100644 index 000000000..eb001720e --- /dev/null +++ b/src/daemon/auth/providers/config_file.rs @@ -0,0 +1,339 @@ +use std::collections::HashMap; +use std::sync::Arc; +use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; +use base64::engine::Engine as _; +use unicode_normalization::UnicodeNormalization; +use crate::commons::KrillResult; +use crate::commons::util::httpclient; +use crate::commons::api::Token; +use crate::commons::error::{ApiAuthError, Error}; +use crate::constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}; +use crate::daemon::auth::{Auth, AuthInfo, LoggedInUser, Permission, RoleMap}; +use crate::daemon::auth::common::crypt; +use crate::daemon::auth::common::session::{ClientSession, LoginSessionCache}; +use crate::daemon::config::Config; +use crate::daemon::http::{HttpResponse, HyperRequest}; + + +//------------ Constants ----------------------------------------------------- + +/// The location of the login page in Krill UI. +const UI_LOGIN_ROUTE_PATH: &str = "/login?withId=true"; + + +//------------ ConfigFileAuthProvider ---------------------------------------- + +pub struct ConfigFileAuthProvider { + users: HashMap, + roles: Arc, + session_key: crypt::CryptState, + session_cache: SessionCache, + fake_password_hash: String, + fake_salt: String, +} + +impl ConfigFileAuthProvider { + pub fn new( + config: &Config, + ) -> KrillResult { + let auth_users = config.auth_users.as_ref().ok_or_else(|| { + Error::ConfigError("Missing [auth_users] config section!".into()) + })?; + + let roles = config.auth_roles.clone(); + let mut users = HashMap::new(); + for (id, details) in auth_users { + let password_hash = details.password_hash.as_ref().ok_or_else(|| { + Error::ConfigError(format!( + "Password hash missing for user '{}'", id + )) + })?.clone(); + let salt = details.salt.as_ref().ok_or_else(|| { + Error::ConfigError(format!( + "Password salt missing for user '{}'", id + )) + })?.clone(); + if !roles.contains(&details.role) { + return Err(Error::ConfigError(format!( + "Undefined role '{}' for user '{}'", details.role, id + ))); + } + users.insert( + id.clone(), + UserDetails { + password_hash: password_hash.into(), + salt, + role: details.role.clone().into(), + } + ); + } + + let session_key = Self::init_session_key(config)?; + + Ok(Self { + users, + roles, + session_key, + session_cache: SessionCache::new(), + fake_password_hash: hex::encode("fake password hash"), + fake_salt: hex::encode("fake salt"), + }) + } + + fn init_session_key(config: &Config) -> KrillResult { + debug!("Initializing login session encryption key"); + crypt::crypt_init(config) + } + + /// Parse HTTP Basic Authorization header + fn get_auth(&self, request: &HyperRequest) -> Option { + let header = + request.headers().get(hyper::http::header::AUTHORIZATION)?; + let auth = header.to_str().ok()?.strip_prefix("Basic ")?; + let auth = BASE64_ENGINE.decode(auth).ok()?; + let auth = String::from_utf8(auth).ok()?; + let (username, password) = auth.split_once(':')?; + + Some(Auth::UsernameAndPassword { + username: username.to_string(), + password: password.to_string(), + }) + } + + fn auth_from_session( + &self, session: &Session + ) -> Result { + self.roles.get(&session.secrets.role).map(|role| { + AuthInfo::user(session.user_id.clone(), role) + }) + } +} + +impl ConfigFileAuthProvider { + pub fn authenticate( + &self, + request: &HyperRequest, + ) -> KrillResult> { + if log_enabled!(log::Level::Trace) { + trace!("Attempting to authenticate the request.."); + } + + let res = match httpclient::get_bearer_token(request) { + Some(token) => { + // see if we can decode, decrypt and deserialize the users + // token into a login session structure + let session = self.session_cache.decode( + token, + &self.session_key, + true, + )?; + + trace!("user_id={}", session.user_id); + + Ok(Some(self.auth_from_session(&session)?)) + } + _ => Ok(None), + }; + + if log_enabled!(log::Level::Trace) { + trace!("Authentication result: {:?}", res); + } + + res + } + + pub fn get_login_url(&self) -> KrillResult { + // Direct Lagosta to show the user the Lagosta API token login form + Ok(HttpResponse::text_no_cache(UI_LOGIN_ROUTE_PATH.into())) + } + + pub fn login(&self, request: &HyperRequest) -> KrillResult { + use scrypt::scrypt; + + let (username, password) = match self.get_auth(request) { + Some(Auth::UsernameAndPassword { username, password }) => { + (username, password) + } + _ => { + trace!("Missing pr incomplete credentials for login attempt"); + return Err(Error::ApiInvalidCredentials( + "Missing credentials".to_string(), + )) + } + }; + + // Do NOT bail out if the user is not known because then the + // unknown user path would return very quickly + // compared to the known user path and timing differences can aid + // attackers. + let (user_password_hash, user_salt) = + match self.users.get(&username) { + Some(user) => { + (user.password_hash.to_string(), user.salt.clone()) + } + None => ( + self.fake_password_hash.clone(), + self.fake_salt.clone(), + ), + }; + + let username = username.trim().nfkc().collect::(); + let password = password.trim().nfkc().collect::(); + + // hash twice with two different salts + // legacy hashing strategy to be compatible with lagosta + let params = scrypt::Params::new( + PW_HASH_LOG_N, + PW_HASH_R, + PW_HASH_P, + scrypt::Params::RECOMMENDED_LEN, + ) + .unwrap(); + let weak_salt = format!("krill-lagosta-{username}"); + let weak_salt = weak_salt.nfkc().collect::(); + + let mut interim_hash: [u8; 32] = [0; 32]; + scrypt( + password.as_bytes(), + weak_salt.as_bytes(), + ¶ms, + &mut interim_hash, + ) + .unwrap(); + + let strong_salt: Vec = hex::decode(user_salt).unwrap(); + let mut hashed_hash: [u8; 32] = [0; 32]; + scrypt( + &interim_hash, + strong_salt.as_slice(), + ¶ms, + &mut hashed_hash, + ) + .unwrap(); + + let encoded_hash = hex::encode(hashed_hash); + + // And now finally check the user, so that both known and + // unknown user code paths do the same work + // and don't result in an obvious timing difference between + // the two scenarios which could potentially + // be used to discover user names. + if encoded_hash != user_password_hash { + trace!("Unknown user {}", username); + return Err(Error::ApiInvalidCredentials( + "Incorrect credentials".to_string(), + )) + } + + let user = match self.users.get(username.as_str()) { + Some(user) => user, + None => { + trace!("Incorrect password for user {}", username); + return Err(Error::ApiInvalidCredentials( + "Incorrect credentials".to_string(), + )); + } + }; + + // Check that the user is allowed to log in. + let role = self.roles.get(&user.role)?; + + if !role.is_allowed(Permission::LOGIN, None) { + let reason = format!( + "Login denied for user '{}': \ + User is not permitted to 'LOGIN'", + username, + ); + warn!("{}", reason); + return Err(Error::ApiInsufficientRights(reason)); + } + + // All good: create a token and return. + let api_token = self.session_cache.encode( + username.clone().into(), + SessionSecret { role: user.role.clone() }, + &self.session_key, + None, + )?; + + Ok(LoggedInUser { + token: api_token, + id: username, + }) + } + + pub fn logout( + &self, + request: &HyperRequest, + ) -> KrillResult { + match httpclient::get_bearer_token(request) { + Some(token) => { + self.session_cache.remove(&token); + + if let Ok(Some(info)) = self.authenticate(request) { + info!("User logged out: {}", info.actor().name()); + } + } + _ => { + warn!( + "Unexpectedly received a logout request \ + without a session token." + ); + } + } + + // Logout is complete, direct Lagosta to show the user the Lagosta + // index page + Ok(HttpResponse::text_no_cache("/".into())) + } + + pub fn sweep(&self) -> KrillResult<()> { + self.session_cache.sweep() + } + + pub fn cache_size(&self) -> usize { + self.session_cache.size() + } +} + + +//------------ ConfigAuthUsers ----------------------------------------------- + +pub type ConfigAuthUsers = HashMap; + + +//------------ ConfigUserDetails --------------------------------------------- + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConfigUserDetails { + #[serde(default)] + pub role: String, + + // optional so that OpenIDConnectAuthProvider can also use config file + // user defined attributes without requiring a dummy password hash + // and salt + pub password_hash: Option, + + pub salt: Option, +} + + +//------------ UserDetails --------------------------------------------------- + +struct UserDetails { + password_hash: Token, + salt: String, + role: Arc, +} + + +//------------ SessionSecret et al ------------------------------------------- + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct SessionSecret { + role: Arc, +} + +type SessionCache = LoginSessionCache; +type Session = ClientSession; + diff --git a/src/daemon/auth/providers/config_file/config.rs b/src/daemon/auth/providers/config_file/config.rs deleted file mode 100644 index dcfea90e0..000000000 --- a/src/daemon/auth/providers/config_file/config.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::collections::HashMap; - -pub type ConfigAuthUsers = HashMap; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ConfigUserDetails { - #[serde(default)] - pub role: String, - - // optional so that OpenIDConnectAuthProvider can also use config file - // user defined attributes without requiring a dummy password hash - // and salt - pub password_hash: Option, - - pub salt: Option, -} diff --git a/src/daemon/auth/providers/config_file/mod.rs b/src/daemon/auth/providers/config_file/mod.rs deleted file mode 100644 index 84c6e892e..000000000 --- a/src/daemon/auth/providers/config_file/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod provider; diff --git a/src/daemon/auth/providers/config_file/provider.rs b/src/daemon/auth/providers/config_file/provider.rs deleted file mode 100644 index f9ea4365e..000000000 --- a/src/daemon/auth/providers/config_file/provider.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; -use base64::engine::Engine as _; -use unicode_normalization::UnicodeNormalization; - -use crate::daemon::http::{HttpResponse, HyperRequest}; -use crate::{ - commons::{ - api::Token, error::Error, util::httpclient, - KrillResult, - }, - constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}, - daemon::{ - auth::common::{ - crypt::{self, CryptState}, - session::*, - }, - auth::providers::config_file::config::ConfigUserDetails, - auth::{Auth, AuthInfo, LoggedInUser}, - config::Config, - }, -}; - -const UI_LOGIN_ROUTE_PATH: &str = "/login?withId=true"; - -struct UserDetails { - password_hash: Token, - salt: String, -} - -fn get_checked_config_user( - id: &str, - user: &ConfigUserDetails, -) -> KrillResult { - let password_hash = user - .password_hash - .as_ref() - .ok_or_else(|| { - Error::ConfigError(format!( - "Password hash missing for user '{}'", - id - )) - })? - .to_string(); - - let salt = user - .salt - .as_ref() - .ok_or_else(|| { - Error::ConfigError(format!( - "Password salt missing for user '{}'", - id - )) - })? - .to_string(); - - Ok(UserDetails { - password_hash: Token::from(password_hash), - salt, - }) -} - -pub struct ConfigFileAuthProvider { - users: HashMap, - session_key: CryptState, - session_cache: Arc, - fake_password_hash: String, - fake_salt: String, -} - -impl ConfigFileAuthProvider { - pub fn new( - config: Arc, - session_cache: Arc, - ) -> KrillResult { - match &config.auth_users { - Some(auth_users) => { - let mut users = HashMap::new(); - for (k, v) in auth_users.iter() { - users.insert(k.clone(), get_checked_config_user(k, v)?); - } - - let session_key = Self::init_session_key(&config)?; - - Ok(ConfigFileAuthProvider { - users, - session_key, - session_cache, - fake_password_hash: hex::encode("fake password hash"), - fake_salt: hex::encode("fake salt"), - }) - } - None => Err(Error::ConfigError( - "Missing [auth_users] config section!".into(), - )), - } - } - - fn init_session_key(config: &Config) -> KrillResult { - debug!("Initializing login session encryption key"); - crypt::crypt_init(config) - } - - /// Parse HTTP Basic Authorization header - fn get_auth(&self, request: &HyperRequest) -> Option { - let header = - request.headers().get(hyper::http::header::AUTHORIZATION)?; - let auth = header.to_str().ok()?.strip_prefix("Basic ")?; - let auth = BASE64_ENGINE.decode(auth).ok()?; - let auth = String::from_utf8(auth).ok()?; - let (username, password) = auth.split_once(':')?; - - Some(Auth::UsernameAndPassword { - username: username.to_string(), - password: password.to_string(), - }) - } -} - -impl ConfigFileAuthProvider { - pub fn authenticate( - &self, - request: &HyperRequest, - ) -> KrillResult> { - if log_enabled!(log::Level::Trace) { - trace!("Attempting to authenticate the request.."); - } - - let res = match httpclient::get_bearer_token(request) { - Some(token) => { - // see if we can decode, decrypt and deserialize the users - // token into a login session structure - let session = self.session_cache.decode( - token, - &self.session_key, - true, - )?; - - trace!("user_id={}", session.user_id); - - Ok(Some(AuthInfo::user(session.user_id))) - } - _ => Ok(None), - }; - - if log_enabled!(log::Level::Trace) { - trace!("Authentication result: {:?}", res); - } - - res - } - - pub fn get_login_url(&self) -> KrillResult { - // Direct Lagosta to show the user the Lagosta API token login form - Ok(HttpResponse::text_no_cache(UI_LOGIN_ROUTE_PATH.into())) - } - - pub fn login(&self, request: &HyperRequest) -> KrillResult { - if let Some(Auth::UsernameAndPassword { username, password }) = - self.get_auth(request) - { - use scrypt::scrypt; - - // Do NOT bail out if the user is not known because then the - // unknown user path would return very quickly - // compared to the known user path and timing differences can aid - // attackers. - let (user_password_hash, user_salt) = - match self.users.get(&username) { - Some(user) => { - (user.password_hash.to_string(), user.salt.clone()) - } - None => ( - self.fake_password_hash.clone(), - self.fake_salt.clone(), - ), - }; - - let username = username.trim().nfkc().collect::(); - let password = password.trim().nfkc().collect::(); - - // hash twice with two different salts - // legacy hashing strategy to be compatible with lagosta - let params = scrypt::Params::new( - PW_HASH_LOG_N, - PW_HASH_R, - PW_HASH_P, - scrypt::Params::RECOMMENDED_LEN, - ) - .unwrap(); - let weak_salt = format!("krill-lagosta-{username}"); - let weak_salt = weak_salt.nfkc().collect::(); - - let mut interim_hash: [u8; 32] = [0; 32]; - scrypt( - password.as_bytes(), - weak_salt.as_bytes(), - ¶ms, - &mut interim_hash, - ) - .unwrap(); - - let strong_salt: Vec = hex::decode(user_salt).unwrap(); - let mut hashed_hash: [u8; 32] = [0; 32]; - scrypt( - &interim_hash, - strong_salt.as_slice(), - ¶ms, - &mut hashed_hash, - ) - .unwrap(); - - let encoded_hash = hex::encode(hashed_hash); - - if encoded_hash == user_password_hash { - // And now finally check the user, so that both known and - // unknown user code paths do the same work - // and don't result in an obvious timing difference between - // the two scenarios which could potentially - // be used to discover user names. - if let Some(_user) = self.users.get(username.as_str()) { - let api_token = self.session_cache.encode( - username.clone().into(), - HashMap::new(), - &self.session_key, - None, - )?; - - Ok(LoggedInUser { - token: api_token, - id: username.clone(), - }) - } else { - trace!("Incorrect password for user {}", username); - Err(Error::ApiInvalidCredentials( - "Incorrect credentials".to_string(), - )) - } - } else { - trace!("Unknown user {}", username); - Err(Error::ApiInvalidCredentials( - "Incorrect credentials".to_string(), - )) - } - } else { - trace!("Missing pr incomplete credentials for login attempt"); - Err(Error::ApiInvalidCredentials( - "Missing credentials".to_string(), - )) - } - } - - pub fn logout( - &self, - request: &HyperRequest, - ) -> KrillResult { - match httpclient::get_bearer_token(request) { - Some(token) => { - self.session_cache.remove(&token); - - if let Ok(Some(info)) = self.authenticate(request) { - info!("User logged out: {}", info.actor.name()); - } - } - _ => { - warn!("Unexpectedly received a logout request without a session token."); - } - } - - // Logout is complete, direct Lagosta to show the user the Lagosta - // index page - Ok(HttpResponse::text_no_cache("/".into())) - } -} diff --git a/src/daemon/auth/providers/mod.rs b/src/daemon/auth/providers/mod.rs index 3e8e8a489..23cbe4ddd 100644 --- a/src/daemon/auth/providers/mod.rs +++ b/src/daemon/auth/providers/mod.rs @@ -8,6 +8,6 @@ pub mod openid_connect; pub use admin_token::AdminTokenAuthProvider; #[cfg(feature = "multi-user")] -pub use config_file::provider::ConfigFileAuthProvider; +pub use config_file::ConfigFileAuthProvider; #[cfg(feature = "multi-user")] pub use openid_connect::provider::OpenIDConnectAuthProvider; diff --git a/src/daemon/auth/providers/openid_connect/claims.rs b/src/daemon/auth/providers/openid_connect/claims.rs new file mode 100644 index 000000000..4913d7ffc --- /dev/null +++ b/src/daemon/auth/providers/openid_connect/claims.rs @@ -0,0 +1,389 @@ +//! Processing OpenID Connect claims. + +use std::sync::Arc; +use regex::{Regex, Replacer}; +use serde::de::{Deserialize, Deserializer, Error as _}; +use serde_json::{Number as JsonNumber, Value as JsonValue}; +use crate::commons::KrillResult; +use crate::commons::error::Error; +use super::util::{FlexibleIdTokenClaims, FlexibleUserInfoClaims}; +use super::config::ConfigAuthOpenIDConnectClaim; + + +//------------ Claims -------------------------------------------------------- + +pub struct Claims<'a> { + claims_conf: &'a [ConfigAuthOpenIDConnectClaim], + id_token_claims: &'a FlexibleIdTokenClaims, + user_info_claims: Option, + + id_standard: Option, + id_additional: Option, + user_standard: Option, + user_additional: Option, +} + +impl<'a> Claims<'a> { + pub fn new( + claims_conf: &'a [ConfigAuthOpenIDConnectClaim], + id_token_claims: &'a FlexibleIdTokenClaims, + user_info_claims: Option, + ) -> Self { + Self { + claims_conf, + id_token_claims, user_info_claims, + id_standard: None, id_additional: None, + user_standard: None, user_additional: None, + } + } + + pub fn extract_id(&mut self) -> KrillResult { + self.extract_claim("id") + } + + pub fn extract_role(&mut self) -> KrillResult> { + self.extract_claim("role").map(Into::into) + } + + fn extract_claim(&mut self, dest: &str) -> KrillResult { + for conf in self.claims_conf.iter().filter(|conf| conf.dest == dest) { + if let Some(res) = self.process_claim_conf(conf)? { + return Ok(res) + } + } + + Err(Self::internal_error( + format!("OpenID Connect: no value found for '{}' claim.", dest), + None + )) + } + + fn process_claim_conf( + &mut self, conf: &ConfigAuthOpenIDConnectClaim + ) -> KrillResult> { + use self::ClaimSource::*; + + match conf.source { + Some(IdTokenStandardClaim) => { + Self::process_claim_json(conf, self.id_standard()?) + } + Some(IdTokenAdditionalClaim) => { + Self::process_claim_json(conf, self.id_additional()?) + } + Some(UserInfoStandardClaim) => { + self.user_standard()?.and_then(|json| { + Self::process_claim_json(conf, json).transpose() + }).transpose() + } + Some(UserInfoAdditionalClaim) => { + self.user_additional()?.and_then(|json| { + Self::process_claim_json(conf, json).transpose() + }).transpose() + } + None => { + if let Some(res) = Self::process_claim_json( + conf, self.id_standard()? + )? { + return Ok(Some(res)) + } + if let Some(res) = Self::process_claim_json( + conf, self.id_additional()? + )? { + return Ok(Some(res)) + } + if let Some(res) = self.user_standard()?.and_then(|json| { + Self::process_claim_json(conf, json).transpose() + }).transpose()? { + return Ok(Some(res)) + } + self.user_additional()?.and_then(|json| { + Self::process_claim_json(conf, json).transpose() + }).transpose() + } + } + } + + fn id_standard(&mut self) -> KrillResult<&JsonValue> { + if self.id_standard.is_none() { + self.id_standard = Some( + serde_json::to_value(self.id_token_claims).map_err(|_| { + Self::internal_error( + "OpenID Connect: \ + failed to generate standard ID token claims", + None + ) + })? + ) + } + Ok(self.id_standard.as_ref().unwrap()) + } + + fn id_additional(&mut self) -> KrillResult<&JsonValue> { + if self.id_additional.is_none() { + self.id_additional = Some( + serde_json::to_value( + self.id_token_claims.additional_claims() + ).map_err(|_| { + Self::internal_error( + "OpenID Connect: \ + failed to generate additional ID token claims", + None + ) + })? + ) + } + Ok(self.id_additional.as_ref().unwrap()) + } + + fn user_standard(&mut self) -> KrillResult> { + let claims = match self.user_info_claims.as_ref() { + Some(claims) => claims, + None => return Ok(None) + }; + if self.user_standard.is_none() { + self.user_standard = Some( + serde_json::to_value(claims).map_err(|_| { + Self::internal_error( + "OpenID Connect: \ + failed to generate standard user info claims", + None + ) + })? + ) + } + Ok(self.user_standard.as_ref()) + } + + fn user_additional(&mut self) -> KrillResult> { + let claims = match self.user_info_claims.as_ref() { + Some(claims) => claims, + None => return Ok(None) + }; + if self.user_additional.is_none() { + self.user_additional = Some( + serde_json::to_value(claims.additional_claims()).map_err(|_| { + Self::internal_error( + "OpenID Connect: \ + failed to generate standard user info claims", + None + ) + })? + ) + } + Ok(self.user_additional.as_ref()) + } + + fn process_claim_json( + conf: &ConfigAuthOpenIDConnectClaim, + json: &JsonValue, + ) -> KrillResult> { + let object = match json { + JsonValue::Object(object) => object, + _ => return Ok(None) + }; + let value = match object.get(&conf.claim) { + Some(value) => value, + None => return Ok(None) + }; + match value { + JsonValue::Array(array) => Self::process_claim_array(conf, array), + JsonValue::Bool(true) => Self::process_claim_str(conf, "true"), + JsonValue::Bool(false) => Self::process_claim_str(conf, "false"), + JsonValue::String(s) => Self::process_claim_str(conf, s), + JsonValue::Number(num) => Self::process_claim_number(conf, num), + _ => Ok(None) + } + } + + fn process_claim_array( + conf: &ConfigAuthOpenIDConnectClaim, + array: &[JsonValue], + ) -> KrillResult> { + for item in array { + let res = match item { + JsonValue::Bool(true) => { + Self::process_claim_str(conf, "true")? + } + JsonValue::Bool(false) => { + Self::process_claim_str(conf, "false")? + } + JsonValue::String(s) => { + Self::process_claim_str(conf, s)? + } + JsonValue::Number(num) => { + Self::process_claim_number(conf, num)? + } + _ => None + }; + if let Some(res) = res { + return Ok(Some(res)) + } + } + Ok(None) + } + + fn process_claim_number( + conf: &ConfigAuthOpenIDConnectClaim, + num: &JsonNumber + ) -> KrillResult> { + Self::process_claim_str(conf, &num.to_string()) + } + + fn process_claim_str( + conf: &ConfigAuthOpenIDConnectClaim, + s: &str, + ) -> KrillResult> { + if let Some(expr) = conf.match_expr.as_ref() { + match conf.subst.as_ref() { + Some(subst) => { + if subst.no_expansion { + match expr.0.find(s) { + Some(m) => { + let mut res = String::with_capacity(s.len()); + res.push_str(&s[..m.start()]); + res.push_str(&subst.expr); + res.push_str(&s[m.end()..]); + Ok(Some(res)) + } + None => Ok(None) + } + } + else { + match expr.0.captures(s) { + Some(c) => { + let mut res = String::with_capacity( + subst.expr.len() + ); + c.expand(&subst.expr, &mut res); + Ok(Some(res)) + } + None => Ok(None) + } + } + } + None => { + if expr.0.is_match(s) { + Ok(Some(s.into())) + } + else { + Ok(None) + } + } + } + } + else { + // If there is no match expression, the value always matches and + // we return it (even if there is a subst expression -- we just + // ignore it). + Ok(Some(s.into())) + } + } + + + /// Log and convert the given error such that the detailed, possibly + /// sensitive details are logged and only the high level statement + /// about the error is passed back to the caller. + fn internal_error(msg: S, additional_info: Option) -> Error + where + S: Into, + { + let msg: String = msg.into(); + match additional_info { + Some(additional_info) => { + warn!("{} [additional info: {}]", msg, additional_info.into()) + } + None => warn!("{}", msg), + }; + Error::ApiLoginError(msg) + } +} + + +//------------ MatchExpression ----------------------------------------------- + +#[derive(Clone, Debug)] +pub struct MatchExpression(Regex); + +impl<'de> Deserialize<'de> for MatchExpression { + fn deserialize>( + deserializer: D + ) -> Result { + String::deserialize(deserializer).and_then(|s| { + Regex::try_from(s).map_err(D::Error::custom) + }).map(Self) + } +} + + +//------------ SubstExpression ----------------------------------------------- + +#[derive(Clone, Debug)] +pub struct SubstExpression { + expr: String, + no_expansion: bool, +} + +impl<'de> Deserialize<'de> for SubstExpression { + fn deserialize>( + deserializer: D + ) -> Result { + let mut expr = String::deserialize(deserializer)?; + let no_expansion = expr.no_expansion().is_some(); + Ok(Self { expr, no_expansion }) + } +} + + +//------------ ClaimSource --------------------------------------------------- + +#[derive(Clone, Copy, Debug)] +pub enum ClaimSource { + IdTokenStandardClaim, + IdTokenAdditionalClaim, + UserInfoStandardClaim, + UserInfoAdditionalClaim, +} + +impl std::fmt::Display for ClaimSource { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use self::ClaimSource::*; + + f.write_str( + match self { + IdTokenStandardClaim => "id-token-standard-claim", + IdTokenAdditionalClaim => "id-token-additional-claim", + UserInfoStandardClaim => "user-info-standard-claim", + UserInfoAdditionalClaim => "user-info-additional-claim", + } + ) + } +} + +impl<'de> Deserialize<'de> for ClaimSource { + fn deserialize( + d: D, + ) -> Result + where + D: Deserializer<'de>, + { + use self::ClaimSource::*; + + match <&'de str>::deserialize(d)? { + "id-token-standard-claim" => Ok(IdTokenStandardClaim), + "id-token-additional-claim" => Ok(IdTokenAdditionalClaim), + "user-info-standard-claim" => Ok(UserInfoStandardClaim), + "user-info-additional-claim" => Ok(UserInfoAdditionalClaim), + s => { + Err(serde::de::Error::custom( + format!( + "expected \"id-token-additional-claim\", \ + \"id-token-standard-claim\", \ + \"user-info-standard-claim\", or \ + \"user-info-additional-claim\", found : \"{}\"", + s + ))) + } + } + } +} + diff --git a/src/daemon/auth/providers/openid_connect/config.rs b/src/daemon/auth/providers/openid_connect/config.rs index 3295f78c3..75ea5df8a 100644 --- a/src/daemon/auth/providers/openid_connect/config.rs +++ b/src/daemon/auth/providers/openid_connect/config.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; - use serde::Deserialize; +use super::claims::{ClaimSource, MatchExpression, SubstExpression}; pub struct ConfigDefaults {} @@ -12,7 +12,8 @@ pub struct ConfigAuthOpenIDConnect { pub client_secret: String, - pub id_claim: String, + #[serde(default = "default_claims")] + pub claims: Vec, #[serde(default)] pub extra_login_scopes: Vec, @@ -31,7 +32,38 @@ pub struct ConfigAuthOpenIDConnect { } fn default_prompt_for_login() -> bool { - // On by default for backward compatability. See: https://github.com/NLnetLabs/krill/issues/614 + // On by default for backward compatability. + // See: https://github.com/NLnetLabs/krill/issues/614 true } + +#[derive(Clone, Debug, Deserialize)] +pub struct ConfigAuthOpenIDConnectClaim { + pub dest: String, + pub source: Option, + pub claim: String, + #[serde(rename = "match")] + pub match_expr: Option, + pub subst: Option, +} + +fn default_claims() -> Vec { + vec![ + ConfigAuthOpenIDConnectClaim { + dest: "id".into(), + source: None, + claim: "email".into(), + match_expr: None, + subst: None, + }, + ConfigAuthOpenIDConnectClaim { + dest: "id".into(), + source: None, + claim: "role".into(), + match_expr: None, + subst: None, + }, + ] +} + diff --git a/src/daemon/auth/providers/openid_connect/mod.rs b/src/daemon/auth/providers/openid_connect/mod.rs index 5551434b5..be28b8c33 100644 --- a/src/daemon/auth/providers/openid_connect/mod.rs +++ b/src/daemon/auth/providers/openid_connect/mod.rs @@ -1,6 +1,7 @@ #[macro_use] pub mod util; +pub mod claims; pub mod config; pub mod httpclient; pub mod provider; diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index 8b7a2a6a3..e19113581 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -28,7 +28,6 @@ //! [openid-connect-rpinitiated-1_0]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html use std::{ - collections::HashMap, ops::Deref, sync::Arc, time::Instant, @@ -70,7 +69,6 @@ use crate::{ session::*, }, providers::openid_connect::{ - config::ConfigAuthOpenIDConnect, httpclient::logging_http_client, util::{ FlexibleClient, FlexibleIdTokenClaims, @@ -78,12 +76,14 @@ use crate::{ WantedMeta, }, }, - Auth, AuthInfo, LoggedInUser, + Auth, AuthInfo, LoggedInUser, Permission, }, config::Config, http::auth::{url_encode, AUTH_CALLBACK_ENDPOINT}, }, }; +use super::claims::Claims; +use super::config::ConfigAuthOpenIDConnect; //------------ Constants ----------------------------------------------------- @@ -97,36 +97,6 @@ const NONCE_COOKIE_NAME: &str = "__Host-krill_login_nonce"; const CSRF_COOKIE_NAME: &str = "__Host-krill_login_csrf_hash"; -//------------ TokenKind ----------------------------------------------------- - -#[allow(clippy::enum_variant_names)] -enum TokenKind { - AccessToken, - RefreshToken, - IdToken, -} - -impl From for String { - fn from(token_kind: TokenKind) -> Self { - match token_kind { - TokenKind::AccessToken => String::from("access_token"), - TokenKind::RefreshToken => String::from("refresh_token"), - TokenKind::IdToken => String::from("id_token"), - } - } -} - -impl From for &'static str { - fn from(token_kind: TokenKind) -> Self { - match token_kind { - TokenKind::AccessToken => "access_token", - TokenKind::RefreshToken => "refresh_token", - TokenKind::IdToken => "id_token", - } - } -} - - //------------ LogoutMode ---------------------------------------------------- enum LogoutMode { @@ -158,11 +128,48 @@ pub struct ProviderConnectionProperties { } +//------------ SessionSecrets ------------------------------------------------ + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct SessionSecrets { + role: Arc, + access_token: String, + refresh_token: Option, + id_token: Option +} + +impl SessionSecrets { + fn new( + role: impl Into>, + token_response: &FlexibleTokenResponse, + ) -> Self { + Self { + role: role.into(), + access_token: token_response.access_token().secret().clone(), + refresh_token: token_response.refresh_token().as_ref().map(|t| { + t.secret().clone() + }), + id_token: { + token_response.extra_fields().id_token().as_ref().map(|t| { + t.to_string() + }) + }, + } + } +} + + +//------------ SessionCache -------------------------------------------------- + +type SessionCache = LoginSessionCache; +type Session = ClientSession; + + //------------ OpenIdConnectAuthProvider ------------------------------------- pub struct OpenIDConnectAuthProvider { config: Arc, - session_cache: Arc, + session_cache: SessionCache, session_key: CryptState, conn: Arc>>, } @@ -170,13 +177,12 @@ pub struct OpenIDConnectAuthProvider { impl OpenIDConnectAuthProvider { pub fn new( config: Arc, - session_cache: Arc, ) -> KrillResult { let session_key = Self::init_session_key(&config)?; Ok(OpenIDConnectAuthProvider { config, - session_cache, + session_cache: SessionCache::new(), session_key, conn: Arc::new(RwLock::new(None)), }) @@ -545,7 +551,7 @@ impl OpenIDConnectAuthProvider { async fn try_revoke_token( &self, - session: &ClientSession, + session: &Session, ) -> Result<(), RevocationErrorResponseType> { // Connect to the OpenID Connect provider OAuth 2.0 token revocation // endpoint to terminate the provider session @@ -554,17 +560,14 @@ impl OpenIDConnectAuthProvider { // and SHOULD support the revocation of access tokens (see // Implementation Note)." let token_to_revoke = if let Some(token) = - session.get_secret(TokenKind::RefreshToken.into()) + session.secrets.refresh_token.as_ref().cloned() { CoreRevocableToken::from(RefreshToken::new(token.clone())) - } else if let Some(token) = - session.get_secret(TokenKind::AccessToken.into()) - { - CoreRevocableToken::from(AccessToken::new(token.clone())) - } else { - return Err(RevocationErrorResponseType::Basic(CoreErrorResponseType::Extension( - "Internal error: Token revocation attempted without a token".to_string(), - ))); + } + else { + CoreRevocableToken::from( + AccessToken::new(session.secrets.access_token.clone()) + ) }; trace!( @@ -634,13 +637,15 @@ impl OpenIDConnectAuthProvider { /// logging and (optionally) retrying. async fn try_refresh_token( &self, - session: &ClientSession, + session: &Session, ) -> Result { - let refresh_token = &session.secrets.get(TokenKind::RefreshToken.into()).ok_or_else(|| { - CoreErrorResponseType::Extension( - "Internal error: Token refresh attempted without a refresh token".to_string(), - ) - })?; + let refresh_token = + &session.secrets.refresh_token.as_ref().ok_or_else(|| { + CoreErrorResponseType::Extension( + "Internal error: Token refresh attempted without \ + a refresh token".to_string(), + ) + })?; debug!( "OpenID Connect: Refreshing token for user: \"{}\"", @@ -665,7 +670,10 @@ impl OpenIDConnectAuthProvider { Ok(token_response) => { let new_token_res = self.session_cache.encode( session.user_id.clone(), - secrets_from_token_response(&token_response), + SessionSecrets::new( + session.secrets.role.clone(), + &token_response + ), &self.session_key, token_response.expires_in(), ); @@ -733,159 +741,6 @@ impl OpenIDConnectAuthProvider { } } - fn extract_claim( - &self, - //_claim_conf: &ConfigAuthOpenIDConnectClaim, - _id_token_claims: &FlexibleIdTokenClaims, - _user_info_claims: Option<&FlexibleUserInfoClaims>, - ) -> KrillResult> { - unimplemented!() - /* - let searchable_claims = match &claim_conf.source { - Some(ClaimSource::ConfigFile) => return Ok(None), - Some(ClaimSource::IdTokenStandardClaim) => { - Some(id_token_claims.to_jmespath()) - } - Some(ClaimSource::IdTokenAdditionalClaim) => { - Some(id_token_claims.additional_claims().to_jmespath()) - } - Some(ClaimSource::UserInfoStandardClaim) - if user_info_claims.is_some() => - { - Some(user_info_claims.unwrap().to_jmespath()) - } - Some(ClaimSource::UserInfoAdditionalClaim) - if user_info_claims.is_some() => - { - Some( - user_info_claims - .unwrap() - .additional_claims() - .to_jmespath(), - ) - } - _ => None, - }; - - // optional because it's not needed when looking up a value in the - // config file instead - let jmespath_string = claim_conf - .jmespath - .as_ref() - .ok_or_else(|| { - OpenIDConnectAuthProvider::internal_error( - "Missing JMESPath configuration value for claim", - None, - ) - })? - .to_string(); - - // Create a new JMESPath Runtime. TODO: Somehow make this a single - // persistent runtime to which API request handling threads (such as - // ours) dispatch search commands to be compiled and executed and - // which can receive results back. Perhaps with a pair of - // channels, one to to send search requests and the other to - // receive search results? - let runtime = jmespathext::init_runtime(); - - // We don't precompile the JMESPath expression because the jmespath - // crate requires it to have a lifetime and storing that in our state - // struct would infect the entire struct with lifetimes, plus logins - // don't happen very often and are slow anyway (as the user has to - // visit the OpenID Connect providers own login form then be - // redirected back to us) so this doesn't have to be fast. - // Note to self: perhaps the lifetime issue could be worked - // around using a Box? - let expr = &runtime.compile(&jmespath_string).map_err(|e| { - OpenIDConnectAuthProvider::internal_error( - format!( - "OpenID Connect: Unable to compile JMESPath expression '{}'", - &jmespath_string - ), - Some(stringify_cause_chain(e)), - ) - })?; - - let claims_to_search = match searchable_claims { - Some(claim) => vec![(claim_conf.source.as_ref().unwrap(), claim)], - None => { - let mut claims = vec![ - ( - &ClaimSource::IdTokenStandardClaim, - id_token_claims.to_jmespath(), - ), - ( - &ClaimSource::IdTokenAdditionalClaim, - id_token_claims.additional_claims().to_jmespath(), - ), - ]; - - if let Some(user_info_claims) = user_info_claims { - claims.extend(vec![ - ( - &ClaimSource::UserInfoStandardClaim, - user_info_claims.to_jmespath(), - ), - ( - &ClaimSource::UserInfoAdditionalClaim, - user_info_claims - .additional_claims() - .to_jmespath(), - ), - ]); - } - - claims - } - }; - - for (source, claims) in claims_to_search.clone() { - let claims = claims.map_err(|e| { - OpenIDConnectAuthProvider::internal_error( - "OpenID Connect: Unable to prepare claims for parsing", - Some(&stringify_cause_chain(e)), - ) - })?; - - debug!("Searching {:?} for \"{}\"..", source, &jmespath_string); - - let result = expr.search(&claims).map_err(|e| { - OpenIDConnectAuthProvider::internal_error( - "OpenID Connect: Error while searching claims", - Some(&stringify_cause_chain(e)), - ) - })?; - debug!("Search result in {:?}: '{:?}'", source, &result); - - // Did the JMESPath search find a match? - if !matches!(*result, jmespath::Variable::Null) { - // Yes. Is it a JMESPath String type? - if let Some(result_str) = result.as_string() { - // Yes. Is it non-empty after trimming leading and - // trailing whitespace? - if !result_str.trim().is_empty() { - // Yes - return Ok(Some(result_str.clone())); - } - } - } - } - - let err_msg_parts = &claims_to_search - .iter() - .map(|(source, claims)| format!("{} {:?}", source, claims)) - .collect::>() - .join(", "); - - debug!( - "Claim \"{}\" not found in {}", - &jmespath_string, err_msg_parts - ); - - Ok(None) - */ - } - fn init_session_key(config: &Config) -> KrillResult { debug!("Initializing session encryption key"); crypt::crypt_init(config) @@ -1248,92 +1103,14 @@ impl OpenIDConnectAuthProvider { Ok(user_info_claims) } - /* - fn resolve_claims( - &self, - claims_conf: HashMap, - user: Option<&ConfigUserDetails>, - id_token_claims: &FlexibleIdTokenClaims, - user_info_claims: Option, - id: &str, - ) -> KrillResult { - let mut attributes: HashMap = HashMap::new(); - for (attr_name, claim_conf) in claims_conf { - if attr_name == "id" { - continue; - } - let attr_value = match &claim_conf.source { - Some(ClaimSource::ConfigFile) if user.is_some() => { - if attr_name == "role" { - Some(user.unwrap().role.clone()) - } - else { - None - } - } - _ => self.extract_claim( - //&claim_conf, - id_token_claims, - user_info_claims.as_ref(), - )?, - }; - - if let Some(attr_value) = attr_value { - // Apply any defined destination mapping for this claim. - // A destination causes the created attribute to have a - // different name than the claim key in the - // configuration. With this we can handle situations - // such as the extracted role value not matching a valid - // role according to policy (by specifying the same - // source claim field multiple times but each time - // using a different JMESPath expression to extract (and - // optionally transform) a different value each time, - // but mapping all of them to the same final attribute, - // e.g. 'role'. A similar case this addresses is where - // different values for an attribute (e.g. 'role') are - // not present in a single claim field but instead may - // be present in one of several claims (e.g. use (part - // of) claim A to check for admins but use (part of) - // claim B to check for readonly users). - let final_attr_name = match claim_conf.dest { - None => attr_name.to_string(), - Some(alt_attr_name) => alt_attr_name.to_string(), - }; - // Only use the first found value - match attributes.entry(final_attr_name.clone()) { - Occupied(found) => { - info!("Skipping found value '{}' for claim '{}' as attribute '{}': attribute already has a value: '{}'", - attr_value, attr_name, final_attr_name, found.get()); - } - Vacant(vacant) => { - debug!( - "Storing found value '{}' for claim '{}' as attribute '{}'", - attr_value, attr_name, final_attr_name - ); - vacant.insert(attr_value); - } - } - } else { - // With Oso policy based configuration the absence of - // claim values isn't necessarily a problem, it's very - // client configuration dependent, but let's mention - // that we didn't find anything just to make it easier - // to spot configuration mistakes via the logs. - info!("No '{}' claim found for user: {}", &attr_name, &id); - } - } - - match attributes.get("role") { - Some(role) => Ok(role.clone()), - None => { - Err(OpenIDConnectAuthProvider::internal_error( - "no role for user".into(), - Some(format!("user ID: {}", id)) - )) - } - } + fn auth_from_session( + &self, session: &Session + ) -> KrillResult { + Ok(AuthInfo::user( + session.user_id.clone(), + self.config.auth_roles.get(&session.secrets.role)? + )) } - */ } impl OpenIDConnectAuthProvider { @@ -1376,17 +1153,14 @@ impl OpenIDConnectAuthProvider { // return match status { SessionStatus::Active => { - return Ok(Some(AuthInfo::user(session.user_id))) + return Ok(Some(self.auth_from_session(&session)?)) } SessionStatus::NeedsRefresh => { // If we have a refresh token try and extend the // session. Otherwise return the cached token // and continue the login session until it expires. - if !session - .secrets - .contains_key(TokenKind::RefreshToken.into()) - { - return Ok(Some(AuthInfo::user(session.user_id))) + if session.secrets.refresh_token.is_none() { + return Ok(Some(self.auth_from_session(&session)?)) } } SessionStatus::Expired => { @@ -1394,10 +1168,7 @@ impl OpenIDConnectAuthProvider { // refresh token. Otherwise, return early // with an error that indicates the user needs to // login again. - if !session - .secrets - .contains_key(TokenKind::RefreshToken.into()) - { + if session.secrets.refresh_token.is_none() { return Err(Error::ApiAuthSessionExpired( "No token to be refreshed".to_string(), )); @@ -1492,7 +1263,9 @@ impl OpenIDConnectAuthProvider { } }; - Ok(Some(AuthInfo::with_new_auth(session.user_id, new_auth))) + let mut auth = self.auth_from_session(&session)?; + auth.set_new_auth(new_auth); + Ok(Some(auth)) } _ => Ok(None), }; @@ -1879,18 +1652,25 @@ impl OpenIDConnectAuthProvider { // configuration without the "id" key :-) // ========================================================================================== - let id = self - .extract_claim( - //id_claim_conf, - id_token_claims, - user_info_claims.as_ref(), - )? - .ok_or_else(|| { - OpenIDConnectAuthProvider::internal_error( - "No value found for 'id' claim", - None, - ) - })?; + let mut claims = Claims::new( + &self.oidc_conf()?.claims, + id_token_claims, user_info_claims + ); + let id = claims.extract_id()?; + let role_name = claims.extract_role()?; + + let role = self.config.auth_roles.get(&role_name)?; + + // Step 4 1/2: Check that the user is allowed to log in. + if !role.is_allowed(Permission::LOGIN, None) { + let reason = format!( + "Login denied for user '{}': \ + User is not permitted to 'LOGIN'", + id, + ); + warn!("{}", reason); + return Err(Error::ApiInsufficientRights(reason)); + } // ========================================================================================== // Step 5: Respond to the user: access granted, or access @@ -1920,7 +1700,7 @@ impl OpenIDConnectAuthProvider { // ========================================================================================== let token = self.session_cache.encode( id.clone().into(), - secrets_from_token_response(&token_response), + SessionSecrets::new(role_name, &token_response), &self.session_key, token_response.expires_in(), )?; @@ -2038,9 +1818,7 @@ impl OpenIDConnectAuthProvider { } => { trace!("OpenID Connect: Directing user to RP-Initiated Logout 1.0 compliant logout endpoint"); - let id_token = session.secrets.get(TokenKind::IdToken.into()); - - self.build_rpinitiated_logout_url(provider_url, post_logout_redirect_url, id_token) + self.build_rpinitiated_logout_url(provider_url, post_logout_redirect_url, session.secrets.id_token.as_ref()) .unwrap_or_else(|err| { OpenIDConnectAuthProvider::internal_error( format!( @@ -2060,35 +1838,19 @@ impl OpenIDConnectAuthProvider { ); Ok(HttpResponse::text_no_cache(go_to_url.into())) } -} - - -//------------ Helper Functions ---------------------------------------------- - -fn secrets_from_token_response( - token_response: &FlexibleTokenResponse, -) -> HashMap { - let mut secrets: HashMap = HashMap::new(); - - secrets.insert( - TokenKind::AccessToken.into(), - token_response.access_token().secret().clone(), - ); - if let Some(refresh_token) = token_response.refresh_token() { - secrets.insert( - TokenKind::RefreshToken.into(), - refresh_token.secret().clone(), - ); - }; - - if let Some(id_token) = token_response.extra_fields().id_token() { - secrets.insert(TokenKind::IdToken.into(), id_token.to_string()); + pub fn sweep(&self) -> KrillResult<()> { + self.session_cache.sweep() } - secrets + pub fn cache_size(&self) -> usize { + self.session_cache.size() + } } + +//------------ Helper Functions ---------------------------------------------- + /* fn with_default_claims( claims: &Option, diff --git a/src/daemon/auth/roles.rs b/src/daemon/auth/roles.rs new file mode 100644 index 000000000..8445ea259 --- /dev/null +++ b/src/daemon/auth/roles.rs @@ -0,0 +1,150 @@ +use std::collections::HashMap; +use std::sync::Arc; +use serde::Deserialize; +use crate::commons::error::ApiAuthError; +use super::{Handle, Permission, PermissionSet}; + + +//------------ Role ---------------------------------------------------------- + +/// The role of actor has. +/// +/// Permissions aren’t assigned to actors directly but rather to roles to +/// which actors are assigned in turn. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct Role { + /// Permissions for requests without specific resources. + none: PermissionSet, + + /// Blanket permission for all resources. + /// + /// This is checked for any resource that isn’t included in + /// the `resources` field. + any: PermissionSet, + + /// Permissions for specific resources. + resources: HashMap, +} + +impl Role { + pub fn admin() -> Self { + Self::simple(PermissionSet::ANY) + } + + pub fn readwrite() -> Self { + Self::simple(PermissionSet::READWRITE) + } + + pub fn readonly() -> Self { + Self::simple(PermissionSet::READONLY) + } + + pub fn testbed() -> Self { + Self::simple(PermissionSet::TESTBED) + } + + pub fn anonymous() -> Self { + Self::simple(PermissionSet::NONE) + } + + pub fn simple(permissions: PermissionSet) -> Self { + Self { + none: permissions, + any: permissions, + resources: Default::default() + } + } + + pub fn with_resources( + permissions: PermissionSet, + resources: impl IntoIterator + ) -> Self { + Self { + none: permissions, + any: PermissionSet::NONE, + resources: resources.into_iter().map(|handle| { + (handle, permissions) + }).collect() + } + } + + pub fn complex( + none: PermissionSet, + any: PermissionSet, + resources: HashMap + ) -> Self { + Self { none, any, resources } + } + + pub fn is_allowed( + &self, + permission: Permission, + resource: Option<&Handle> + ) -> bool { + match resource { + Some(resource) => { + match self.resources.get(resource) { + Some(permissions) => permissions.has(permission), + None => self.any.has(permission), + } + } + None => { + self.none.has(permission) + } + } + } +} + +impl From for Role { + fn from(src: RoleConf) -> Self { + match src.cas { + Some(cas) => Self::with_resources(src.permissions, cas), + None => Self::simple(src.permissions) + } + } +} + + +//------------ RoleConf ------------------------------------------------------ + +/// The role definition used in the config file. +/// +/// This currently only allows creation of a subset of the things that +/// [`Role`] supports. This is on purpose to keep the config format simple. +#[derive(Clone, Debug, Deserialize)] +struct RoleConf { + permissions: PermissionSet, + + cas: Option>, +} + + +//------------ RoleMap ------------------------------------------------------- + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct RoleMap(HashMap>); + +impl RoleMap { + pub fn new() -> Self { + Self::default() + } + + pub fn add( + &mut self, name: impl Into, role: impl Into> + ) { + self.0.insert(name.into(), role.into()); + } + + pub fn contains(&self, name: &str) -> bool { + self.0.contains_key(name) + } + + pub fn get(&self, name: &str) -> Result, ApiAuthError> { + self.0.get(name).cloned().ok_or_else(|| { + ApiAuthError::ApiAuthPermanentError( + "user with undefined role not caught by config check".into() + ) + }) + } +} + diff --git a/src/daemon/ca/manager.rs b/src/daemon/ca/manager.rs index 972877e38..ee8658187 100644 --- a/src/daemon/ca/manager.rs +++ b/src/daemon/ca/manager.rs @@ -48,8 +48,7 @@ use crate::{ CASERVER_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, daemon::{ - auth::Handle, - auth::policy::{AuthPolicy, Permission}, + auth::{AuthInfo, Handle, Permission}, ca::{ CaObjectsStore, CaStatus, CertAuth, CertAuthCommand, CertAuthCommandDetails, DeprecatedRepository, @@ -618,17 +617,17 @@ impl CaManager { /// Gets the CAs that the given policy allows read access to. pub fn ca_list( - &self, auth: &AuthPolicy + &self, auth: &AuthInfo, ) -> KrillResult { Ok(CertAuthList::new( self.ca_store .list()? .into_iter() .filter(|handle| { - auth.is_allowed( + auth.check_permission( Permission::CA_READ, Some(&Handle::from(handle)) - ) + ).is_ok() }) .map(CertAuthSummary::new) .collect(), @@ -2464,7 +2463,7 @@ impl CaManager { /// Schedule synchronizing all CAs with their repositories. pub fn cas_schedule_repo_sync_all( &self, - auth: &AuthPolicy, + auth: &AuthInfo, ) -> KrillResult<()> { for ca in self.ca_list(auth)?.cas() { self.cas_schedule_repo_sync(ca.handle().clone())?; diff --git a/src/daemon/config.rs b/src/daemon/config.rs index 5a7a0fe32..01ee5a108 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -5,6 +5,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, str::FromStr, + sync::Arc, }; use chrono::Duration; @@ -32,6 +33,7 @@ use crate::{ }, constants::*, daemon::{ + auth::{Role, RoleMap}, http::tls_keys::{self, HTTPS_SUB_DIR}, mq::{in_seconds, Priority}, }, @@ -40,7 +42,7 @@ use crate::{ #[cfg(feature = "multi-user")] use crate::daemon::auth::providers::{ - config_file::config::ConfigAuthUsers, + config_file::ConfigAuthUsers, openid_connect::ConfigAuthOpenIDConnect, }; @@ -119,6 +121,14 @@ impl ConfigDefaults { AuthType::AdminToken } + pub fn auth_roles() -> Arc { + let mut res = RoleMap::new(); + res.add("admin", Role::admin()); + res.add("readwrite", Role::readwrite()); + res.add("readonly", Role::readonly()); + res.into() + } + pub fn admin_token() -> Token { match env::var(KRILL_ENV_ADMIN_TOKEN) { Ok(token) => Token::from(token), @@ -568,6 +578,9 @@ pub struct Config { #[cfg(feature = "multi-user")] pub auth_openidconnect: Option, + #[serde(default = "ConfigDefaults::auth_roles")] + pub auth_roles: Arc, + #[serde(default, deserialize_with = "deserialize_signer_ref")] pub default_signer: SignerReference, @@ -1123,6 +1136,7 @@ impl Config { let auth_users = None; #[cfg(feature = "multi-user")] let auth_openidconnect = None; + let auth_roles = ConfigDefaults::auth_roles(); let default_signer = SignerReference::default(); let one_off_signer = SignerReference::default(); @@ -1252,6 +1266,7 @@ impl Config { auth_users, #[cfg(feature = "multi-user")] auth_openidconnect, + auth_roles, default_signer, one_off_signer, signers, diff --git a/src/daemon/http/mod.rs b/src/daemon/http/mod.rs index 15d02de96..36a1b94d5 100644 --- a/src/daemon/http/mod.rs +++ b/src/daemon/http/mod.rs @@ -1,7 +1,6 @@ use std::io; use std::str::FromStr; use std::str::from_utf8; -use std::sync::Arc; use bytes::Bytes; use http_body_util::{BodyExt, Either, Empty, Full, Limited}; use hyper::body::Body; @@ -12,12 +11,11 @@ use rpki::ca::{provisioning, publication}; use serde::Serialize; use serde::de::DeserializeOwned; -use crate::daemon::auth::{AuthInfo, Handle, LoggedInUser}; -use crate::daemon::auth::policy::{AuthPolicy, Permission}; +use crate::daemon::auth::{AuthInfo, Handle, LoggedInUser, Permission}; use crate::{ commons::{ actor::Actor, - error::Error, + error::{ApiAuthError, Error}, KrillResult, }, constants::HTTP_USER_AGENT_TRUNCATE, @@ -343,8 +341,8 @@ impl HttpResponse { Self::response_from_error(Error::ApiInvalidCredentials(reason)) } - pub fn forbidden(reason: String) -> Self { - Self::response_from_error(Error::ApiInsufficientRights(reason)) + pub fn forbidden(err: String) -> Self { + Self::response_from_error(Error::ApiInsufficientRights(err)) } } @@ -355,21 +353,18 @@ pub struct Request { path: RequestPath, state: State, auth: AuthInfo, - auth_policy: Arc, } impl Request { pub async fn new(request: HyperRequest, state: State) -> Self { let path = RequestPath::from_request(&request); let auth = state.authenticate_request(&request).await; - let auth_policy = state.get_auth_policy(&auth.actor); Request { request, path, state, auth, - auth_policy, } } @@ -393,31 +388,31 @@ impl Request { } } - pub async fn upgrade_from_anonymous(&mut self, actor: Actor) { - if self.auth.actor.is_anonymous() { - self.auth.actor = actor.into(); + pub async fn upgrade_from_anonymous(&mut self, auth: AuthInfo) { + if self.auth.actor().is_anonymous() { + self.auth = auth; info!( "Permitted anonymous actor to become actor '{}' \ for the duration of this request", - self.auth.actor.name() + self.auth.actor().name() ); } } - pub fn is_allowed( + pub fn check_permission( &self, permission: Permission, resource: Option<&Handle> - ) -> bool { - self.state.is_allowed(&self.auth.actor, permission, resource) + ) -> Result<(), ApiAuthError> { + self.auth.check_permission(permission, resource) } pub fn actor(&self) -> Actor { - self.auth.actor.clone() + self.auth.actor().clone() } - pub fn auth_policy(&self) -> &AuthPolicy { - &self.auth_policy + pub fn auth_info(&self) -> &AuthInfo { + &self.auth } pub fn auth_info_mut(&mut self) -> &mut AuthInfo { diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index 3f04fc8db..1a43ed35f 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -44,8 +44,7 @@ use crate::{ KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, }, daemon::{ - auth::{Auth, Handle}, - auth::policy::Permission, + auth::{Auth, Handle, Permission}, ca::CaStatus, config::Config, http::{ @@ -350,7 +349,7 @@ async fn map_requests( // Save any updated auth details, e.g. if an OpenID Connect token needed // refreshing. - let new_auth = req.auth_info_mut().new_auth.take(); + let new_auth = req.auth_info_mut().take_new_auth(); // We used to use .or_else() here but that causes a large recursive call // tree due to these calls being to async functions, large enough with the @@ -1209,26 +1208,15 @@ macro_rules! aa { aa!($req, $perm, Some(&$resource), $action, false) }}; ($req:ident, $perm:expr, $resource:expr, $action:expr, $benign:expr) => {{ - if $req.is_allowed($perm, $resource) { - $action - } - else { - let msg = match $resource { - Some(res) => { - format!( - "User '{}' does not have permission '{}' \ - on resource '{}'", - $req.actor().name(), $perm, res, - ) - }, - None => { - format!( - "User '{}' does not have permission '{}'", - $req.actor().name(), $perm, - ) - } - }; - Ok(HttpResponse::forbidden(msg).with_benign($benign)) + match $req.check_permission($perm, $resource) { + Ok(()) => { $action } + Err(err) => { + Ok( + HttpResponse::forbidden( + err.to_string() + ).with_benign($benign) + ) + } } }}; } @@ -1769,7 +1757,7 @@ async fn api_all_ca_issues(req: Request) -> RoutingResult { match *req.method() { Method::GET => aa!(req, Permission::CA_READ, { render_json_res( - req.state().all_ca_issues(req.auth_policy()).await + req.state().all_ca_issues(req.auth_info()).await ) }), _ => render_unknown_method(), @@ -1791,7 +1779,7 @@ async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { async fn api_cas_list(req: Request) -> RoutingResult { aa!(req, Permission::CA_LIST, { - render_json_res(req.state().ca_list(req.auth_policy())) + render_json_res(req.state().ca_list(req.auth_info())) }) } @@ -2531,11 +2519,7 @@ async fn api_republish_all(req: Request, force: bool) -> RoutingResult { async fn api_resync_all(req: Request) -> RoutingResult { match *req.method() { Method::POST => aa!(req, Permission::CA_ADMIN, { - render_empty_res( - req.state().cas_repo_sync_all( - req.auth_policy() - ) - ) + render_empty_res(req.state().cas_repo_sync_all(req.auth_info())) }), _ => render_unknown_method(), } diff --git a/src/daemon/http/testbed.rs b/src/daemon/http/testbed.rs index 0f7f390d1..17eae300b 100644 --- a/src/daemon/http/testbed.rs +++ b/src/daemon/http/testbed.rs @@ -3,8 +3,8 @@ use hyper::Method; use rpki::ca::idexchange::PublisherHandle; use crate::{ - constants::ACTOR_DEF_TESTBED, daemon::{ + auth::AuthInfo, ca::testbed_ca_handle, http::{ server::{ @@ -55,7 +55,7 @@ pub async fn testbed(mut req: Request) -> RoutingResult { // Krill CAs and publishers. Upgrade anonymous users with testbed // rights ready for the next call in the chain to the testbed() // API call handler functions. - req.upgrade_from_anonymous(ACTOR_DEF_TESTBED).await; + req.upgrade_from_anonymous(AuthInfo::testbed()).await; let mut path = req.path().clone(); match path.next() { diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index ac1068d34..6729267e5 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -16,8 +16,7 @@ use rpki::{ uri, }; -use crate::daemon::auth::{AuthInfo, Handle}; -use crate::daemon::auth::policy::{AuthPolicy, Permission}; +use crate::daemon::auth::AuthInfo; use crate::{ commons::{ actor::Actor, @@ -62,7 +61,6 @@ use crate::{ #[cfg(feature = "multi-user")] use crate::daemon::auth::{ - common::session::LoginSessionCache, providers::{ConfigFileAuthProvider, OpenIDConnectAuthProvider}, }; @@ -75,7 +73,7 @@ pub struct KrillServer { service_uri: uri::Https, // Component responsible for API authorization checks - authorizer: Authorizer, + authorizer: Arc, // Publication server, with configured publishers repo_manager: Arc, @@ -92,10 +90,6 @@ pub struct KrillServer { // Time this server was started started: Timestamp, - #[cfg(feature = "multi-user")] - // Global login session cache - login_session_cache: Arc, - // System actor system_actor: Actor, @@ -128,9 +122,6 @@ impl KrillServer { .build()?; let signer = Arc::new(signer); - #[cfg(feature = "multi-user")] - let login_session_cache = Arc::new(LoginSessionCache::new()); - // Construct the authorizer used to verify API access requests and to // tell Lagosta where to send end-users to login and logout. // TODO: remove the ugly duplication, however attempts to do so have @@ -145,22 +136,14 @@ impl KrillServer { #[cfg(feature = "multi-user")] AuthType::ConfigFile => Authorizer::new( config.clone(), - ConfigFileAuthProvider::new( - config.clone(), - login_session_cache.clone(), - )? - .into(), + ConfigFileAuthProvider::new(&config)?.into(), )?, #[cfg(feature = "multi-user")] AuthType::OpenIDConnect => Authorizer::new( config.clone(), - OpenIDConnectAuthProvider::new( - config.clone(), - login_session_cache.clone(), - )? - .into(), + OpenIDConnectAuthProvider::new(config.clone())?.into(), )?, - }; + }.into(); let system_actor = ACTOR_DEF_KRILL; // Task queue Arc is shared between ca_manager, repo_manager and the @@ -207,8 +190,6 @@ impl KrillServer { bgp_analyser, mq, started: Timestamp::now(), - #[cfg(feature = "multi-user")] - login_session_cache, system_actor, config: config.clone(), }; @@ -319,7 +300,7 @@ impl KrillServer { self.repo_manager.clone(), self.bgp_analyser.clone(), #[cfg(feature = "multi-user")] - self.login_session_cache.clone(), + self.authorizer.clone(), self.config.clone(), self.system_actor.clone(), ) @@ -370,20 +351,7 @@ impl KrillServer { #[cfg(feature = "multi-user")] pub fn login_session_cache_size(&self) -> usize { - self.login_session_cache.size() - } - - pub fn get_auth_policy(&self, actor: &Actor) -> Arc { - self.authorizer.get_policy(actor) - } - - pub fn is_allowed( - &self, - actor: &Actor, - permission: Permission, - resource: Option<&Handle> - ) -> bool { - self.authorizer.is_allowed(actor, permission, resource) + self.authorizer.login_session_cache_size() } } @@ -989,7 +957,7 @@ impl KrillServer { pub async fn all_ca_issues( &self, - auth: &AuthPolicy, + auth: &AuthInfo, ) -> KrillResult { let mut all_issues = AllCertAuthIssues::default(); for ca in self.ca_list(auth)?.cas() { @@ -1037,7 +1005,7 @@ impl KrillServer { } /// Re-sync all CAs with their repositories - pub fn cas_repo_sync_all(&self, auth: &AuthPolicy) -> KrillEmptyResult { + pub fn cas_repo_sync_all(&self, auth: &AuthInfo) -> KrillEmptyResult { self.ca_manager.cas_schedule_repo_sync_all(auth) } @@ -1067,7 +1035,7 @@ impl KrillServer { /// # Admin CAS impl KrillServer { - pub fn ca_list(&self, auth: &AuthPolicy) -> KrillResult { + pub fn ca_list(&self, auth: &AuthInfo) -> KrillResult { self.ca_manager.ca_list(auth) } diff --git a/src/daemon/scheduler.rs b/src/daemon/scheduler.rs index 727063525..2a8d1756a 100644 --- a/src/daemon/scheduler.rs +++ b/src/daemon/scheduler.rs @@ -40,7 +40,7 @@ use crate::{ }; #[cfg(feature = "multi-user")] -use crate::daemon::auth::common::session::LoginSessionCache; +use crate::daemon::auth::Authorizer; use super::mq::TaskResult; @@ -51,7 +51,7 @@ pub struct Scheduler { bgp_analyser: Arc, #[cfg(feature = "multi-user")] // Responsible for purging expired cached login tokens - login_session_cache: Arc, + authorizer: Arc, config: Arc, system_actor: Actor, started: Timestamp, @@ -63,9 +63,7 @@ impl Scheduler { ca_manager: Arc, repo_manager: Arc, bgp_analyser: Arc, - #[cfg(feature = "multi-user")] login_session_cache: Arc< - LoginSessionCache, - >, + #[cfg(feature = "multi-user")] authorizer: Arc, config: Arc, system_actor: Actor, ) -> Self { @@ -75,7 +73,7 @@ impl Scheduler { repo_manager, bgp_analyser, #[cfg(feature = "multi-user")] - login_session_cache, + authorizer, config, system_actor, started: Timestamp::now(), @@ -562,7 +560,7 @@ impl Scheduler { #[cfg(feature = "multi-user")] fn sweep_login_cache(&self) -> Result { - if let Err(e) = self.login_session_cache.sweep() { + if let Err(e) = self.authorizer.sweep() { error!( "Background sweep of session decryption cache failed: {}", e diff --git a/src/pubd/manager.rs b/src/pubd/manager.rs index 1444abab2..71513ba55 100644 --- a/src/pubd/manager.rs +++ b/src/pubd/manager.rs @@ -844,7 +844,7 @@ mod tests { let session_after = stats_after.session(); let snapshot_after_session_reset = find_in_session_and_serial_dir( - &data_dir.path(), + data_dir.path(), &session_after, RRDP_FIRST_SERIAL, "snapshot.xml", diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b1008e0de..34f31dd1f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -135,6 +135,7 @@ impl TestConfig { let auth_users = None; #[cfg(feature = "multi-user")] let auth_openidconnect = None; + let auth_roles = ConfigDefaults::auth_roles(); let default_signer = SignerReference::default(); let one_off_signer = SignerReference::default(); @@ -273,6 +274,7 @@ impl TestConfig { auth_users, #[cfg(feature = "multi-user")] auth_openidconnect, + auth_roles, default_signer, one_off_signer, signers, From 8066b6aa2cefe3ab612840445f9ef35701a78b59 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 11:41:56 +0100 Subject: [PATCH 04/24] Restructure the auth_users config. --- src/daemon/auth/providers/config_file.rs | 108 +++++++++++++---------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index eb001720e..fdd835aec 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -36,38 +36,10 @@ impl ConfigFileAuthProvider { pub fn new( config: &Config, ) -> KrillResult { - let auth_users = config.auth_users.as_ref().ok_or_else(|| { + let users = config.auth_users.as_ref().ok_or_else(|| { Error::ConfigError("Missing [auth_users] config section!".into()) - })?; - + })?.clone(); let roles = config.auth_roles.clone(); - let mut users = HashMap::new(); - for (id, details) in auth_users { - let password_hash = details.password_hash.as_ref().ok_or_else(|| { - Error::ConfigError(format!( - "Password hash missing for user '{}'", id - )) - })?.clone(); - let salt = details.salt.as_ref().ok_or_else(|| { - Error::ConfigError(format!( - "Password salt missing for user '{}'", id - )) - })?.clone(); - if !roles.contains(&details.role) { - return Err(Error::ConfigError(format!( - "Undefined role '{}' for user '{}'", details.role, id - ))); - } - users.insert( - id.clone(), - UserDetails { - password_hash: password_hash.into(), - salt, - role: details.role.clone().into(), - } - ); - } - let session_key = Self::init_session_key(config)?; Ok(Self { @@ -299,33 +271,75 @@ impl ConfigFileAuthProvider { //------------ ConfigAuthUsers ----------------------------------------------- -pub type ConfigAuthUsers = HashMap; - - -//------------ ConfigUserDetails --------------------------------------------- - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ConfigUserDetails { - #[serde(default)] - pub role: String, - - // optional so that OpenIDConnectAuthProvider can also use config file - // user defined attributes without requiring a dummy password hash - // and salt - pub password_hash: Option, - - pub salt: Option, +pub type ConfigAuthUsers = HashMap; + + +//------------ LegacyUserDetails --------------------------------------------- + +/// The actual user details type used in the config file. +/// +/// Previous versions of Krill used a concept of user-defined attributes. This +/// has now been simplified to just a singled attribute “role.” In order to +/// allow tranistioning from the old world to the new, we allow the role name +/// to be in an “attributes” hash map or its own field. In the former case, +/// we will accept the config file but warn. We will also accept additional +/// attributes but warn about those, too. +/// +/// However, the password-related fields are now mandatory since we are not +/// using this configuration for the OpenID Connect provider any more. +/// +/// This is all implemented by using the `try_from` Serde container attribute. +#[derive(Clone, Debug, Deserialize)] +struct LegacyUserDetails { + password_hash: String, + salt: String, + role: Option, + attributes: Option>, } //------------ UserDetails --------------------------------------------------- -struct UserDetails { +#[derive(Clone, Debug, Deserialize)] +#[serde(try_from = "LegacyUserDetails")] +pub struct UserDetails { password_hash: Token, salt: String, role: Arc, } +impl TryFrom for UserDetails { + type Error = String; + + fn try_from(src: LegacyUserDetails) -> Result { + let role = if let Some(mut attributes) = src.attributes { + warn!( + "The 'attributes' auth_user field is deprecated. \ + Please use the 'role' field directly." + ); + match attributes.remove("role") { + Some(role) => role, + None => { + return Err("missing 'role' attribute".into()); + } + } + } + else { + match src.role { + Some(role) => role, + None => { + return Err("missing 'role' field".into()); + } + } + }; + Ok(Self { + password_hash: src.password_hash.into(), + salt: src.salt, + role: role.into() + }) + } +} + //------------ SessionSecret et al ------------------------------------------- From 74538ae79d0878ba61a929a2140672366f4bd191 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 14:34:08 +0100 Subject: [PATCH 05/24] Fix casing of permission variants. --- src/daemon/auth/permission.rs | 141 ++++++++++-------- src/daemon/auth/providers/config_file.rs | 4 +- .../auth/providers/openid_connect/provider.rs | 4 +- src/daemon/ca/manager.rs | 2 +- src/daemon/http/server.rs | 128 ++++++++-------- 5 files changed, 146 insertions(+), 133 deletions(-) diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index 7c2eec262..1d4ac9e5e 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -1,20 +1,22 @@ -use std::fmt; +use std::{fmt, str}; use serde::{Deserialize, Serialize}; //------------ Permission ---------------------------------------------------- macro_rules! define_permission { - ( $( $variant:ident, )* ) => { + ( $( ($variant:ident, $text:expr), )* ) => { /// The set of available permissions. /// /// Each API request requires for the actor to have exactly one of these /// permissions. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] - #[allow(non_camel_case_types)] // XXX Fix this #[repr(u32)] pub enum Permission { - $( $variant, )* + $( + #[serde(rename = $text)] + $variant, + )* } impl Permission { @@ -23,12 +25,23 @@ macro_rules! define_permission { } } + impl str::FromStr for Permission { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + $( $text => Ok(Self::$variant), )* + _ => Err("unknown permission") + } + } + } + impl fmt::Display for Permission { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str( match *self { $( - Self::$variant => stringify!($variant), + Self::$variant => ($text), )* } ) @@ -42,29 +55,29 @@ macro_rules! define_permission { } define_permission! { - LOGIN, - PUB_ADMIN, - PUB_LIST, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - CA_LIST, - CA_READ, - CA_CREATE, - CA_UPDATE, - CA_ADMIN, - CA_DELETE, - ROUTES_READ, - ROUTES_UPDATE, - ROUTES_ANALYSIS, - ASPAS_READ, - ASPAS_UPDATE, - ASPAS_ANALYSIS, - BGPSEC_READ, - BGPSEC_UPDATE, - RTA_LIST, - RTA_READ, - RTA_UPDATE, + (Login, "login"), + (PubAdmin, "pub-admin"), + (PubList, "pub-list"), + (PubRead, "pub-read"), + (PubCreate, "pub-create"), + (PubDelete, "pub-delete"), + (CaList, "ca-list"), + (CaRead, "ca-read"), + (CaCreate, "ca-create"), + (CaUpdate, "ca-update"), + (CaAdmin, "ca-admin"), + (CaDelete, "ca-delete"), + (RoutesRead, "routes-read"), + (RoutesUpdate, "routes-update"), + (RoutesAnalysis, "routes-analysis"), + (AspasRead, "aspas-read"), + (AspasUpdate, "aspas-update"), + (AspasAnalysis, "aspas-analyisis"), + (BgpsecRead, "bgpsec-read"), + (BgpsecUpdate, "bgpsec-update"), + (RtaList, "rta-list"), + (RtaRead, "rta-read"), + (RtaUpdate, "rta-update"), } @@ -134,48 +147,48 @@ mod policy { pub const NONE: Self = Self(0); pub const READONLY: Self = Self::from_permissions(&[ - CA_LIST, - CA_READ, - PUB_LIST, - PUB_READ, - ROUTES_READ, - ROUTES_ANALYSIS, - ASPAS_READ, - ASPAS_ANALYSIS, - BGPSEC_READ, - RTA_LIST, - RTA_READ + CaList, + CaRead, + PubList, + PubRead, + RoutesRead, + RoutesAnalysis, + AspasRead, + AspasAnalysis, + BgpsecRead, + RtaList, + RtaRead ]); pub const READWRITE: Self = Self::from_permissions(&[ - CA_LIST, - CA_READ, - CA_CREATE, - CA_UPDATE, - PUB_LIST, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - ROUTES_READ, - ROUTES_ANALYSIS, - ROUTES_UPDATE, - ASPAS_READ, - ASPAS_UPDATE, - ASPAS_ANALYSIS, - BGPSEC_READ, - BGPSEC_UPDATE, - RTA_LIST, - RTA_READ, - RTA_UPDATE + CaList, + CaRead, + CaCreate, + CaUpdate, + PubList, + PubRead, + PubCreate, + PubDelete, + RoutesRead, + RoutesAnalysis, + RoutesUpdate, + AspasRead, + AspasUpdate, + AspasAnalysis, + BgpsecRead, + BgpsecUpdate, + RtaList, + RtaRead, + RtaUpdate ]); pub const TESTBED: Self = Self::from_permissions(&[ - CA_READ, - CA_UPDATE, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - PUB_ADMIN + CaRead, + CaUpdate, + PubRead, + PubCreate, + PubDelete, + PubAdmin ]); } } diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index fdd835aec..8320269a0 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -210,10 +210,10 @@ impl ConfigFileAuthProvider { // Check that the user is allowed to log in. let role = self.roles.get(&user.role)?; - if !role.is_allowed(Permission::LOGIN, None) { + if !role.is_allowed(Permission::Login, None) { let reason = format!( "Login denied for user '{}': \ - User is not permitted to 'LOGIN'", + User is not permitted to 'login'", username, ); warn!("{}", reason); diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index e19113581..d5f776a9d 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -1662,10 +1662,10 @@ impl OpenIDConnectAuthProvider { let role = self.config.auth_roles.get(&role_name)?; // Step 4 1/2: Check that the user is allowed to log in. - if !role.is_allowed(Permission::LOGIN, None) { + if !role.is_allowed(Permission::Login, None) { let reason = format!( "Login denied for user '{}': \ - User is not permitted to 'LOGIN'", + User is not permitted to 'login'", id, ); warn!("{}", reason); diff --git a/src/daemon/ca/manager.rs b/src/daemon/ca/manager.rs index ee8658187..4e9d30e7b 100644 --- a/src/daemon/ca/manager.rs +++ b/src/daemon/ca/manager.rs @@ -625,7 +625,7 @@ impl CaManager { .into_iter() .filter(|handle| { auth.check_permission( - Permission::CA_READ, + Permission::CaRead, Some(&Handle::from(handle)) ).is_ok() }) diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index 1a43ed35f..bdb9b5d7e 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -1234,18 +1234,18 @@ async fn api(req: Request) -> RoutingResult { Some("authorized") => api_authorized(req).await, restricted_endpoint => { // Make sure access is allowed - aa!(req, Permission::LOGIN, { + aa!(req, Permission::Login, { match restricted_endpoint { Some("bulk") => api_bulk(req, &mut path).await, Some("cas") => api_cas(req, &mut path).await, Some("pubd") => aa!( req, - Permission::PUB_ADMIN, + Permission::PubAdmin, api_publication_server(req, &mut path).await ), Some("ta") => aa!( req, - Permission::CA_ADMIN, + Permission::CaAdmin, api_ta(req, &mut path).await ), _ => render_unknown_method(), @@ -1263,7 +1263,7 @@ async fn api_authorized(req: Request) -> RoutingResult { // triggers Lagosta to show a login form, not something to warn about! aa!(no_warn req, - Permission::LOGIN, + Permission::Login, match *req.method() { Method::GET => render_ok(), _ => render_unknown_method(), @@ -1288,7 +1288,7 @@ async fn api_bulk(req: Request, path: &mut RequestPath) -> RoutingResult { async fn api_cas(req: Request, path: &mut RequestPath) -> RoutingResult { match path.path_arg::() { - Some(ca) => aa!(req, Permission::CA_READ, Handle::from(&ca), { + Some(ca) => aa!(req, Permission::CaRead, Handle::from(&ca), { match path.next() { None => match *req.method() { Method::GET => api_ca_info(req, ca).await, @@ -1417,7 +1417,7 @@ async fn api_ca_sync( path: &mut RequestPath, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { if req.is_post() { match path.next() { Some("parents") => { @@ -1514,7 +1514,7 @@ pub async fn api_stale_publishers( req: Request, seconds: Option<&str>, ) -> RoutingResult { - aa!(req, Permission::PUB_LIST, { + aa!(req, Permission::PubList, { let seconds = seconds.unwrap_or(""); match i64::from_str(seconds) { Ok(seconds) => { @@ -1529,7 +1529,7 @@ pub async fn api_stale_publishers( /// Returns a json structure with all publishers in it. pub async fn api_list_pbl(req: Request) -> RoutingResult { - aa!(req, Permission::PUB_LIST, { + aa!(req, Permission::PubList, { render_json_res( req.state() .publishers() @@ -1540,7 +1540,7 @@ pub async fn api_list_pbl(req: Request) -> RoutingResult { /// Adds a publisher pub async fn api_add_pbl(req: Request) -> RoutingResult { - aa!(req, Permission::PUB_CREATE, { + aa!(req, Permission::PubCreate, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1557,7 +1557,7 @@ pub async fn api_remove_pbl( req: Request, publisher: PublisherHandle, ) -> RoutingResult { - aa!(req, Permission::PUB_DELETE, { + aa!(req, Permission::PubDelete, { let actor = req.actor(); render_empty_res(req.state().remove_publisher(publisher, &actor)) }) @@ -1571,7 +1571,7 @@ pub async fn api_show_pbl( ) -> RoutingResult { aa!( req, - Permission::PUB_READ, + Permission::PubRead, render_json_res(req.state().get_publisher(&publisher)) ) } @@ -1584,7 +1584,7 @@ pub async fn api_repository_response_xml( req: Request, publisher: PublisherHandle, ) -> RoutingResult { - aa!(req, Permission::PUB_READ, { + aa!(req, Permission::PubRead, { match repository_response(&req, &publisher).await { Ok(repository_response) => { Ok(HttpResponse::xml(repository_response.to_xml_vec())) @@ -1599,7 +1599,7 @@ pub async fn api_repository_response_json( req: Request, publisher: PublisherHandle, ) -> RoutingResult { - aa!(req, Permission::PUB_READ, { + aa!(req, Permission::PubRead, { match repository_response(&req, &publisher).await { Ok(res) => render_json(res), Err(e) => render_error(e), @@ -1615,7 +1615,7 @@ async fn repository_response( } pub async fn api_ca_add_child(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1632,7 +1632,7 @@ async fn api_ca_child_update( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1649,7 +1649,7 @@ pub async fn api_ca_child_remove( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); render_empty_res( req.state().ca_child_remove(&ca, child, &actor).await, @@ -1664,7 +1664,7 @@ async fn api_ca_child_show( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_child_show(&ca, &child).await) ) @@ -1677,14 +1677,14 @@ async fn api_ca_child_export( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().api_ca_child_export(&ca, &child).await) ) } async fn api_ca_child_import(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CA_ADMIN, Handle::from(&ca), { + aa!(req, Permission::CaAdmin, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1702,7 +1702,7 @@ async fn api_ca_stats_child_connections( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_stats_child_connections(&ca).await) ) @@ -1715,7 +1715,7 @@ async fn api_ca_parent_res_json( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res( req.state().ca_parent_response(&ca, child.clone()).await @@ -1728,7 +1728,7 @@ pub async fn api_ca_parent_res_xml( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CA_READ, Handle::from(&ca), { + aa!(req, Permission::CaRead, Handle::from(&ca), { match req.state().ca_parent_response(&ca, child.clone()).await { Ok(res) => Ok(HttpResponse::xml(res.to_xml_vec())), Err(e) => render_error(e), @@ -1740,7 +1740,7 @@ pub async fn api_ca_parent_res_xml( async fn api_cas_import(req: Request) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_ADMIN, { + Method::POST => aa!(req, Permission::CaAdmin, { let server = req.state().clone(); match req.json().await { Ok(structure) => { @@ -1755,7 +1755,7 @@ async fn api_cas_import(req: Request) -> RoutingResult { async fn api_all_ca_issues(req: Request) -> RoutingResult { match *req.method() { - Method::GET => aa!(req, Permission::CA_READ, { + Method::GET => aa!(req, Permission::CaRead, { render_json_res( req.state().all_ca_issues(req.auth_info()).await ) @@ -1769,7 +1769,7 @@ async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_issues(&ca).await) ), @@ -1778,13 +1778,13 @@ async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { } async fn api_cas_list(req: Request) -> RoutingResult { - aa!(req, Permission::CA_LIST, { + aa!(req, Permission::CaList, { render_json_res(req.state().ca_list(req.auth_info())) }) } pub async fn api_ca_init(req: Request) -> RoutingResult { - aa!(req, Permission::CA_CREATE, { + aa!(req, Permission::CaCreate, { let state = req.state().clone(); match req.json().await { @@ -1800,7 +1800,7 @@ async fn api_ca_id( ca: CaHandle, ) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + Method::POST => aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_update_id(ca, &actor).await) }), @@ -1824,7 +1824,7 @@ async fn api_ca_id( async fn api_ca_info(req: Request, handle: CaHandle) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&handle), render_json_res(req.state().ca_info(&handle).await) ) @@ -1834,7 +1834,7 @@ async fn api_ca_delete(req: Request, handle: CaHandle) -> RoutingResult { let actor = req.actor(); aa!( req, - Permission::CA_DELETE, + Permission::CaDelete, Handle::from(&handle), render_json_res(req.state().ca_delete(&handle, &actor).await) ) @@ -1847,7 +1847,7 @@ async fn api_ca_my_parent_contact( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_my_parent_contact(&ca, &parent).await) ) @@ -1859,7 +1859,7 @@ async fn api_ca_my_parent_statuses( ) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res( req.state() @@ -1930,7 +1930,7 @@ async fn api_ca_bgpsec_definitions_show( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::BGPSEC_READ, Handle::from(&ca), { + aa!(req, Permission::BgpsecRead, Handle::from(&ca), { render_json_res(req.state().ca_bgpsec_definitions_show(ca).await) }) } @@ -1939,7 +1939,7 @@ async fn api_ca_bgpsec_definitions_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::BGPSEC_UPDATE, Handle::from(&ca), { + aa!(req, Permission::BgpsecUpdate, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1990,7 +1990,7 @@ async fn api_ca_history_commands( ) -> RoutingResult { match *req.method() { Method::GET => { - aa!(req, Permission::CA_READ, Handle::from(&handle), { + aa!(req, Permission::CaRead, Handle::from(&handle), { // /api/v1/cas/{ca}/history/commands // //// let mut crit = CommandHistoryCriteria::default(); @@ -2042,7 +2042,7 @@ async fn api_ca_command_details( match path.path_arg() { Some(key) => match *req.method() { Method::GET => { - aa!(req, Permission::CA_READ, Handle::from(&ca), { + aa!(req, Permission::CaRead, Handle::from(&ca), { match req.state().ca_command_details(&ca, key) { Ok(details) => render_json(details), Err(e) => match e { @@ -2064,7 +2064,7 @@ async fn api_ca_child_req_xml(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), match ca_child_req(&req, &ca).await { Ok(child_request) => @@ -2080,7 +2080,7 @@ async fn api_ca_child_req_json(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), match ca_child_req(&req, &ca).await { Ok(req) => render_json(req), @@ -2105,7 +2105,7 @@ async fn api_ca_publisher_req_json( match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_publisher_req(&ca).await) ), @@ -2120,7 +2120,7 @@ async fn api_ca_publisher_req_xml( match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), match req.state().ca_publisher_req(&ca).await { Ok(publisher_request) => @@ -2135,7 +2135,7 @@ async fn api_ca_publisher_req_xml( async fn api_ca_repo_details(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res(req.state().ca_repo_details(&ca).await) ) @@ -2145,7 +2145,7 @@ async fn api_ca_repo_status(req: Request, ca: CaHandle) -> RoutingResult { match *req.method() { Method::GET => aa!( req, - Permission::CA_READ, + Permission::CaRead, Handle::from(&ca), render_json_res( req.state() @@ -2193,7 +2193,7 @@ fn extract_repository_contact( } async fn api_ca_repo_update(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); @@ -2215,7 +2215,7 @@ async fn api_ca_parent_add_or_update( ca: CaHandle, parent_override: Option, ) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); let server = req.state().clone(); @@ -2284,7 +2284,7 @@ async fn api_ca_remove_parent( ca: CaHandle, parent: ParentHandle, ) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); render_empty_res( req.state().ca_parent_remove(ca, parent, &actor).await, @@ -2294,7 +2294,7 @@ async fn api_ca_remove_parent( /// Force a key roll for a CA, i.e. use a max key age of 0 seconds. async fn api_ca_kr_init(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_init(ca, &actor).await) }) @@ -2303,7 +2303,7 @@ async fn api_ca_kr_init(req: Request, ca: CaHandle) -> RoutingResult { /// Force key activation for all new keys, i.e. use a staging period of 0 /// seconds. async fn api_ca_kr_activate(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, Handle::from(&ca), { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_activate(ca, &actor).await) }) @@ -2316,7 +2316,7 @@ async fn api_ca_aspas_definitions_show( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::ASPAS_READ, Handle::from(&ca), { + aa!(req, Permission::AspasRead, Handle::from(&ca), { let state = req.state().clone(); render_json_res(state.ca_aspas_definitions_show(ca).await) }) @@ -2327,7 +2327,7 @@ async fn api_ca_aspas_definitions_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2347,7 +2347,7 @@ async fn api_ca_aspas_update_aspa( ca: CaHandle, customer: Asn, ) -> RoutingResult { - aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2368,7 +2368,7 @@ async fn api_ca_aspas_delete( ca: CaHandle, customer: Asn, ) -> RoutingResult { - aa!(req, Permission::ASPAS_UPDATE, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2381,7 +2381,7 @@ async fn api_ca_aspas_delete( /// Update the route authorizations for this CA async fn api_ca_routes_update(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::ROUTES_UPDATE, Handle::from(&ca), { + aa!(req, Permission::RoutesUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2401,7 +2401,7 @@ async fn api_ca_routes_try_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::ROUTES_UPDATE, Handle::from(&ca), { + aa!(req, Permission::RoutesUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2452,7 +2452,7 @@ async fn api_ca_routes_try_update( /// show the route authorizations for this CA async fn api_ca_routes_show(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::ROUTES_READ, Handle::from(&ca), { + aa!(req, Permission::RoutesRead, Handle::from(&ca), { match req.state().ca_routes_show(&ca).await { Ok(roas) => render_json(roas), Err(_) => render_unknown_resource(), @@ -2466,7 +2466,7 @@ async fn api_ca_routes_analysis( path: &mut RequestPath, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::ROUTES_ANALYSIS, Handle::from(&ca), { + aa!(req, Permission::RoutesAnalysis, Handle::from(&ca), { match path.next() { Some("full") => { render_json_res(req.state().ca_routes_bgp_analysis(&ca).await) @@ -2509,7 +2509,7 @@ async fn api_ca_routes_analysis( async fn api_republish_all(req: Request, force: bool) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_ADMIN, { + Method::POST => aa!(req, Permission::CaAdmin, { render_empty_res(req.state().republish_all(force).await) }), _ => render_unknown_method(), @@ -2518,7 +2518,7 @@ async fn api_republish_all(req: Request, force: bool) -> RoutingResult { async fn api_resync_all(req: Request) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_ADMIN, { + Method::POST => aa!(req, Permission::CaAdmin, { render_empty_res(req.state().cas_repo_sync_all(req.auth_info())) }), _ => render_unknown_method(), @@ -2528,7 +2528,7 @@ async fn api_resync_all(req: Request) -> RoutingResult { /// Refresh all CAs async fn api_refresh_all(req: Request) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_ADMIN, { + Method::POST => aa!(req, Permission::CaAdmin, { render_empty_res(req.state().cas_refresh_all().await) }), _ => render_unknown_method(), @@ -2538,7 +2538,7 @@ async fn api_refresh_all(req: Request) -> RoutingResult { /// Schedule check suspend for all CAs async fn api_suspend_all(req: Request) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CA_ADMIN, { + Method::POST => aa!(req, Permission::CaAdmin, { render_empty_res(req.state().cas_schedule_suspend_all()) }), _ => render_unknown_method(), @@ -2618,7 +2618,7 @@ async fn api_ca_rta( async fn api_ca_rta_list(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, - Permission::RTA_LIST, + Permission::RtaList, Handle::from(&ca), render_json_res(req.state().rta_list(ca).await) ) @@ -2631,7 +2631,7 @@ async fn api_ca_rta_show( ) -> RoutingResult { aa!( req, - Permission::RTA_READ, + Permission::RtaRead, Handle::from(&ca), render_json_res(req.state().rta_show(ca, name).await) ) @@ -2642,7 +2642,7 @@ async fn api_ca_rta_sign( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { @@ -2659,7 +2659,7 @@ async fn api_ca_rta_multi_prep( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); @@ -2677,7 +2677,7 @@ async fn api_ca_rta_multi_sign( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RTA_UPDATE, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, Handle::from(&ca), { let actor = req.actor(); let state = req.state().clone(); match req.json().await { From 48c6bea986f00297f4d7bdf50609bcff26cd776c Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 15:27:19 +0100 Subject: [PATCH 06/24] Clean up names and import paths a bit. --- src/daemon/auth/authorizer.rs | 90 +++++++++---------- src/daemon/auth/providers/admin_token.rs | 8 +- src/daemon/auth/providers/config_file.rs | 8 +- src/daemon/auth/providers/mod.rs | 6 -- .../auth/providers/openid_connect/claims.rs | 1 + .../auth/providers/openid_connect/config.rs | 2 - .../auth/providers/openid_connect/mod.rs | 16 ++-- .../auth/providers/openid_connect/provider.rs | 36 ++++---- src/daemon/krillserver.rs | 32 +------ 9 files changed, 80 insertions(+), 119 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 67ac76df1..43256c986 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -1,33 +1,20 @@ //! Authorization for the API use std::fmt; -use std::any::Any; use std::str::FromStr; use std::sync::Arc; use rpki::ca::idexchange::{InvalidHandle, MyHandle}; use serde::{Deserialize, Serialize}; +use crate::commons::KrillResult; use crate::commons::actor::Actor; +use crate::commons::api::Token; use crate::commons::error::ApiAuthError; - -use crate::{ - commons::{ - api::Token, - KrillResult, - }, - daemon::{ - auth::{ - Permission, Role, - providers::AdminTokenAuthProvider, - }, - config::Config, - http::{HttpResponse, HyperRequest}, - }, -}; - +use crate::daemon::config::{AuthType, Config}; +use crate::daemon::http::{HttpResponse, HyperRequest}; +use super::{Permission, Role}; +use super::providers::admin_token; #[cfg(feature = "multi-user")] -use crate::daemon::auth::providers::{ - ConfigFileAuthProvider, OpenIDConnectAuthProvider, -}; +use super::providers::{config_file, openid_connect}; //------------ AuthProvider -------------------------------------------------- @@ -46,31 +33,31 @@ use crate::daemon::auth::providers::{ /// to login and logout? /// * introspection - who is the currently "logged in" user? pub enum AuthProvider { - Token(AdminTokenAuthProvider), + Token(admin_token::AuthProvider), #[cfg(feature = "multi-user")] - ConfigFile(ConfigFileAuthProvider), + ConfigFile(config_file::AuthProvider), #[cfg(feature = "multi-user")] - OpenIdConnect(OpenIDConnectAuthProvider), + OpenIdConnect(openid_connect::AuthProvider), } -impl From for AuthProvider { - fn from(provider: AdminTokenAuthProvider) -> Self { +impl From for AuthProvider { + fn from(provider: admin_token::AuthProvider) -> Self { AuthProvider::Token(provider) } } #[cfg(feature = "multi-user")] -impl From for AuthProvider { - fn from(provider: ConfigFileAuthProvider) -> Self { +impl From for AuthProvider { + fn from(provider: config_file::AuthProvider) -> Self { AuthProvider::ConfigFile(provider) } } #[cfg(feature = "multi-user")] -impl From for AuthProvider { - fn from(provider: OpenIDConnectAuthProvider) -> Self { +impl From for AuthProvider { + fn from(provider: openid_connect::AuthProvider) -> Self { AuthProvider::OpenIdConnect(provider) } } @@ -165,7 +152,7 @@ impl AuthProvider { /// accessed. pub struct Authorizer { primary_provider: AuthProvider, - legacy_provider: Option, + legacy_provider: Option, } impl Authorizer { @@ -178,29 +165,32 @@ impl Authorizer { /// /// # Legacy support for krillc /// - /// As krillc only supports [AdminTokenAuthProvider] based authentication, - /// if `P` an instance of some other provider, an instance of - /// [AdminTokenAuthProvider] will also be created. This will be used as a - /// fallback when Lagosta is configured to use some other [AuthProvider]. + /// As krillc only supports [admin_token::AuthProvider] + /// based authentication, if `P` an instance of some other provider, an + /// instance of [admin_token::AuthProvider] will also be created. This + /// will be used as a fallback when Lagosta is configured to use some + /// other authentication provider. pub fn new( config: Arc, - primary_provider: AuthProvider, ) -> KrillResult { - let value_any = &primary_provider as &dyn Any; - let is_admin_token_provider = - value_any.downcast_ref::().is_some(); - - let legacy_provider = if is_admin_token_provider { - // the configured provider is the admin token provider so no - // admin token provider is needed for backward compatibility - None - } else { - // the configured provider is not the admin token provider so we - // also need an instance of the admin token provider in order to - // provider backward compatibility for krillc and other API - // clients that only understand the original, legacy, - // admin token based authentication. - Some(AdminTokenAuthProvider::new(config.clone())) + let (primary_provider, legacy_provider) = match config.auth_type { + AuthType::AdminToken => { + (admin_token::AuthProvider::new(config).into(), None) + } + #[cfg(feature = "multi-user")] + AuthType::ConfigFile => { + ( + config_file::AuthProvider::new(&config)?.into(), + Some(admin_token::AuthProvider::new(config)) + ) + } + #[cfg(feature = "multi-user")] + AuthType::OpenIDConnect => { + ( + openid_connect::AuthProvider::new(config.clone())?.into(), + Some(admin_token::AuthProvider::new(config)) + ) + } }; Ok(Authorizer { diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index bcd69578a..ce06871fb 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -16,15 +16,15 @@ use crate::{ // Lagosta could change this path without requiring that we update to match. const LAGOSTA_LOGIN_ROUTE_PATH: &str = "/login"; -pub struct AdminTokenAuthProvider { +pub struct AuthProvider { required_token: Token, user_id: Arc, role: Arc, } -impl AdminTokenAuthProvider { +impl AuthProvider { pub fn new(config: Arc) -> Self { - AdminTokenAuthProvider { + AuthProvider { required_token: config.admin_token.clone(), // XXX Get from config. user_id: "admin".into(), @@ -33,7 +33,7 @@ impl AdminTokenAuthProvider { } } -impl AdminTokenAuthProvider { +impl AuthProvider { pub fn authenticate( &self, request: &HyperRequest, diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index 8320269a0..ab3eac357 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -21,9 +21,9 @@ use crate::daemon::http::{HttpResponse, HyperRequest}; const UI_LOGIN_ROUTE_PATH: &str = "/login?withId=true"; -//------------ ConfigFileAuthProvider ---------------------------------------- +//------------ AuthProvider -------------------------------------------------- -pub struct ConfigFileAuthProvider { +pub struct AuthProvider { users: HashMap, roles: Arc, session_key: crypt::CryptState, @@ -32,7 +32,7 @@ pub struct ConfigFileAuthProvider { fake_salt: String, } -impl ConfigFileAuthProvider { +impl AuthProvider { pub fn new( config: &Config, ) -> KrillResult { @@ -81,7 +81,7 @@ impl ConfigFileAuthProvider { } } -impl ConfigFileAuthProvider { +impl AuthProvider { pub fn authenticate( &self, request: &HyperRequest, diff --git a/src/daemon/auth/providers/mod.rs b/src/daemon/auth/providers/mod.rs index 23cbe4ddd..ef1048882 100644 --- a/src/daemon/auth/providers/mod.rs +++ b/src/daemon/auth/providers/mod.rs @@ -5,9 +5,3 @@ pub mod config_file; #[cfg(feature = "multi-user")] pub mod openid_connect; -pub use admin_token::AdminTokenAuthProvider; - -#[cfg(feature = "multi-user")] -pub use config_file::ConfigFileAuthProvider; -#[cfg(feature = "multi-user")] -pub use openid_connect::provider::OpenIDConnectAuthProvider; diff --git a/src/daemon/auth/providers/openid_connect/claims.rs b/src/daemon/auth/providers/openid_connect/claims.rs index 4913d7ffc..da633d5d9 100644 --- a/src/daemon/auth/providers/openid_connect/claims.rs +++ b/src/daemon/auth/providers/openid_connect/claims.rs @@ -337,6 +337,7 @@ impl<'de> Deserialize<'de> for SubstExpression { //------------ ClaimSource --------------------------------------------------- #[derive(Clone, Copy, Debug)] +#[allow(clippy::enum_variant_names)] pub enum ClaimSource { IdTokenStandardClaim, IdTokenAdditionalClaim, diff --git a/src/daemon/auth/providers/openid_connect/config.rs b/src/daemon/auth/providers/openid_connect/config.rs index 75ea5df8a..fd1a21be8 100644 --- a/src/daemon/auth/providers/openid_connect/config.rs +++ b/src/daemon/auth/providers/openid_connect/config.rs @@ -2,8 +2,6 @@ use std::collections::HashMap; use serde::Deserialize; use super::claims::{ClaimSource, MatchExpression, SubstExpression}; -pub struct ConfigDefaults {} - #[derive(Clone, Debug, Deserialize)] pub struct ConfigAuthOpenIDConnect { pub issuer_url: String, diff --git a/src/daemon/auth/providers/openid_connect/mod.rs b/src/daemon/auth/providers/openid_connect/mod.rs index be28b8c33..c7f40c9fe 100644 --- a/src/daemon/auth/providers/openid_connect/mod.rs +++ b/src/daemon/auth/providers/openid_connect/mod.rs @@ -1,9 +1,13 @@ +//! An authentication provider using OpenID Connect. + +pub use self::config::ConfigAuthOpenIDConnect; +pub use self::provider::AuthProvider; + #[macro_use] -pub mod util; +mod util; -pub mod claims; -pub mod config; -pub mod httpclient; -pub mod provider; +mod claims; +mod config; +mod httpclient; +mod provider; -pub use config::ConfigAuthOpenIDConnect; diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index d5f776a9d..8de1fb408 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -165,22 +165,22 @@ type SessionCache = LoginSessionCache; type Session = ClientSession; -//------------ OpenIdConnectAuthProvider ------------------------------------- +//------------ AuthProvider -------------------------------------------------- -pub struct OpenIDConnectAuthProvider { +pub struct AuthProvider { config: Arc, session_cache: SessionCache, session_key: CryptState, conn: Arc>>, } -impl OpenIDConnectAuthProvider { +impl AuthProvider { pub fn new( config: Arc, ) -> KrillResult { let session_key = Self::init_session_key(&config)?; - Ok(OpenIDConnectAuthProvider { + Ok(Self { config, session_cache: SessionCache::new(), session_key, @@ -866,7 +866,7 @@ impl OpenIDConnectAuthProvider { let conn_guard = self.conn.read().await; conn_guard.as_ref().ok_or_else(|| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "Connection to provider not yet established", None, ) @@ -982,7 +982,7 @@ impl OpenIDConnectAuthProvider { None => cause_chain_str, }; - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( format!("OpenID Connect: Code exchange failed: {}", msg), Some(additional_info), ) @@ -1016,14 +1016,14 @@ impl OpenIDConnectAuthProvider { .extra_fields() .id_token() .ok_or_else(|| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: ID token is missing, does the provider support OpenID Connect?", None, ) })? // happens if the server only supports OAuth2 .claims(&id_token_verifier, &nonce_hash) .map_err(|e| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( format!("OpenID Connect: ID token verification failed: {}", e), Some(stringify_cause_chain(e)), ) @@ -1055,7 +1055,7 @@ impl OpenIDConnectAuthProvider { conn.client .user_info(token_response.access_token().clone(), None) .map_err(|e| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: Provider has no user info endpoint", Some(&stringify_cause_chain(e)), ) @@ -1090,7 +1090,7 @@ impl OpenIDConnectAuthProvider { _ => "Unknown error".to_string(), }; - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( format!("OpenID Connect: UserInfo request failed: {}", msg), Some(stringify_cause_chain(e)), ) @@ -1113,7 +1113,7 @@ impl OpenIDConnectAuthProvider { } } -impl OpenIDConnectAuthProvider { +impl AuthProvider { // Connect Core 1.0 section 3.1.26 Authentication Error Response // OAuth 2.0 RFC-674 4.1.2.1 (Authorization Request Errors) & 5.2 (Access // Token Request Errors) @@ -1132,7 +1132,7 @@ impl OpenIDConnectAuthProvider { trace!("Attempting to authenticate the request.."); self.initialize_connection_if_needed().await.map_err(|err| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: Cannot authenticate request: Failed to connect to provider", Some(&stringify_cause_chain(err)), ) @@ -1303,7 +1303,7 @@ impl OpenIDConnectAuthProvider { // parties" self.initialize_connection_if_needed().await.map_err(|err| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: Cannot get login URL: Failed to connect to provider", Some(&stringify_cause_chain(err)), ) @@ -1489,7 +1489,7 @@ impl OpenIDConnectAuthProvider { cookie_name, cookie_value ); HeaderValue::from_str(&cookie_str).map_err(|err| { - OpenIDConnectAuthProvider::internal_error( + AuthProvider::internal_error( format!( "Unable to construct HTTP cookie '{}' with value '{}'", cookie_name, cookie_value @@ -1519,7 +1519,7 @@ impl OpenIDConnectAuthProvider { request: &HyperRequest, ) -> KrillResult { self.initialize_connection_if_needed().await.map_err(|err| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: Cannot login user: Failed to connect to provider", Some(&stringify_cause_chain(err)), ) @@ -1775,7 +1775,7 @@ impl OpenIDConnectAuthProvider { // there's no point trying to log the token out of the provider if // we know there's a problem with the provider self.initialize_connection_if_needed().await.map_err(|err| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( "OpenID Connect: Cannot logout with provider: Failed to connect to provider", Some(&stringify_cause_chain(err)), ) @@ -1792,7 +1792,7 @@ impl OpenIDConnectAuthProvider { .. } => { if let Err(err) = self.try_revoke_token(&session).await { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( format!( "Error while revoking token for user '{}'", session.user_id @@ -1820,7 +1820,7 @@ impl OpenIDConnectAuthProvider { self.build_rpinitiated_logout_url(provider_url, post_logout_redirect_url, session.secrets.id_token.as_ref()) .unwrap_or_else(|err| { - OpenIDConnectAuthProvider::internal_error( + Self::internal_error( format!( "Error while building OpenID Connect RP-Initiated Logout URL for user '{}'", session.user_id diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index 6729267e5..ffaed10f2 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -42,12 +42,12 @@ use crate::{ }, constants::*, daemon::{ - auth::{providers::AdminTokenAuthProvider, Authorizer, LoggedInUser}, + auth::{Authorizer, LoggedInUser}, ca::{ self, testbed_ca_handle, CaManager, CaStatus, ResourceTaggedAttestation, RtaContentRequest, RtaPrepareRequest, }, - config::{AuthType, Config}, + config::Config, http::{HttpResponse, HyperRequest}, mq::{now, Task, TaskQueue}, scheduler::Scheduler, @@ -59,11 +59,6 @@ use crate::{ }, }; -#[cfg(feature = "multi-user")] -use crate::daemon::auth::{ - providers::{ConfigFileAuthProvider, OpenIDConnectAuthProvider}, -}; - //------------ KrillServer --------------------------------------------------- /// This is the Krill server that is doing all the orchestration for all @@ -122,28 +117,7 @@ impl KrillServer { .build()?; let signer = Arc::new(signer); - // Construct the authorizer used to verify API access requests and to - // tell Lagosta where to send end-users to login and logout. - // TODO: remove the ugly duplication, however attempts to do so have - // so far failed due to incompatible match arm types, or - // unknown size of dyn AuthProvider, or concrete type needs to - // be known in async fn, etc. - let authorizer = match config.auth_type { - AuthType::AdminToken => Authorizer::new( - config.clone(), - AdminTokenAuthProvider::new(config.clone()).into(), - )?, - #[cfg(feature = "multi-user")] - AuthType::ConfigFile => Authorizer::new( - config.clone(), - ConfigFileAuthProvider::new(&config)?.into(), - )?, - #[cfg(feature = "multi-user")] - AuthType::OpenIDConnect => Authorizer::new( - config.clone(), - OpenIDConnectAuthProvider::new(config.clone())?.into(), - )?, - }.into(); + let authorizer = Authorizer::new(config.clone())?.into(); let system_actor = ACTOR_DEF_KRILL; // Task queue Arc is shared between ca_manager, repo_manager and the From 87adc8dd0e9126306d6f931557dd8bb42bc412a1 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 16:52:59 +0100 Subject: [PATCH 07/24] =?UTF-8?q?Remove=20authorizer=E2=80=99s=20Auth=20ty?= =?UTF-8?q?pe.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/daemon/auth/authorizer.rs | 161 +++++++++--------- src/daemon/auth/common/crypt.rs | 22 ++- src/daemon/auth/common/session.rs | 12 +- src/daemon/auth/mod.rs | 2 +- src/daemon/auth/providers/admin_token.rs | 18 +- src/daemon/auth/providers/config_file.rs | 31 ++-- .../auth/providers/openid_connect/provider.rs | 78 ++++----- src/daemon/http/server.rs | 12 +- 8 files changed, 162 insertions(+), 174 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 43256c986..78ef52019 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -16,12 +16,13 @@ use super::providers::admin_token; #[cfg(feature = "multi-user")] use super::providers::{config_file, openid_connect}; + //------------ AuthProvider -------------------------------------------------- /// An AuthProvider authenticates and authorizes a given token. /// /// An AuthProvider is expected to configure itself using the global Krill -/// [`CONFIG`] object. This avoids propagation of potentially many provider +/// from configuration. This avoids propagation of potentially many provider /// specific configuration values from the calling code to the provider /// implementation. /// @@ -32,7 +33,10 @@ use super::providers::{config_file, openid_connect}; /// * discovery - as an interactive client where should I send my users /// to login and logout? /// * introspection - who is the currently "logged in" user? -pub enum AuthProvider { +/// +/// This type is a wrapper around the available backend specific auth +/// providers that can be found in the [super::providers] module. +enum AuthProvider { Token(admin_token::AuthProvider), #[cfg(feature = "multi-user")] @@ -63,10 +67,19 @@ impl From for AuthProvider { } impl AuthProvider { + /// Authenticate a user from information included in an HTTP request. + /// + /// Returns `Ok(None)` to indicate that no authentication information + /// was present in the request and the request should thus be treated + /// as not anonymous. + /// + /// If authentication succeeded, returns the auth info. If it failed, + /// it either returns an auth info created via [`AuthInfo::error`] or + /// just a plain error which the caller needs to convert. pub async fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> Result, ApiAuthError> { match &self { AuthProvider::Token(provider) => provider.authenticate(request), #[cfg(feature = "multi-user")] @@ -80,6 +93,7 @@ impl AuthProvider { } } + /// Returns an HTTP text response with the login URL. pub async fn get_login_url(&self) -> KrillResult { match &self { AuthProvider::Token(provider) => provider.get_login_url(), @@ -92,6 +106,7 @@ impl AuthProvider { } } + /// Establishes a client session from credentials in an HTTP request. pub async fn login( &self, request: &HyperRequest, @@ -107,6 +122,7 @@ impl AuthProvider { } } + /// Returns an HTTP text response with the logout URL. pub async fn logout( &self, request: &HyperRequest, @@ -122,7 +138,10 @@ impl AuthProvider { } } - /// Sweeps out session information. + /// Sweeps out client session information. + /// + /// This method should be called regularly to remove expired sessions + /// from the cache. pub fn sweep(&self) -> KrillResult<()> { match self { AuthProvider::Token(_) => Ok(()), @@ -148,28 +167,23 @@ impl AuthProvider { //------------ Authorizer ---------------------------------------------------- -/// This type is responsible for checking authorizations when the API is -/// accessed. +/// Checks authorizations when the API is accessed. pub struct Authorizer { + /// The auth provider configured by the user. primary_provider: AuthProvider, + + /// A fallback token auth provider when it isn’t the primary provider. + /// + /// This is necessary to support the command line client which only + /// supports admin token authentication. legacy_provider: Option, } impl Authorizer { /// Creates an instance of the Authorizer. /// - /// The given [AuthProvider] will be used to verify API access requests, - /// to handle direct login attempts (if supported) and to determine - /// the URLs to pass on to clients (e.g. Lagosta) that want to know - /// where to direct end-users to login and logout. - /// - /// # Legacy support for krillc - /// - /// As krillc only supports [admin_token::AuthProvider] - /// based authentication, if `P` an instance of some other provider, an - /// instance of [admin_token::AuthProvider] will also be created. This - /// will be used as a fallback when Lagosta is configured to use some - /// other authentication provider. + /// The authorizer will be created according to information provided via + /// `config`. pub fn new( config: Arc, ) -> KrillResult { @@ -200,19 +214,30 @@ impl Authorizer { } /// Authenticates an HTTP request. + /// + /// The method will always return authentication information. + /// + /// If there was no authentiation information in the request, the returned + /// auth info will indicate an anonymous user which will fail all + /// permission checks with “insufficient permissions.” + /// + /// If authentication failed, the returned auth info will also indicate + /// an anonymous user but it will fail permission checks with appropriate + /// error information. pub async fn authenticate_request( &self, request: &HyperRequest ) -> AuthInfo { trace!("Determining actor for request {:?}", &request); - // Try the legacy provider first, if any + // Try the legacy provider first, if any. let authenticate_res = match &self.legacy_provider { Some(provider) => provider.authenticate(request), None => Ok(None), }; // Try the real provider if we did not already successfully - // authenticate + // authenticate. This ignores any possible errors thrown by the + // legacy provider. let authenticate_res = match authenticate_res { Ok(Some(res)) => Ok(Some(res)), _ => self.primary_provider.authenticate(request).await, @@ -235,13 +260,12 @@ impl Authorizer { res } - /// Return the URL at which an end-user should be directed to login with - /// the configured provider. + /// Returns an HTTP text response with the login URL. pub async fn get_login_url(&self) -> KrillResult { self.primary_provider.get_login_url().await } - /// Establish an authenticated session from credentials in an HTTP request. + /// Establishes a client session from credentials in an HTTP request. pub async fn login( &self, request: &HyperRequest ) -> KrillResult { @@ -256,8 +280,7 @@ impl Authorizer { Ok(user) } - /// Return the URL at which an end-user should be directed to logout with - /// the configured provider. + /// Returns an HTTP text response with the logout URL. pub async fn logout( &self, request: &HyperRequest, @@ -266,6 +289,9 @@ impl Authorizer { } /// Sweeps out session information. + /// + /// This method should be called regularly to remove expired sessions + /// from the cache. pub fn sweep(&self) -> KrillResult<()> { self.primary_provider.sweep() } @@ -301,8 +327,8 @@ pub struct AuthInfo { /// The actor for the authenticated user. actor: Actor, - /// Optional authentication information to be included in a response. - new_auth: Option, + /// Optional updated bearer token. + new_token: Option, /// Access permissions. /// @@ -312,49 +338,65 @@ pub struct AuthInfo { } impl AuthInfo { + /// Creates auth info for the given user ID and role. pub fn user( user_id: impl Into>, role: Arc, ) -> Self { Self { actor: Actor::user(user_id), - new_auth: None, + new_token: None, permissions: Ok(role), } } + /// Creates auth info for the testbed actor. pub fn testbed() -> Self { Self::user("testbed", Role::testbed().into()) } + /// Creates auth info for the anonymous actor. + /// + /// This actor fails all permission checks with insufficient permissions. fn anonymous() -> Self { Self { actor: Actor::anonymous(), - new_auth: None, + new_token: None, permissions: Ok(Role::anonymous().into()), } } - fn error(err: impl Into) -> Self { + /// Creates auth info for an authentication failure. + fn error(err: ApiAuthError) -> Self { Self { actor: Actor::anonymous(), - new_auth: None, - permissions: Err(err.into()) + new_token: None, + permissions: Err(err) } } - pub fn set_new_auth(&mut self, new_auth: Auth) { - self.new_auth = Some(new_auth); + /// Sets the updated bearer token. + /// + /// If set, this new token needs to be included in an HTTP response. + pub fn set_new_token(&mut self, new_token: Token) { + self.new_token = Some(new_token); } - pub fn actor(&self) -> &Actor { - &self.actor + /// Takes out an updated bearer token if presnet + pub fn take_new_token(&mut self) -> Option { + self.new_token.take() } - pub fn take_new_auth(&mut self) -> Option { - self.new_auth.take() + /// Returns a reference to the actor. + pub fn actor(&self) -> &Actor { + &self.actor } + /// Checks permissions for an operation. + /// + /// Returns an authentication error if either the request was not + /// authenticated or it was but the authenticated user does not have + /// sufficient permissions. pub fn check_permission( &self, permission: Permission, @@ -374,49 +416,6 @@ impl AuthInfo { } -//------------ Auth ---------------------------------------------------------- - -#[derive(Clone, Debug)] -pub enum Auth { - Bearer(Token), - AuthorizationCode { - code: Token, - state: String, - nonce: String, - csrf_token_hash: String, - }, - UsernameAndPassword { - username: String, - password: String, - }, -} - -impl Auth { - pub fn bearer(token: Token) -> Self { - Auth::Bearer(token) - } - pub fn authorization_code( - code: Token, - state: String, - nonce: String, - csrf_token_hash: String, - ) -> Self { - Auth::AuthorizationCode { - code, - state, - nonce, - csrf_token_hash, - } - } - - pub fn username_and_password_hash( - username: String, - password: String, - ) -> Self { - Auth::UsernameAndPassword { username, password } - } -} - //------------ Handle -------------------------------------------------------- /// Handle for Authorization purposes. diff --git a/src/daemon/auth/common/crypt.rs b/src/daemon/auth/common/crypt.rs index b3bd7a0e5..c640f284e 100644 --- a/src/daemon/auth/common/crypt.rs +++ b/src/daemon/auth/common/crypt.rs @@ -22,13 +22,11 @@ // 4: https://github.com/NLnetLabs/krill/issues/382 use std::sync::atomic::{AtomicU64, Ordering}; - use kvx::{namespace, segment, Key, Namespace, Segment}; - -use crate::{ - commons::{error::Error, util::ext_serde, KrillResult}, - daemon::config::Config, -}; +use crate::commons::KrillResult; +use crate::commons::error::{ApiAuthError, Error}; +use crate::commons::util::ext_serde; +use crate::daemon::config::Config; const CHACHA20_KEY_BIT_LEN: usize = 256; const CHACHA20_KEY_BYTE_LEN: usize = CHACHA20_KEY_BIT_LEN / 8; @@ -145,11 +143,13 @@ pub(crate) fn encrypt( // `payload` should be of the form nonce + tag + cipher text. // Returns the plain text resulting from decryption, or an error. -pub(crate) fn decrypt(key: &[u8], payload: &[u8]) -> KrillResult> { +pub(crate) fn decrypt( + key: &[u8], payload: &[u8] +) -> Result, ApiAuthError> { // TODO: Do we need to get the cipher each time or could we do this just // once? if payload.len() <= CLEARTEXT_PREFIX_LEN { - return Err(Error::Custom( + return Err(ApiAuthError::ApiInvalidCredentials( "Decryption error: Insufficient data".to_string(), )); } @@ -167,7 +167,11 @@ pub(crate) fn decrypt(key: &[u8], payload: &[u8]) -> KrillResult> { cipher_text, tag, ) - .map_err(|err| Error::Custom(format!("Decryption error: {}", &err))) + .map_err(|err| { + ApiAuthError::ApiInvalidCredentials( + format!("Decryption error: {}", &err) + ) + }) } pub(crate) fn crypt_init(config: &Config) -> KrillResult { diff --git a/src/daemon/auth/common/session.rs b/src/daemon/auth/common/session.rs index 5d286290f..dc42753ad 100644 --- a/src/daemon/auth/common/session.rs +++ b/src/daemon/auth/common/session.rs @@ -6,9 +6,9 @@ use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::engine::Engine as _; use serde::{Deserialize, Serialize}; use serde::de::DeserializeOwned; -use crate::commons::api::Token; -use crate::commons::error::Error; use crate::commons::KrillResult; +use crate::commons::api::Token; +use crate::commons::error::{ApiAuthError, Error}; use crate::daemon::auth::common::crypt; use crate::daemon::auth::common::crypt::{CryptState, NonceState}; @@ -88,7 +88,7 @@ struct CachedSession { } pub type EncryptFn = fn(&[u8], &[u8], &NonceState) -> KrillResult>; -pub type DecryptFn = fn(&[u8], &[u8]) -> KrillResult>; +pub type DecryptFn = fn(&[u8], &[u8]) -> Result, ApiAuthError>; /// A short term cache to reduce the impact of session token decryption and /// deserialization (e.g. for multiple requests in a short space of time by @@ -238,7 +238,7 @@ impl LoginSessionCache { token: Token, key: &CryptState, add_to_cache: bool, - ) -> KrillResult> + ) -> Result, ApiAuthError> where S: Clone + DeserializeOwned { if let Some(session) = self.lookup_session(&token) { trace!("Session cache hit for session id {}", &session.user_id); @@ -250,7 +250,7 @@ impl LoginSessionCache { let bytes = BASE64_ENGINE.decode(token.as_ref().as_bytes()).map_err( |err| { debug!("Invalid bearer token: cannot decode: {}", err); - Error::ApiInvalidCredentials( + ApiAuthError::ApiInvalidCredentials( "Invalid bearer token".to_string(), ) }, @@ -265,7 +265,7 @@ impl LoginSessionCache { "Invalid bearer token: cannot deserialize: {}", err ); - Error::ApiInvalidCredentials( + ApiAuthError::ApiInvalidCredentials( "Invalid bearer token".to_string(), ) })?; diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index f8532caff..3bf3a0552 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -4,7 +4,7 @@ pub mod providers; pub mod common; pub use self::authorizer::{ - Auth, AuthInfo, AuthProvider, Authorizer, Handle, LoggedInUser + AuthInfo, Authorizer, Handle, LoggedInUser }; pub use self::permission::{Permission, PermissionSet}; pub use self::roles::{Role, RoleMap}; diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index ce06871fb..89f14f4a8 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -1,13 +1,11 @@ use std::sync::Arc; - +use crate::commons::KrillResult; +use crate::commons::api::Token; +use crate::commons::error::{ApiAuthError, Error}; +use crate::commons::util::httpclient; +use crate::daemon::auth::{AuthInfo, LoggedInUser, Role}; +use crate::daemon::config::Config; use crate::daemon::http::{HttpResponse, HyperRequest}; -use crate::{ - commons::{ - api::Token, error::Error, util::httpclient, - KrillResult, - }, - daemon::{auth::{AuthInfo, LoggedInUser, Role}, config::Config}, -}; // This is NOT an actual relative path to redirect to. Instead it is the path // string of an entry in the Vue router routes table to "route" to (in the @@ -37,7 +35,7 @@ impl AuthProvider { pub fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> Result, ApiAuthError> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } @@ -48,7 +46,7 @@ impl AuthProvider { self.user_id.clone(), self.role.clone() ))) } - Some(_) => Err(Error::ApiInvalidCredentials( + Some(_) => Err(ApiAuthError::ApiInvalidCredentials( "Invalid bearer token".to_string(), )), None => Ok(None), diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index ab3eac357..898ce4413 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -4,11 +4,11 @@ use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::engine::Engine as _; use unicode_normalization::UnicodeNormalization; use crate::commons::KrillResult; -use crate::commons::util::httpclient; use crate::commons::api::Token; use crate::commons::error::{ApiAuthError, Error}; +use crate::commons::util::httpclient; use crate::constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}; -use crate::daemon::auth::{Auth, AuthInfo, LoggedInUser, Permission, RoleMap}; +use crate::daemon::auth::{AuthInfo, LoggedInUser, Permission, RoleMap}; use crate::daemon::auth::common::crypt; use crate::daemon::auth::common::session::{ClientSession, LoginSessionCache}; use crate::daemon::config::Config; @@ -66,7 +66,7 @@ impl AuthProvider { let auth = String::from_utf8(auth).ok()?; let (username, password) = auth.split_once(':')?; - Some(Auth::UsernameAndPassword { + Some(Auth { username: username.to_string(), password: password.to_string(), }) @@ -85,7 +85,7 @@ impl AuthProvider { pub fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> Result, ApiAuthError> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); } @@ -122,12 +122,10 @@ impl AuthProvider { pub fn login(&self, request: &HyperRequest) -> KrillResult { use scrypt::scrypt; - let (username, password) = match self.get_auth(request) { - Some(Auth::UsernameAndPassword { username, password }) => { - (username, password) - } - _ => { - trace!("Missing pr incomplete credentials for login attempt"); + let auth = match self.get_auth(request) { + Some(auth) => auth, + None => { + trace!("Missing or incomplete credentials for login attempt"); return Err(Error::ApiInvalidCredentials( "Missing credentials".to_string(), )) @@ -139,7 +137,7 @@ impl AuthProvider { // compared to the known user path and timing differences can aid // attackers. let (user_password_hash, user_salt) = - match self.users.get(&username) { + match self.users.get(&auth.username) { Some(user) => { (user.password_hash.to_string(), user.salt.clone()) } @@ -149,8 +147,8 @@ impl AuthProvider { ), }; - let username = username.trim().nfkc().collect::(); - let password = password.trim().nfkc().collect::(); + let username = auth.username.trim().nfkc().collect::(); + let password = auth.password.trim().nfkc().collect::(); // hash twice with two different salts // legacy hashing strategy to be compatible with lagosta @@ -351,3 +349,10 @@ struct SessionSecret { type SessionCache = LoginSessionCache; type Session = ClientSession; + +//------------ Auth ---------------------------------------------------------- + +struct Auth { + username: String, + password: String, +} diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index 8de1fb408..672a22e59 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -58,7 +58,7 @@ use crate::daemon::http::{HttpResponse, HyperRequest}; use crate::{ commons::{ api::Token, - error::Error, + error::{ApiAuthError, Error}, util::{httpclient, sha256}, KrillResult, }, @@ -76,7 +76,7 @@ use crate::{ WantedMeta, }, }, - Auth, AuthInfo, LoggedInUser, Permission, + AuthInfo, LoggedInUser, Permission, }, config::Config, http::auth::{url_encode, AUTH_CALLBACK_ENDPOINT}, @@ -638,7 +638,7 @@ impl AuthProvider { async fn try_refresh_token( &self, session: &Session, - ) -> Result { + ) -> Result { let refresh_token = &session.secrets.refresh_token.as_ref().ok_or_else(|| { CoreErrorResponseType::Extension( @@ -682,7 +682,7 @@ impl AuthProvider { Ok(new_token) => { // The new token was successfully acquired from the OpenID Connect Provider, // and early returned. - Ok(Auth::Bearer(new_token)) + Ok(new_token) } Err(err) => Err(CoreErrorResponseType::Extension(format!( "Internal error: Error while encoding the refreshed token {}", @@ -838,12 +838,12 @@ impl AuthProvider { self.extract_cookie(request, CSRF_COOKIE_NAME) { trace!("OpenID Connect: Detected RFC-6749 section 4.1.2 redirected Authorization Response"); - return Some(Auth::authorization_code( - Token::from(code), + return Some(Auth { + code: Token::from(code), state, nonce, csrf_token_hash, - )); + }); } else { debug!("OpenID Connect: Ignoring potential RFC-6749 section 4.1.2 redirected Authorization Response due to missing CSRF token hash cookie."); } @@ -1128,7 +1128,7 @@ impl AuthProvider { pub async fn authenticate( &self, request: &HyperRequest, - ) -> KrillResult> { + ) -> Result, ApiAuthError> { trace!("Attempting to authenticate the request.."); self.initialize_connection_if_needed().await.map_err(|err| { @@ -1169,7 +1169,7 @@ impl AuthProvider { // with an error that indicates the user needs to // login again. if session.secrets.refresh_token.is_none() { - return Err(Error::ApiAuthSessionExpired( + return Err(ApiAuthError::ApiAuthSessionExpired( "No token to be refreshed".to_string(), )); } @@ -1178,13 +1178,13 @@ impl AuthProvider { // Token needs refresh and we have a refresh token, try to // refresh - let new_auth = match self.try_refresh_token(&session).await { - Ok(auth) => { + let new_token = match self.try_refresh_token(&session).await { + Ok(token) => { trace!( "OpenID Connect: Successfully refreshed token for user \"{}\"", session.user_id ); - auth + token } Err(err) => { trace!("OpenID Connect: RFC 6749 5.2 Error response returned..."); @@ -1202,7 +1202,7 @@ impl AuthProvider { "OpenID Connect: invalid_grant {:?}", err ); - return Err(Error::ApiInvalidCredentials( + return Err(ApiAuthError::ApiInvalidCredentials( "Unable to extend login session: your session has been terminated.".to_string(), )); } @@ -1212,7 +1212,7 @@ impl AuthProvider { "OpenID Connect: RFC 6749 5.2 {:?}", err ); - return Err(Error::ApiAuthPermanentError( + return Err(ApiAuthError::ApiAuthPermanentError( "Unable to extend login session: the provider rejected the request.".to_string(), )); } @@ -1228,7 +1228,7 @@ impl AuthProvider { "OpenID Connect: RFC 6749 5.2 {:?}", err ); - return Err(Error::ApiInsufficientRights( + return Err(ApiAuthError::ApiInsufficientRights( "Unable to extend login session: the authorization was revoked for this user, client or action.".to_string(), )); } @@ -1247,13 +1247,13 @@ impl AuthProvider { "temporarily_unavailable" | "server_error" => { warn!("OpenID Connect: RFC 6749 5.2 {:?}", err); - return Err(Error::ApiAuthTransientError( + return Err(ApiAuthError::ApiAuthTransientError( "Unable to extend login session: could not contact the provider".to_string(), )); } _ => { warn!("OpenID Connect: RFC 6749 5.2 unknown error {:?}", err); - return Err(Error::ApiAuthTransientError( + return Err(ApiAuthError::ApiAuthTransientError( "Unable to extend login session: unknown error".to_string(), )); } @@ -1264,7 +1264,7 @@ impl AuthProvider { }; let mut auth = self.auth_from_session(&session)?; - auth.set_new_auth(new_auth); + auth.set_new_token(new_token); Ok(Some(auth)) } _ => Ok(None), @@ -1529,7 +1529,7 @@ impl AuthProvider { // OpenID Connect Authorization Code Flow // See: https://tools.ietf.org/html/rfc6749#section-4.1 // https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps - Some(Auth::AuthorizationCode { + Some(Auth { code, state, nonce, @@ -1708,7 +1708,7 @@ impl AuthProvider { Ok(LoggedInUser { token, id, }) } - _ => Err(Error::ApiInvalidCredentials( + None => Err(Error::ApiInvalidCredentials( "Request is not RFC-6749 section 4.1.2 compliant".to_string(), )), } @@ -1849,36 +1849,18 @@ impl AuthProvider { } -//------------ Helper Functions ---------------------------------------------- +//------------ Auth ---------------------------------------------------------- -/* -fn with_default_claims( - claims: &Option, -) -> ConfigAuthOpenIDConnectClaims { - let mut claims = match claims { - Some(claims) => claims.clone(), - None => ConfigAuthOpenIDConnectClaims::new(), - }; - - claims - .entry("id".into()) - .or_insert(ConfigAuthOpenIDConnectClaim { - source: None, - jmespath: Some("email".to_string()), - dest: None, - }); - - claims - .entry("role".into()) - .or_insert(ConfigAuthOpenIDConnectClaim { - source: None, - jmespath: Some("role".to_string()), - dest: None, - }); - - claims +#[derive(Clone, Debug)] +struct Auth { + code: Token, + state: String, + nonce: String, + csrf_token_hash: String, } -*/ + + +//------------ Helper Functions ---------------------------------------------- // Based on: https://github.com/ramosbugs/openidconnect-rs/blob/main/examples/google.rs#L38 pub fn stringify_cause_chain(fail: F) -> String { diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index bdb9b5d7e..ecf59e0f3 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -44,7 +44,7 @@ use crate::{ KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, }, daemon::{ - auth::{Auth, Handle, Permission}, + auth::{Handle, Permission}, ca::CaStatus, config::Config, http::{ @@ -349,7 +349,7 @@ async fn map_requests( // Save any updated auth details, e.g. if an OpenID Connect token needed // refreshing. - let new_auth = req.auth_info_mut().take_new_auth(); + let new_token = req.auth_info_mut().take_new_token(); // We used to use .or_else() here but that causes a large recursive call // tree due to these calls being to async functions, large enough with the @@ -401,7 +401,7 @@ async fn map_requests( // Augment the response with any updated auth details that were determined // above. - let res = add_new_auth_to_response(res, new_auth); + let res = add_new_token_to_response(res, new_token); // Log the request and the response. logger.end(res.as_ref()); @@ -1170,11 +1170,11 @@ fn add_authorization_headers_to_response( } } -fn add_new_auth_to_response( +fn add_new_token_to_response( res: Result, - opt_auth: Option, + opt_token: Option, ) -> Result { - if let Some(Auth::Bearer(token)) = opt_auth { + if let Some(token) = opt_token { res.map(|ok_res| add_authorization_headers_to_response(ok_res, token)) } else { res From 65114b1ec499a441ac9e09d4712a708f0607a8a3 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 17:12:57 +0100 Subject: [PATCH 08/24] =?UTF-8?q?Remove=20auth=E2=80=99s=20Handle=20wrappe?= =?UTF-8?q?r.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commons/error.rs | 8 ++- src/daemon/auth/authorizer.rs | 10 ++-- src/daemon/auth/mod.rs | 4 +- src/daemon/auth/roles.rs | 13 +++-- src/daemon/ca/manager.rs | 5 +- src/daemon/http/mod.rs | 5 +- src/daemon/http/server.rs | 100 +++++++++++++++++----------------- 7 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/commons/error.rs b/src/commons/error.rs index 87e791caa..901521e66 100644 --- a/src/commons/error.rs +++ b/src/commons/error.rs @@ -6,7 +6,9 @@ use hyper::StatusCode; use rpki::{ ca::{ - idexchange::{CaHandle, ChildHandle, ParentHandle, PublisherHandle}, + idexchange::{ + CaHandle, ChildHandle, MyHandle, ParentHandle, PublisherHandle + }, provisioning, provisioning::ResourceClassName, publication, @@ -28,7 +30,7 @@ use crate::{ util::httpclient, }, daemon::{ca::RoaPayloadJsonMapKey, http::tls_keys}, - daemon::auth::{Handle, Permission}, + daemon::auth::Permission, ta, upgrades::UpgradeError, }; @@ -131,7 +133,7 @@ pub enum ApiAuthError { impl ApiAuthError { pub fn insufficient_rights( - actor: &Actor, perm: Permission, resource: Option<&Handle> + actor: &Actor, perm: Permission, resource: Option<&MyHandle> ) -> Self { Self::ApiInsufficientRights( match resource { diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 78ef52019..46dd509b4 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -1,10 +1,8 @@ //! Authorization for the API -use std::fmt; -use std::str::FromStr; use std::sync::Arc; -use rpki::ca::idexchange::{InvalidHandle, MyHandle}; -use serde::{Deserialize, Serialize}; +use rpki::ca::idexchange::MyHandle; +use serde::Serialize; use crate::commons::KrillResult; use crate::commons::actor::Actor; use crate::commons::api::Token; @@ -400,7 +398,7 @@ impl AuthInfo { pub fn check_permission( &self, permission: Permission, - resource: Option<&Handle> + resource: Option<&MyHandle> ) -> Result<(), ApiAuthError> { if self.permissions.as_ref().map_err(Clone::clone)? .is_allowed(permission, resource) @@ -415,6 +413,7 @@ impl AuthInfo { } } +/* //------------ Handle -------------------------------------------------------- @@ -452,3 +451,4 @@ impl AsRef for Handle { } } +*/ diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index 3bf3a0552..7ed5b6661 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -3,9 +3,7 @@ pub mod providers; pub mod common; -pub use self::authorizer::{ - AuthInfo, Authorizer, Handle, LoggedInUser -}; +pub use self::authorizer::{AuthInfo, Authorizer, LoggedInUser}; pub use self::permission::{Permission, PermissionSet}; pub use self::roles::{Role, RoleMap}; diff --git a/src/daemon/auth/roles.rs b/src/daemon/auth/roles.rs index 8445ea259..c99b7e33f 100644 --- a/src/daemon/auth/roles.rs +++ b/src/daemon/auth/roles.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use std::sync::Arc; +use rpki::ca::idexchange::MyHandle; use serde::Deserialize; use crate::commons::error::ApiAuthError; -use super::{Handle, Permission, PermissionSet}; +use super::{Permission, PermissionSet}; //------------ Role ---------------------------------------------------------- @@ -23,7 +24,7 @@ pub struct Role { any: PermissionSet, /// Permissions for specific resources. - resources: HashMap, + resources: HashMap, } impl Role { @@ -57,7 +58,7 @@ impl Role { pub fn with_resources( permissions: PermissionSet, - resources: impl IntoIterator + resources: impl IntoIterator ) -> Self { Self { none: permissions, @@ -71,7 +72,7 @@ impl Role { pub fn complex( none: PermissionSet, any: PermissionSet, - resources: HashMap + resources: HashMap ) -> Self { Self { none, any, resources } } @@ -79,7 +80,7 @@ impl Role { pub fn is_allowed( &self, permission: Permission, - resource: Option<&Handle> + resource: Option<&MyHandle> ) -> bool { match resource { Some(resource) => { @@ -115,7 +116,7 @@ impl From for Role { struct RoleConf { permissions: PermissionSet, - cas: Option>, + cas: Option>, } diff --git a/src/daemon/ca/manager.rs b/src/daemon/ca/manager.rs index 4e9d30e7b..b22ba1cdf 100644 --- a/src/daemon/ca/manager.rs +++ b/src/daemon/ca/manager.rs @@ -48,7 +48,7 @@ use crate::{ CASERVER_NS, STATUS_NS, TA_PROXY_SERVER_NS, TA_SIGNER_SERVER_NS, }, daemon::{ - auth::{AuthInfo, Handle, Permission}, + auth::{AuthInfo, Permission}, ca::{ CaObjectsStore, CaStatus, CertAuth, CertAuthCommand, CertAuthCommandDetails, DeprecatedRepository, @@ -625,8 +625,7 @@ impl CaManager { .into_iter() .filter(|handle| { auth.check_permission( - Permission::CaRead, - Some(&Handle::from(handle)) + Permission::CaRead, Some(handle) ).is_ok() }) .map(CertAuthSummary::new) diff --git a/src/daemon/http/mod.rs b/src/daemon/http/mod.rs index 36a1b94d5..a910c199a 100644 --- a/src/daemon/http/mod.rs +++ b/src/daemon/http/mod.rs @@ -8,10 +8,11 @@ use hyper::header::USER_AGENT; use hyper::http::uri::PathAndQuery; use hyper::{HeaderMap, Method, StatusCode}; use rpki::ca::{provisioning, publication}; +use rpki::ca::idexchange::MyHandle; use serde::Serialize; use serde::de::DeserializeOwned; -use crate::daemon::auth::{AuthInfo, Handle, LoggedInUser, Permission}; +use crate::daemon::auth::{AuthInfo, LoggedInUser, Permission}; use crate::{ commons::{ actor::Actor, @@ -402,7 +403,7 @@ impl Request { pub fn check_permission( &self, permission: Permission, - resource: Option<&Handle> + resource: Option<&MyHandle> ) -> Result<(), ApiAuthError> { self.auth.check_permission(permission, resource) } diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index ecf59e0f3..6dfb088d0 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -18,7 +18,7 @@ use hyper::Method; use hyper_util::rt::{TokioExecutor, TokioIo}; use rpki::ca::idexchange; use rpki::ca::idexchange::{ - CaHandle, ChildHandle, ParentHandle, PublisherHandle, + CaHandle, ChildHandle, MyHandle, ParentHandle, PublisherHandle, }; use rpki::repository::resources::Asn; use serde::Serialize; @@ -44,7 +44,7 @@ use crate::{ KRILL_VERSION_MINOR, KRILL_VERSION_PATCH, }, daemon::{ - auth::{Handle, Permission}, + auth::Permission, ca::CaStatus, config::Config, http::{ @@ -1021,7 +1021,7 @@ pub async fn metrics(req: Request) -> RoutingResult { //------------ Publication --------------------------------------------------- -/// Handle RFC8181 queries and return the appropriate response. +/// MyHandle RFC8181 queries and return the appropriate response. pub async fn rfc8181(req: Request) -> RoutingResult { if req.path().segment() == "rfc8181" { let mut path = req.path().clone(); @@ -1196,10 +1196,10 @@ fn add_new_token_to_response( // similar to how this macro is used in each function. macro_rules! aa { (no_warn $req:ident, $perm:expr, $action:expr) => {{ - aa!($req, $perm, Option::<&Handle>::None, $action, true) + aa!($req, $perm, Option::<&MyHandle>::None, $action, true) }}; ($req:ident, $perm:expr, $action:expr) => {{ - aa!($req, $perm, Option::<&Handle>::None, $action, false) + aa!($req, $perm, Option::<&MyHandle>::None, $action, false) }}; (no_warn $req:ident, $perm:expr, $resource:expr, $action:expr) => {{ aa!($req, $perm, Some(&$resource), $action, true) @@ -1288,7 +1288,7 @@ async fn api_bulk(req: Request, path: &mut RequestPath) -> RoutingResult { async fn api_cas(req: Request, path: &mut RequestPath) -> RoutingResult { match path.path_arg::() { - Some(ca) => aa!(req, Permission::CaRead, Handle::from(&ca), { + Some(ca) => aa!(req, Permission::CaRead, ca, { match path.next() { None => match *req.method() { Method::GET => api_ca_info(req, ca).await, @@ -1417,7 +1417,7 @@ async fn api_ca_sync( path: &mut RequestPath, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { if req.is_post() { match path.next() { Some("parents") => { @@ -1615,7 +1615,7 @@ async fn repository_response( } pub async fn api_ca_add_child(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1632,7 +1632,7 @@ async fn api_ca_child_update( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1649,7 +1649,7 @@ pub async fn api_ca_child_remove( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); render_empty_res( req.state().ca_child_remove(&ca, child, &actor).await, @@ -1665,7 +1665,7 @@ async fn api_ca_child_show( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_child_show(&ca, &child).await) ) } @@ -1678,13 +1678,13 @@ async fn api_ca_child_export( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().api_ca_child_export(&ca, &child).await) ) } async fn api_ca_child_import(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CaAdmin, Handle::from(&ca), { + aa!(req, Permission::CaAdmin, ca, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1703,7 +1703,7 @@ async fn api_ca_stats_child_connections( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_stats_child_connections(&ca).await) ) } @@ -1716,7 +1716,7 @@ async fn api_ca_parent_res_json( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res( req.state().ca_parent_response(&ca, child.clone()).await ) @@ -1728,7 +1728,7 @@ pub async fn api_ca_parent_res_xml( ca: CaHandle, child: ChildHandle, ) -> RoutingResult { - aa!(req, Permission::CaRead, Handle::from(&ca), { + aa!(req, Permission::CaRead, ca, { match req.state().ca_parent_response(&ca, child.clone()).await { Ok(res) => Ok(HttpResponse::xml(res.to_xml_vec())), Err(e) => render_error(e), @@ -1770,7 +1770,7 @@ async fn api_ca_issues(req: Request, ca: CaHandle) -> RoutingResult { Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_issues(&ca).await) ), _ => render_unknown_method(), @@ -1800,7 +1800,7 @@ async fn api_ca_id( ca: CaHandle, ) -> RoutingResult { match *req.method() { - Method::POST => aa!(req, Permission::CaUpdate, Handle::from(&ca), { + Method::POST => aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); render_empty_res(req.state().ca_update_id(ca, &actor).await) }), @@ -1825,7 +1825,7 @@ async fn api_ca_info(req: Request, handle: CaHandle) -> RoutingResult { aa!( req, Permission::CaRead, - Handle::from(&handle), + handle, render_json_res(req.state().ca_info(&handle).await) ) } @@ -1835,7 +1835,7 @@ async fn api_ca_delete(req: Request, handle: CaHandle) -> RoutingResult { aa!( req, Permission::CaDelete, - Handle::from(&handle), + handle, render_json_res(req.state().ca_delete(&handle, &actor).await) ) } @@ -1848,7 +1848,7 @@ async fn api_ca_my_parent_contact( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_my_parent_contact(&ca, &parent).await) ) } @@ -1860,7 +1860,7 @@ async fn api_ca_my_parent_statuses( aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res( req.state() .ca_status(&ca) @@ -1930,7 +1930,7 @@ async fn api_ca_bgpsec_definitions_show( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::BgpsecRead, Handle::from(&ca), { + aa!(req, Permission::BgpsecRead, ca, { render_json_res(req.state().ca_bgpsec_definitions_show(ca).await) }) } @@ -1939,7 +1939,7 @@ async fn api_ca_bgpsec_definitions_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::BgpsecUpdate, Handle::from(&ca), { + aa!(req, Permission::BgpsecUpdate, ca, { let actor = req.actor(); let server = req.state().clone(); match req.json().await { @@ -1990,7 +1990,7 @@ async fn api_ca_history_commands( ) -> RoutingResult { match *req.method() { Method::GET => { - aa!(req, Permission::CaRead, Handle::from(&handle), { + aa!(req, Permission::CaRead, handle, { // /api/v1/cas/{ca}/history/commands // //// let mut crit = CommandHistoryCriteria::default(); @@ -2042,7 +2042,7 @@ async fn api_ca_command_details( match path.path_arg() { Some(key) => match *req.method() { Method::GET => { - aa!(req, Permission::CaRead, Handle::from(&ca), { + aa!(req, Permission::CaRead, ca, { match req.state().ca_command_details(&ca, key) { Ok(details) => render_json(details), Err(e) => match e { @@ -2065,7 +2065,7 @@ async fn api_ca_child_req_xml(req: Request, ca: CaHandle) -> RoutingResult { Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, match ca_child_req(&req, &ca).await { Ok(child_request) => Ok(HttpResponse::xml(child_request.to_xml_vec())), @@ -2081,7 +2081,7 @@ async fn api_ca_child_req_json(req: Request, ca: CaHandle) -> RoutingResult { Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, match ca_child_req(&req, &ca).await { Ok(req) => render_json(req), Err(e) => render_error(e), @@ -2106,7 +2106,7 @@ async fn api_ca_publisher_req_json( Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_publisher_req(&ca).await) ), _ => render_unknown_method(), @@ -2121,7 +2121,7 @@ async fn api_ca_publisher_req_xml( Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, match req.state().ca_publisher_req(&ca).await { Ok(publisher_request) => Ok(HttpResponse::xml(publisher_request.to_xml_vec())), @@ -2136,7 +2136,7 @@ async fn api_ca_repo_details(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res(req.state().ca_repo_details(&ca).await) ) } @@ -2146,7 +2146,7 @@ async fn api_ca_repo_status(req: Request, ca: CaHandle) -> RoutingResult { Method::GET => aa!( req, Permission::CaRead, - Handle::from(&ca), + ca, render_json_res( req.state() .ca_status(&ca) @@ -2193,7 +2193,7 @@ fn extract_repository_contact( } async fn api_ca_repo_update(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); let server = req.state().clone(); @@ -2215,7 +2215,7 @@ async fn api_ca_parent_add_or_update( ca: CaHandle, parent_override: Option, ) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); let server = req.state().clone(); @@ -2284,7 +2284,7 @@ async fn api_ca_remove_parent( ca: CaHandle, parent: ParentHandle, ) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); render_empty_res( req.state().ca_parent_remove(ca, parent, &actor).await, @@ -2294,7 +2294,7 @@ async fn api_ca_remove_parent( /// Force a key roll for a CA, i.e. use a max key age of 0 seconds. async fn api_ca_kr_init(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_init(ca, &actor).await) }) @@ -2303,7 +2303,7 @@ async fn api_ca_kr_init(req: Request, ca: CaHandle) -> RoutingResult { /// Force key activation for all new keys, i.e. use a staging period of 0 /// seconds. async fn api_ca_kr_activate(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::CaUpdate, Handle::from(&ca), { + aa!(req, Permission::CaUpdate, ca, { let actor = req.actor(); render_empty_res(req.state().ca_keyroll_activate(ca, &actor).await) }) @@ -2316,7 +2316,7 @@ async fn api_ca_aspas_definitions_show( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::AspasRead, Handle::from(&ca), { + aa!(req, Permission::AspasRead, ca, { let state = req.state().clone(); render_json_res(state.ca_aspas_definitions_show(ca).await) }) @@ -2327,7 +2327,7 @@ async fn api_ca_aspas_definitions_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::AspasUpdate, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2347,7 +2347,7 @@ async fn api_ca_aspas_update_aspa( ca: CaHandle, customer: Asn, ) -> RoutingResult { - aa!(req, Permission::AspasUpdate, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2368,7 +2368,7 @@ async fn api_ca_aspas_delete( ca: CaHandle, customer: Asn, ) -> RoutingResult { - aa!(req, Permission::AspasUpdate, Handle::from(&ca), { + aa!(req, Permission::AspasUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2381,7 +2381,7 @@ async fn api_ca_aspas_delete( /// Update the route authorizations for this CA async fn api_ca_routes_update(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::RoutesUpdate, Handle::from(&ca), { + aa!(req, Permission::RoutesUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2401,7 +2401,7 @@ async fn api_ca_routes_try_update( req: Request, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::RoutesUpdate, Handle::from(&ca), { + aa!(req, Permission::RoutesUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2452,7 +2452,7 @@ async fn api_ca_routes_try_update( /// show the route authorizations for this CA async fn api_ca_routes_show(req: Request, ca: CaHandle) -> RoutingResult { - aa!(req, Permission::RoutesRead, Handle::from(&ca), { + aa!(req, Permission::RoutesRead, ca, { match req.state().ca_routes_show(&ca).await { Ok(roas) => render_json(roas), Err(_) => render_unknown_resource(), @@ -2466,7 +2466,7 @@ async fn api_ca_routes_analysis( path: &mut RequestPath, ca: CaHandle, ) -> RoutingResult { - aa!(req, Permission::RoutesAnalysis, Handle::from(&ca), { + aa!(req, Permission::RoutesAnalysis, ca, { match path.next() { Some("full") => { render_json_res(req.state().ca_routes_bgp_analysis(&ca).await) @@ -2619,7 +2619,7 @@ async fn api_ca_rta_list(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, Permission::RtaList, - Handle::from(&ca), + ca, render_json_res(req.state().rta_list(ca).await) ) } @@ -2632,7 +2632,7 @@ async fn api_ca_rta_show( aa!( req, Permission::RtaRead, - Handle::from(&ca), + ca, render_json_res(req.state().rta_show(ca, name).await) ) } @@ -2642,7 +2642,7 @@ async fn api_ca_rta_sign( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RtaUpdate, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); match req.json().await { @@ -2659,7 +2659,7 @@ async fn api_ca_rta_multi_prep( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RtaUpdate, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); @@ -2677,7 +2677,7 @@ async fn api_ca_rta_multi_sign( ca: CaHandle, name: RtaName, ) -> RoutingResult { - aa!(req, Permission::RtaUpdate, Handle::from(&ca), { + aa!(req, Permission::RtaUpdate, ca, { let actor = req.actor(); let state = req.state().clone(); match req.json().await { From 4838bab69617fb028f4fe26114fe5ee45c23679b Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 17:13:39 +0100 Subject: [PATCH 09/24] Remove polar files. --- defaults/abac.polar | 37 ----------- defaults/aliases.polar | 13 ---- defaults/rbac.polar | 42 ------------ defaults/roles.polar | 142 ----------------------------------------- defaults/rules.polar | 136 --------------------------------------- 5 files changed, 370 deletions(-) delete mode 100644 defaults/abac.polar delete mode 100644 defaults/aliases.polar delete mode 100644 defaults/rbac.polar delete mode 100644 defaults/roles.polar delete mode 100644 defaults/rules.polar diff --git a/defaults/abac.polar b/defaults/abac.polar deleted file mode 100644 index 5d283e265..000000000 --- a/defaults/abac.polar +++ /dev/null @@ -1,37 +0,0 @@ -################################################################################ -### Attribute Based Access Control (ABAC) -################################################################################ - -# Restricting access to CAs per user: -# =================================== -# As with defining roles per actor, how defining CAs per actor is done depends -# also in the same way on your krill.conf. - -# 1. Assigning CA access to users based on user attributes: -# ========================================================= -# See roles.polar for how the "role" attribute is used, but instead use -# "inc_cas" and "exc_cas" attributes. - -# 2. Assigning CA access through explicit rules that you define here for users -# defined in your krill.conf file: -# ======================================================================== -# You can also assign CA access directly by writing an actor_cannot_access_ca() -# rule per user as shown below: -# -# To deny access to one or more CAs for a specific user create a rule like so in -# THIS FILE: -# -# actor_cannot_access_ca(actor: Actor{name: "some@user.com"}, ca: Handle) if -# ca.name in ["some_ca_handle", "some_other_ca_handle"] and cut; -# -# To grant access ONLY to one or more CAs for a specific user, create rules -# like so in THIS FILE which first block access to all CAs for the user then -# grant access to specified CAs only for that user: -# -# actor_cannot_access_ca(actor: Actor{name: "some@user.com"}, _: Handle) if -# true; -# actor_can_access_ca(actor: Actor{name: "some@user.com"}, ca: Handle) if -# ca.name in ["some_ca_handle", "some_other_ca_handle"]; - -# actor_cannot_access_ca(actor: Actor{name: "admin-token"}, ca: Handle) if -# ca.name in ["ca2"] and cut; \ No newline at end of file diff --git a/defaults/aliases.polar b/defaults/aliases.polar deleted file mode 100644 index c1c2ee61a..000000000 --- a/defaults/aliases.polar +++ /dev/null @@ -1,13 +0,0 @@ -################################################################################ -### Role aliases -################################################################################ - -# Role names can be aliased so that they can be referred to, e.g. in actor -# attributes, via other names. For example the following aliases the "readonly" -# role to the name "Read Only" and does a quick sanity check to show that it -# works. -# -# role_allow("Read Only", action: Permission) if -# role_allow("readonly", action); -# -# ?= role_allow("Read Only", CA_LIST); diff --git a/defaults/rbac.polar b/defaults/rbac.polar deleted file mode 100644 index fc13494bd..000000000 --- a/defaults/rbac.polar +++ /dev/null @@ -1,42 +0,0 @@ -################################################################################ -### Role Based Access Control (RBAC) -################################################################################ - -# 1. Assigning roles to users based on user attributes: -# ===================================================== -# Appropriately set the "role" attribute on your users, e.g. if set to "admin" -# for a user it would grant that user the "admin" role. The available roles can -# be seen in the roles.polar file. - -# 1a. With: "auth_type" = "config-file" -# ------------------------------------- -# You can assign roles like so in your _krill.conf_ file (NOT IN THIS FILE): -# (note: to generate the password hash see `krillc config user --help`). -# -# [auth_users] -# "some@user.com" = { attributes={ role="admin" }, password_hash="xxx" } - -# 1b. With: "auth_type" = "openid-connect" -# ---------------------------------------- -# You will need to define a "role" claim in your _krill.conf_ file (NOT IN THIS -# FILE) which identifies a field in the OpenID Connect service JSON ID Token or -# UserInfo responses that is set to a string value equal to the name of one of -# the roles defined in the roles.polar file, e.g.: -# -# [auth_openidconnect.claims] -# role = { jmespath = "some_role_field" } -# -# Your "jmespath" may need to be more complex than this, e.g. if you need -# to use only part of the claim value as the role string. - - -# 2. Assigning roles through explicit rules that you define here for users -# defined in your krill.conf file: -# ======================================================================== -# You can also assign roles directly by writing an actor_has_role() rule per user -# in THIS FILE, e.g. like this: -# -# actor_has_role(actor: Actor, role: "admin") if actor.name = "some@user.com"; -# -# Note: The "some@user.com" value MUST be a key under "[auth_users]" in your -# _krill.conf_ file. \ No newline at end of file diff --git a/defaults/roles.polar b/defaults/roles.polar deleted file mode 100644 index e591bfa18..000000000 --- a/defaults/roles.polar +++ /dev/null @@ -1,142 +0,0 @@ -################################################################################ -### Role mappings -################################################################################ - -# Note: mapping of roles to users is not defined here. -# -# Users that authenticate using .htpasswd credentials a role should be assigned -# to them in the mappings.polar or other .polar file using actor_has_role() (see -# below). - -# Users that authenticate with an OpenID Connect provider should have a role -# assigned to them via an attribute extracted from the OpenID Connect provider -# response, or via an explicit actor_has_role() assignment as mentioned above. - - -################################################################################ -### Role definitions -################################################################################ - -# All roles have the right to login: -# ---------------------------------- -# Actors with a role, any role, can login to the UI and are permitted to use the -# REST API. This is because roles are only assigned to actors if they were able -# to authenticate and a role mapping exists for them. Conversely, actors that -# are able to authenticate but for whom no role mapping exists, will not be -# permitted to login to the UI or to use the REST API. -# - -# If called with Option::None then some_role will be the Oso value nil. -# Otherwise some_role should be a string that we want to contain some value -# other than whitespace, so we check that it is non-empty after trimming any -# leading and/or trailing whitespace. -role_allow(some_role, action: Permission) if - not some_role = nil and - not some_role.trim().is_empty() and - action = LOGIN; - -### TEST: [ -# Actors with a role can login. -?= role_allow("some role", LOGIN); -# Conversely, actors without a role cannot do anything. -?= not role_allow(nil, LOGIN); -?= not role_allow("", LOGIN); -?= not role_allow(" ", LOGIN); -?= not role_allow(nil, nil); -?= not role_allow(nil, _); -### ] - - -# The admin role has the right to do anything with any resource: -# -------------------------------------------------------------- -role_allow("admin", _action: Permission); - -### TEST: [ -?= role_allow("admin", _); -?= not role_allow("admin", "take over the world"); -?= role_allow("admin", CA_CREATE); -### ] - - -# The readonly role has the following rights: -# ------------------------------------------- -role_allow("readonly", action: Permission) if - action in [ - CA_LIST, - CA_READ, - PUB_LIST, - PUB_READ, - ROUTES_READ, - ROUTES_ANALYSIS, - ASPAS_READ, - ASPAS_ANALYSIS, - BGPSEC_READ, - RTA_LIST, - RTA_READ - ]; - -### TEST: [ -?= role_allow("readonly", CA_LIST); -?= role_allow("readonly", CA_READ); -?= not role_allow("readonly", CA_CREATE); -?= not role_allow("readonly", CA_CREATE); -# etc -### ] - - -# The readwrite role has the following rights: -# -------------------------------------------- -role_allow("readwrite", action: Permission) if - action in [ - CA_LIST, - CA_READ, - CA_CREATE, - CA_UPDATE, - PUB_LIST, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - ROUTES_READ, - ROUTES_ANALYSIS, - ROUTES_UPDATE, - ASPAS_READ, - ASPAS_UPDATE, - ASPAS_ANALYSIS, - BGPSEC_READ, - BGPSEC_UPDATE, - RTA_LIST, - RTA_READ, - RTA_UPDATE - ]; - -### TEST: [ -?= role_allow("readwrite", CA_LIST); -?= role_allow("readwrite", CA_READ); -?= role_allow("readwrite", CA_CREATE); -?= role_allow("readwrite", CA_CREATE); -# etc -### ] - - -# The testbed role has the following rights: -# ------------------------------------------ -# Note: The testbed role is a special case which is automatically assigned -# temporarily to anonymous users accessing the testbed UI/API. It should not be -# used outside of this file. -role_allow("testbed", action: Permission) if - action in [ - CA_READ, - CA_UPDATE, - PUB_READ, - PUB_CREATE, - PUB_DELETE, - PUB_ADMIN - ]; - -### TEST: [ -?= role_allow("testbed", CA_READ); -?= role_allow("testbed", CA_UPDATE); -?= role_allow("testbed", PUB_ADMIN); -?= not role_allow("testbed", ROUTES_UPDATE); -# etc -### ] \ No newline at end of file diff --git a/defaults/rules.polar b/defaults/rules.polar deleted file mode 100644 index eb8a2a3c1..000000000 --- a/defaults/rules.polar +++ /dev/null @@ -1,136 +0,0 @@ -################################################################################ -### Access rules -################################################################################ - - -# A dummy rule which can be "overridden" by a more specific match. -# Allows overriding rules that are hard to write a more specific rule for, -# especially because matching on a Permission variant is not considered more -# specific than on any variant of Permission due to this issue: -# https://github.com/osohq/oso/issues/801 -disallow(_, _, _) if false; - - -# note: using = or != with application types results in error: -# "comparison operators are unimplemented in the oso Rust library" -# so we don't compare nil to actor.attr() results to see if an attribute is set. - - -################################################################################ -### Check access to Krill REST APIs by requested action -################################################################################ -# The action belongs to a role and thus to have access the user must have the -# required role that includes the requested action. - -allow(actor: Actor, action: Permission, nil) if - not disallow(actor, action, _resource) and - actor_has_role(actor, role) and - role_allow(role, action); - -### TEST: [ -# Sanity check: verify that the built-in admin-token test actor can login.c -# Exercises the rules above. -?= allow(Actor.builtin("admin-token"), LOGIN, nil); -### ] - - -# Assign roles to users automatically if they have a "role" attribute: -# -------------------------------------------------------------------- -actor_has_role(actor: Actor, role) if role in actor.attr("role"); - - - -################################################################################ -### Check access to Krill CAs by requested action and requested CA handle -################################################################################ -# The action belongs to a role and thus to have access the user must have the -# required role that includes the requested action. Additionally the user must -# have explicit or implicit access to the specified CA handle, either because by -# default access isn't restricted per CA handle, or because the user is neither -# explicitly or implicitly denied access to the CA or is explicitly granted -# access to the CA. -allow(actor: Actor, action: Permission, ca: Handle) if - not disallow(actor, action, ca) and - actor_has_role(actor, role) and - role_allow(role, action) and - actor_can_access_ca(actor, ca); - -### TEST: [ -?= allow(Actor.builtin("admin-token"), CA_READ, _); -### ] - - -# Restrict access to CAs based on user "inc_cas" and "exc_cas" attributes: -# ------------------------------------------------------------------------ -# Attribute values are expected to be comma-separated value strings where each -# value is a CA handle. Excludes override includes. Excludes exclude one or more -# CA handles. Includes include one or more CA handles and consequently exclude -# (deny access to) all other CA handles. -# -# Define a rule that will fail to deny access for any actor for any CA handle. -# This is the default situation, i.e. all actors have access to all CAs. -actor_cannot_access_ca(_: Actor, _: Handle) if false; - -# Next define a rule that will succeed either if: -# 1. There is no rule that explicitly blocks access to the specified CA for -# the specified actor. -# 2a. The actor has no "inc_cas" or "exc_cas" attributes that grant or deny -# access to CAs, _OR_ -# 2ba. The actor has an "exc_cas" attribute which does NOT include the -# specified CA handle (i.e. the CA is not excluded from the set the -# actor has access), _AND_ -# 2bba. The actor does not have an "inc_cas" attribute (i.e. the actor is not -# restricted to certain CAs), _OR_ -# 2bbb. The actor has an "inc_cas" attribute which includes the specified CA -# handle (i.e. the CA is included in the set the actor is explicitly -# given access to). -actor_can_access_ca(actor: Actor, ca: Handle) if - # if an inline rule prevents access to the CA stop processing this rule - not actor_cannot_access_ca(actor, ca) and - - ( - # else, if neither include nor exclude attributes exist for this actor, - # allow access to the CA and stop processing this rule - (not _ in actor.attr("inc_cas") and not _ in actor.attr("exc_cas")) or - - # else, if the exclude attribute exists for this actor AND the given CA - # handle is NOT in the set of excluded CAs (which are defined as - # comma-separated CA handle values in a single string attribute) then do not - # exclude access yet, continue below, otherwise stop and deny access - (_ in actor.attr("exc_cas") and not ca.name in actor.attr("exc_cas").unwrap().split(",")) or - - # else, if the include attribute does not exist for this actor then allow - # access, otherwise only allow access if the given CA handle *IS* in the - # include set. - (_ in actor.attr("inc_cas") and ca.name in actor.attr("inc_cas").unwrap().split(",")) - ); - - -### TEST: [ -# test specific CA access restrictions defined inline using Polar rules -actor_cannot_access_ca(_actor: Actor{name: "dummy-test-actor2"}, ca: Handle) if - ca.name in ["dummy-test-ca2"] and cut; - -actor_cannot_access_ca(_actor: Actor{name: "dummy-test-actor3"}, ca: Handle) if - ca.name in ["dummy-test-ca3"] and cut; - -?= not actor_cannot_access_ca(new Actor("dummy-test-actor1", {}), new Handle("dummy-test-ca1")); -?= not actor_cannot_access_ca(new Actor("dummy-test-actor1", {}), new Handle("dummy-test-ca2")); -?= not actor_cannot_access_ca(new Actor("dummy-test-actor1", {}), new Handle("dummy-test-ca3")); - -?= not actor_cannot_access_ca(new Actor("dummy-test-actor2", {}), new Handle("dummy-test-ca1")); -?= actor_cannot_access_ca(new Actor("dummy-test-actor2", {}), new Handle("dummy-test-ca2")); -?= not actor_cannot_access_ca(new Actor("dummy-test-actor2", {}), new Handle("dummy-test-ca3")); - -?= not actor_cannot_access_ca(new Actor("dummy-test-actor3", {}), new Handle("dummy-test-ca1")); -?= not actor_cannot_access_ca(new Actor("dummy-test-actor3", {}), new Handle("dummy-test-ca2")); -?= actor_cannot_access_ca(new Actor("dummy-test-actor3", {}), new Handle("dummy-test-ca3")); - -# test CA access restrictions based on actor attribute values -?= actor_can_access_ca(new Actor("a", {}), new Handle("ca1")); -?= actor_can_access_ca(new Actor("a", {inc_cas: "ca1"}), new Handle("ca1")); -?= not actor_can_access_ca(new Actor("a", {inc_cas: "ca1"}), new Handle("ca2")); -?= not actor_can_access_ca(new Actor("a", {exc_cas: "ca1"}), new Handle("ca1")); -?= actor_can_access_ca(new Actor("a", {exc_cas: "ca1"}), new Handle("ca2")); - -### ] From baaf9282d8115ffdde0c0033b78717b2dd6e15f7 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 17:17:41 +0100 Subject: [PATCH 10/24] =?UTF-8?q?Remove=20it,=20don=E2=80=99t=20just=20com?= =?UTF-8?q?ment=20it=20out=20...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/daemon/auth/authorizer.rs | 39 ----------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 46dd509b4..c38eea0fc 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -413,42 +413,3 @@ impl AuthInfo { } } -/* - -//------------ Handle -------------------------------------------------------- - -/// Handle for Authorization purposes. -// This type is a wrapper so the we can implement the PolarClass trait which -// is required when multi-user is enabled. We always need to pass the handle -// into the authorization macro, even if multi-user is not enabled. So we need -// this type even then. -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct Handle(MyHandle); - -impl fmt::Display for Handle { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From<&MyHandle> for Handle { - fn from(h: &MyHandle) -> Self { - Handle(h.clone()) - } -} - -impl FromStr for Handle { - type Err = InvalidHandle; - - fn from_str(s: &str) -> Result { - MyHandle::from_str(s).map(Handle) - } -} - -impl AsRef for Handle { - fn as_ref(&self) -> &MyHandle { - &self.0 - } -} - -*/ From 2e06b8db4fdd2c146d58c8758738f8901b6bce6f Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 31 Oct 2024 17:29:02 +0100 Subject: [PATCH 11/24] Move modules from auth::common into auth. --- src/daemon/auth/common/mod.rs | 13 -------- src/daemon/auth/{common => }/crypt.rs | 11 ------- src/daemon/auth/mod.rs | 8 +++-- src/daemon/auth/providers/config_file.rs | 4 +-- .../auth/providers/openid_connect/provider.rs | 6 ++-- src/daemon/auth/{common => }/session.rs | 31 ++----------------- 6 files changed, 11 insertions(+), 62 deletions(-) delete mode 100644 src/daemon/auth/common/mod.rs rename src/daemon/auth/{common => }/crypt.rs (94%) rename src/daemon/auth/{common => }/session.rs (93%) diff --git a/src/daemon/auth/common/mod.rs b/src/daemon/auth/common/mod.rs deleted file mode 100644 index 6d96e7754..000000000 --- a/src/daemon/auth/common/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(feature = "multi-user")] -pub mod crypt; - -#[derive(Debug, Clone)] -pub struct NoResourceType; -impl std::fmt::Display for NoResourceType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "") - } -} - -#[cfg(feature = "multi-user")] -pub mod session; diff --git a/src/daemon/auth/common/crypt.rs b/src/daemon/auth/crypt.rs similarity index 94% rename from src/daemon/auth/common/crypt.rs rename to src/daemon/auth/crypt.rs index c640f284e..3785fdba2 100644 --- a/src/daemon/auth/common/crypt.rs +++ b/src/daemon/auth/crypt.rs @@ -98,17 +98,6 @@ impl CryptState { nonce: NonceState::new()?, }) } - - pub fn from_key_vec(key_vec: Vec) -> KrillResult { - let boxed_array: Box<[u8; CHACHA20_KEY_BYTE_LEN]> = - key_vec.into_boxed_slice().try_into().map_err(|_| { - Error::custom( - "Unable to process session encryption key".to_string(), - ) - })?; - - Self::from_key_bytes(*boxed_array) - } } // Returns nonce + tag + cipher text, or an error. diff --git a/src/daemon/auth/mod.rs b/src/daemon/auth/mod.rs index 7ed5b6661..40316795c 100644 --- a/src/daemon/auth/mod.rs +++ b/src/daemon/auth/mod.rs @@ -1,12 +1,14 @@ -pub mod authorizer; -pub mod providers; -pub mod common; pub use self::authorizer::{AuthInfo, Authorizer, LoggedInUser}; pub use self::permission::{Permission, PermissionSet}; pub use self::roles::{Role, RoleMap}; +pub mod providers; + +mod authorizer; +#[cfg(feature = "multi-user")] mod crypt; mod permission; mod roles; +#[cfg(feature = "multi-user")] mod session; diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index 898ce4413..ee81bdb7b 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -8,9 +8,9 @@ use crate::commons::api::Token; use crate::commons::error::{ApiAuthError, Error}; use crate::commons::util::httpclient; use crate::constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R}; +use crate::daemon::auth::crypt; use crate::daemon::auth::{AuthInfo, LoggedInUser, Permission, RoleMap}; -use crate::daemon::auth::common::crypt; -use crate::daemon::auth::common::session::{ClientSession, LoginSessionCache}; +use crate::daemon::auth::session::{ClientSession, LoginSessionCache}; use crate::daemon::config::Config; use crate::daemon::http::{HttpResponse, HyperRequest}; diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index 672a22e59..5fe3e97d2 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -64,10 +64,7 @@ use crate::{ }, daemon::{ auth::{ - common::{ - crypt::{self, CryptState}, - session::*, - }, + crypt::{self, CryptState}, providers::openid_connect::{ httpclient::logging_http_client, util::{ @@ -76,6 +73,7 @@ use crate::{ WantedMeta, }, }, + session::*, AuthInfo, LoggedInUser, Permission, }, config::Config, diff --git a/src/daemon/auth/common/session.rs b/src/daemon/auth/session.rs similarity index 93% rename from src/daemon/auth/common/session.rs rename to src/daemon/auth/session.rs index dc42753ad..acbda68af 100644 --- a/src/daemon/auth/common/session.rs +++ b/src/daemon/auth/session.rs @@ -9,8 +9,8 @@ use serde::de::DeserializeOwned; use crate::commons::KrillResult; use crate::commons::api::Token; use crate::commons::error::{ApiAuthError, Error}; -use crate::daemon::auth::common::crypt; -use crate::daemon::auth::common::crypt::{CryptState, NonceState}; +use crate::daemon::auth::crypt; +use crate::daemon::auth::crypt::{CryptState, NonceState}; const MAX_CACHE_SECS: u64 = 30; @@ -118,33 +118,6 @@ impl LoginSessionCache { } } - pub fn with_ttl(self, ttl_secs: u64) -> Self { - LoginSessionCache { - cache: self.cache, - encrypt_fn: self.encrypt_fn, - decrypt_fn: self.decrypt_fn, - ttl_secs, - } - } - - pub fn with_encrypter(self, encrypt_fn: EncryptFn) -> Self { - LoginSessionCache { - cache: self.cache, - encrypt_fn, - decrypt_fn: self.decrypt_fn, - ttl_secs: self.ttl_secs, - } - } - - pub fn with_decrypter(self, decrypt_fn: DecryptFn) -> Self { - LoginSessionCache { - cache: self.cache, - encrypt_fn: self.encrypt_fn, - decrypt_fn, - ttl_secs: self.ttl_secs, - } - } - fn time_now_secs_since_epoch() -> KrillResult { Ok(SystemTime::now() .duration_since(UNIX_EPOCH) From ee9ce95afa39a528e4b53297b935f74734ddab7f Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Fri, 1 Nov 2024 12:54:23 +0100 Subject: [PATCH 12/24] Removed unused config items, rewrite multi-user default conf. --- defaults/krill-multi-user.conf | 707 ++++++++++++++++++--------------- src/daemon/auth/session.rs | 9 +- src/daemon/config.rs | 26 -- 3 files changed, 387 insertions(+), 355 deletions(-) diff --git a/defaults/krill-multi-user.conf b/defaults/krill-multi-user.conf index 7698d7b18..3ed210463 100644 --- a/defaults/krill-multi-user.conf +++ b/defaults/krill-multi-user.conf @@ -1,44 +1,50 @@ -###################################################################################### -# # -# ----==== WEB UI MULTI-USER LOGIN CONFIGURATION ====---- # -# # -# The settings below can be used to permit multiple users with configurable # -# access rights to login to the Krill web interface. # -# # -###################################################################################### +############################################################################## +# # +# ----==== WEB UI MULTI-USER LOGIN CONFIGURATION ====---- # +# # +# The settings below can be used to permit multiple users with configurable # +# access rights to login to the Krill web interface. # +# # +############################################################################## -# # Global auth(entication & authorization) settings # -# These control which auth provider in Krill will be used to authenticate users -# and settings common to all auth providers. See below for more details. +# These control which auth provider in Krill will be used to authenticate +# users and settings common to all auth providers. See below for more details. # # auth_type = "admin-token" -# auth_policies = ["...", ...] -# auth_private_attributes = ["...", ...] # Auth type (optional) # -# Which provider to use for authentication (AuthN), authorization (AuthZ) and -# identity (ID). Also affects which login form the Krill web UI displays, or -# (in the case of auth_type = "openid-connect") the user is redirected to. +# Which provider to use for authentication (AuthN), identity (ID), and +# authorization (AuthZ). Also affects which login form the Krill web UI +# displays, or (in the case of auth_type = "openid-connect") the user is +# redirected to. # # Supported values: "admin-token" (default), "config-file" or "openid-connect". # # At-a-glance comparison: # ======================= -# Setting Value AuthN AuthZ ID -# ---------------------------------------------------------------------------- -# "admin-token" admin_token role = "admin" id = "admin-token" -# ---------------------------------------------------------------------------- -# "openid-connect" provider provider provider -# checked supplied supplied -# ---------------------------------------------------------------------------- -# "config-file" values are taken from the [auth_users] section in this -# config file +# +# Setting Value AuthN ID AuthZ +# -------------------------------------------------------------------------- +# "admin-token" token matches "admin-token" special built-in +# admin_token role with full access +# config value +# -------------------------------------------------------------------------- +# "config-file" login username login username role name from user’s +# appears as key role field in +# in [auth_users] [auth_users] section +# section # ---------------------------------------------------------------------------- +# "openid-connect" provider provider role name provider +# checked supplied supplied +# +# The role names determined by the "openid-connect" and "config-file" types +# are looked up in the [auth_roles] section to determine access permissions. +# See "Auth roles" below. # # NOTE: At present the admin-token provider is used as a fallback provider # when using "openid-connect" or "config-file" as the primary provider. This is @@ -48,61 +54,17 @@ ### auth_type = "admin-token" -# Auth policies (optional) -# -# One or more paths to external authorization policy files to use in addition to -# those built-in to Krill. The files must be in Oso Polar format [*1] and are -# loaded after the built-in Krill policies. -# -# Custom authorization policies are intended to handle requirements that are too -# complex for just the settings available in krill.conf and is an advanced -# topic beyond the scope of this documentation. -# -# The built-in policies treat the following user attributes specially: -# -# - "role" - One of "admin", "readwrite" or "readonly". See the full Krill -# documentation for more information about which permissions are -# associated with each role. -# - "inc_cas" - A comma-separated set of CA handles which should be included -# in the set the user is permitted to see. If present this -# attribute will prevent the user seeing or interacting with any -# CA handle that is not in this set. -# - "exc_cas" - A comma-separated set of CA handles which should be excluded -# from the set the user is permitted to see. Overrides inc_cas. -# If inc_cas is not set, any CA handle NOT in exc_cas will be -# visible to the user who may interact with it according to -# the permissions granted to the user (e.g. through a role -# assignment). -# -# Note: The inc_cas and exc_cas settings only restrict visibility of and -# interaction with specified CAs via the Krill web UI. CA handles are still -# visible in the repository content and metrics output by Krill. -# -# References: -# *1 - https://docs.osohq.com/getting-started/policies/index.html -# -### auth_policies = ["...", ...] - - -# Auth private attributes (optional) -# -# Zero or more user attributes that should not be revealed by (or even sent to) -# the Krill web UI. For example, you may wish to hide "exc_cas" so that a user -# doesn't know which CAs they are prevented from seeing! -# -### auth_private_attributes = ["...", ...] - - # Config File auth provider details (mandatory when auth_type = "config-file") # -# The Config File auth provider allows you to define one or more users which can -# then be used to login to the Krill web UI. +# The Config File auth provider allows you to define one or more users which +# can then be used to login to the Krill web UI. # # Example: # auth_type = "config-file" # # [auth_users] -# "joe@example.com" = { attributes={ role="admin", exc_cas="ca1" }, password_hash="...", salt="..." } +# "joe@example.com" = { role="admin", password_hash="...", salt="..." } +# "jill@example.com" = { role="read-ca1", password_hash="...", salt="..." } # # Syntax: # auth_users = { "some id" = { ... } [, "another id" = { ... }, ...] } @@ -112,33 +74,31 @@ # "some id" = { ... } # "another id" = { ... } # -# Where { ... } can contain the following fields: +# +# The "some id" and "another id" terms indicate the email address or other +# identifier for the user. It will need to be entered in the username form +# field in the web UI when logging in. Krill also shows it in the event +# history as the actor to which the action is attributed. +# +# The { ... } above can contain the following fields: # # Field Mandatory? Notes -# ---------------------------------------------------------------------------- -# id Yes Email address or other identifier for the user. -# To be entered in the username form field in the -# web UI when logging in. Also shown in the Krill -# event history as the actor to which the action is -# attributed. -# -# password_hash Yes Generate these values using the 'krillc config user' -# salt Yes command on the command line. The web UI will hash -# the password entered in the login form and submit -# it to Krill for comparison to this hash, thereby -# ensuring that passwords are neither transmitted -# nor persisted. Per password salts prevents use of -# rainbow table attacks. Dual salting prevents use of -# stolen password hashes from the config file being -# used to login without knowing the passwords. -# -# attributes No Zero or more key=value pairs, e.g. role="admin". -# The built-in authorization policy (see above) -# requires a role attribute with value "admin", -# "readonly" or "readwrite". Attribute key=value -# pairs may be displayed by the Krill web UI. To -# prevent attributes being sent to the UI, use the -# auth_private_attributes setting (see above). +# -------------------------------------------------------------------------- +# +# password_hash Yes Generate these values using the +# 'krillc config user' command on the command +# salt Yes line. The web UI will hash the password entered +# in the login form and submit it to Krill for +# comparison to this hash, thereby ensuring that +# passwords are neither transmitted nor +# persisted. Per password salts prevents use of +# rainbow table attacks. Dual salting prevents +# use of stolen password hashes from the config +# file being used to login without knowing the +# passwords. +# +# role Yes The name of the role which determines the +# user’s access rights. See "Auth roles" below. # ### auth_type = "config-file" ### @@ -146,12 +106,13 @@ ### ... -# OpenID Connect auth provider details (mandatory when auth_type = "openid-connect") +# OpenID Connect auth provider details +# (mandatory when auth_type = "openid-connect") # # The OpenID Connect auth provider delegates authentication of users to an # external provider that implements the OpenID Connect Core 1.0 specification. -# It can also optionally retrieve user attributes (known as "claims" [*1]) from -# the provider, or from an [auth_users] section in the Krill configuration file. +# Krill uses user attributes (known as "claims" [*1]) from the provider to +# determine the user ID and role name for a user. # # Syntax: # auth_openidconnect = { issuer_url="...", client_id="...", client_secret="..." } @@ -167,23 +128,24 @@ # prompt_for_login = false # logout_url = "..." # -# [auth_openidconnect.claims] +# [[auth_openidconnect.claims]] # ... # -# Where { ... } can contain the following fields: +# Where [auth_openidconnect] can contain the following fields: # -# (Sub)Field Mandatory? Notes -# ---------------------------------------------------------------------------- -# issuer_url Yes Provided by your OpenID Connect provider. This is -# the URL of the OpenID Connect provider discovery -# endpoint. "/.well-known/openid_configuration" -# will be appended if not present. Krill will fetch -# the OpenID Connect Discovery 1.0 compliant JSON -# response from this URL when Krill starts up. If -# this URL does not match the "issuer" value in the -# discovery endpoint response or if the discovery -# endpoint cannot be contacted, Krill will fail to -# start. +# Field Mandatory? Notes +# -------------------------------------------------------------------------- +# issuer_url Yes Provided by your OpenID Connect provider. This +# is the URL of the OpenID Connect provider +# discovery endpoint. +# "/.well-known/openid_configuration" +# will be appended if not present. Krill will +# fetch the OpenID Connect Discovery 1.0 +# compliant JSON response from this URL when +# Krill starts up. If this URL does not match the +# "issuer" value in the discovery endpoint +# response or if the discovery endpoint cannot be +# contacted, Krill will fail to start. # # client_id Yes Provided by your OpenID Connect provider. # @@ -192,38 +154,40 @@ # insecure No Defaults to false. Setting this to true will # disable verification of the signature of the # OpenID Connect provider token ID endpoint -# response. Setting this to false may allow attackers -# to modify responses from the provider without -# being detected. Setting this to false is strongly -# discouraged. +# response. Setting this to false may allow +# attackers to modify responses from the provider +# without being detected. Setting this to false +# is strongly discouraged. # # extra_login_scopes No Provider specific. Defaults to "". A # comma-separated list of OAuth 2.0 scopes to be -# passed to the provider when a user is directed to -# login with the provider. Scopes are typically -# used to instruct the provider to send additional -# user details along with provider token responses. -# One common scope is "profile" which often causes -# the server to respond with email addresses and -# other personal details about the user. If the -# OpenID Connect provider discovery endpoint shows -# that "email" is a supported scope then the "email" -# scope will be requested automatically, you don't -# need to specify it here in that case. -# -# extra_login_params No A { key=value, ... } map of additional HTTP query -# parameters to send with the authorization request -# to the provider when redirecting the user to the -# OpenID Connect provider login form. Section -# 3.1.2.1. Authentication Request in the OpenID -# Connect Core 1.0 specification [*2] lists various -# parameters that can be sent but the supported set -# varies by provider. The prompt=login parameter is -# automatically sent by the provider (though this -# behavior can be disabled, see prompt_for_login -# below) and thus does not need to be provided -# using this setting. Can also be specified as a -# separate TOML table, e.g.: +# passed to the provider when a user is directed +# to login with the provider. Scopes are +# typically used to instruct the provider to send +# additional user details along with provider +# token responses. One common scope is "profile" +# which often causes the server to respond with +# email addresses and other personal details +# about the user. If the OpenID Connect provider +# discovery endpoint shows that "email" is a +# supported scope then the "email" scope will be +# requested automatically, you don't need to +# specify it here in that case. +# +# extra_login_params No A { key=value, ... } map of additional HTTP +# query parameters to send with the authorization +# request to the provider when redirecting the +# user to the OpenID Connect provider login form. +# Section 3.1.2.1. Authentication Request in the +# OpenID Connect Core 1.0 specification [*2] +# lists various parameters that can be sent but +# the supported set varies by provider. The +# prompt=login parameter is automatically sent by +# the provider (though this behavior can be +# disabled, see prompt_for_login below) and thus +# does not need to be provided using this +# setting. Can also be specified as a separate +# TOML table, e.g.: # # [openid_connect.extra_login_params] # display=popup @@ -233,146 +197,153 @@ # disable the default behaviour of sending the # prompt=login parameter to the provider. This # also allows a different prompt= to be -# specified using extra_login_params, from the set -# defined in Section 3.1.2.1. Authentication +# specified using extra_login_params, from the +# set defined in Section 3.1.2.1. Authentication # Request in the OpenID Connect Core 1.0 # specification [*2]: "none", "login", "consent" # or "select_account". # -# logout_url No A URL to direct the browser to redirect the user -# to in order to logout. Ideally this is not needed -# as the provider OpenID Connect Discovery response -# should contain the details Krill needs, but for -# some providers a logout_url must be specified -# explicitly. If the provider discovery response -# doesn't announce support for any supported -# mechanisms and no logout_url value is set then -# Krill will default to directing the user back to -# the Krill UI index page from where the user will -# be directed to login again via the OpenID Connect -# provider. -# -# claims No A { ={...}, ... } map used to extract and -# +-- source No optionally transform claim values from the OpenID -# +-- jmespath Yes Connect provider responses [*3, *4]. Each claim -# +-- dest No specification results in zero or one additional -# attribute name=value pairs that can be shown -# in the Krill web UI and can be tested by the -# authorization policy.. Can also be specified as -# a separate TOML table, e.g.: -# -# [openid_connect.claims] -# name = { source="...", jmespath="...", dest="..."} -# name2 = { ... } -# -# An "id" claim is required. If not specified the -# following default "id" claim configuration will -# be used: -# -# id = { jmespath="email" } -# -# To prevent attributes being sent to the UI, use -# the auth_private_attributes setting (see above). -# -# source If the 'source' subfield is not provided, all -# available token and userinfo claim responses from -# the OpenID Connect provider will be searched for -# a field that matches the 'jmespath' expression. +# logout_url No A URL to direct the browser to redirect the +# user to in order to logout. Ideally this is not +# needed as the provider OpenID Connect Discovery +# response should contain the details Krill +# needs, but for some providers a logout_url must +# be specified explicitly. If the provider +# discovery response doesn't announce support for +# any supported mechanisms and no logout_url +# value is set then Krill will default to +# directing the user back to the Krill UI index +# page from where the user will be directed to +# login again via the OpenID Connect provider. +# +# claims No A list used to extract and optionally transform +# claim values from the OpenID Connect provider +# responses. These will typically given as +# separate TOML array tables. The fields are +# described in the following section. +# +# +# Each [[auth_openidconnect.claims]] occurence describes one claim +# transformation rule. Each rule describes a test against the claim values +# contained in the OpenID Connect provider response [*3, *4]. If a tests +# succeeds, the rule is used to set an attribute. For each attribute only the +# first succeeding rule is considered. +# +# Field Mandatory? Notes +# -------------------------------------------------------------------------- +# dest Yes The attribute that should be set if the rule +# applies. There are currently two attributes: +# +# "id" the user ID shown in the Krill UI and +# as the actor name in the Krill event +# log. +# +# "role" the name of the role that should be +# used for the user to determine +# access permissions. Roles are defined +# via the [auth_roles] config section +# and described below. +# +# source No If the 'source' subfield is not provided, all +# available token and userinfo claim responses +# from the OpenID Connect provider will be +# searched for a field that matches the 'claim' +# value. # # If specified the value identifies a specific # claim set to search and can be one of the # following values: # -# config-file # id-token-standard-claim # id-token-additional-claim # user-info-standard-claim # user-info-additional-claim # -# The source = "config-file" value is special, it -# doesn't refer to an OpenID Connect provider -# response claim set but rather to user attributes -# looked up using the "id" claim value as a key to -# index into the [auth_users] user attribute map. -# -# The "id" claim value cannot therefore itself be -# taken from [auth_users], and password_hash values -# in [auth_users] are ignored as authentication is -# handled by the OpenID Connect provider. -# -# dest The optional "dest" field can be used to set the -# value of an attribute by a different name than -# the claims key used. This can be used to specify -# multiple claim rules that attempt to extract a -# a value for the same claim. The first matching -# rule in such cases will be used. -# -# jmespath The "jmespath" field specifies a JMESPath [*5] -# expression which is used to find a matching field -# in the OpenID Connect provider JSON response. In -# addition to the standard JMESPath functions the -# Krill implementation includes two custom regular -# expression based functions to match and -# optionally replace parts of the value of the -# fieldm matched by the JMESPath expression. These -# two functions are: -# -# recap(, 'capturing regex') -# resub(, 'search regex', replace')) -# -# With these extra functions cases where part of a -# complex string should be matched, extacted and -# (with resub) mapped to a value that matches what -# the authorization policy expects. E.g. it could -# be used to match a substring and then to "output" -# a particular Krill role name. -# -# If the combination of "resub()" and "dest" is -# not powerful enough you can take value matching -# even further using policy file rules. "dest" and -# "resub" may be combined with policy file rules in -# order to simplify the policy file rules needed. -# -# When determining the right "jmespath" expression -# to use, match failures will be logged at "info" -# level (as the auth policy in use may not require -# all configured claims to be found for all users) -# including a list of claims that are available to -# match. Additionally at "debug" level details -# about the claim search process are logged and at -# "trace" level the OpenID HTTP Connect provider -# HTTP/JSON responses are logged. -# -# Escaping: If you need to use double quotes to -# escape a JMESPath identifier you will need to use -# jmespath='...' or jmespath='''...''' instead of -# jmespath="..." in the Krill configuration file. -# See the JMESPath [*6] and TOML [*7] specs for -# more information about quoting and escaping. +# claim Yes The name of the field that is being looked at. +# +# match No A regular expression that is applied to the +# value of the claim provided by the 'claim' +# field. +# +# A claim value matches if the regular expression +# matches. This could be a partial match, i.e., +# the rexpression "foo" matches "foo" but also +# "foobar" and "barfoobar". Enclose the text in +# a leading hat and trailing dollar sign for a +# full match, i.e., "^foo$" will only match "foo". +# +# If the expression matches, the claim value will +# be transformed using the expression given in +# the 'subst' field. +# +# Simple claim values are compared using their +# string representation. E.g. a boolean value is +# treated as having the string values "true" or +# "false" and numbers are similarly converted +# using standard JSON rules. +# +# For arrays, each element is matched and the +# first match is used. +# +# Objects never match. +# +# If the 'match' field in missing, any simple +# values matches and is used as is, i.e., the +# expression in the 'subst' field is ignored. +# For array claim values, the first element is +# used. +# +# subst No This field describes a transformation of a +# value matched via the 'match'. It can be a +# simple string or can contain references to +# substrings captured by the 'match' regular +# expression. +# +# All instances of "$ref" in the subst expression +# are replaced with the substring corresponding +# to the capture group identified by "ref". +# +# "ref" may be an integer corresponding to the +# index of the capture group (counted by order +# of opening parenthesis where 0 is the entire +# match) or it can be a name (consisting of +# letters, digits or underscores) corresponding +# to a named capture group. +# If "ref" isn’t a valid capture group (whether +# the name doesn’t exist or isn’t a valid index), +# then it is replaced with the empty string. +# +# The longest possible name is used. For example, +# "$1a" looks up the capture group named "1a" and +# not the capture group at index 1. To exert more +# precise control over the name, use braces, +# e.g., "${1}a". +# +# To write a literal "$" use "$$". # # References: # *1: https://openid.net/specs/openid-connect-core-1_0.html#Claims # *2: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest # *3: https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse # *4: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse -# *5: https://jmespath.org/ -# *6: https://jmespath.org/specification.html#identifiers -# *7: https://toml.io/en/v1.0.0#string # -# ------------------------------------------------------------------------------ +# +# ---------------------------------------------------------------------------- # Registering Krill with an OpenID Connect provider: -# ------------------------------------------------------------------------------ +# ---------------------------------------------------------------------------- # In order to communicate with an OpenID Connect provider, Krill must first be -# registered with that provider. As a result of registration you will be issued -# a client_id and a client_secret, and possibly also an issuer_url (or you may -# have to consult the provider documentation to determine the issuer_url). +# registered with that provider. As a result of registration you will be +# issued a client_id and a client_secret, and possibly also an issuer_url (or +# you may have to consult the provider documentation to determine the +# issuer_url). # # When registering you will usually need to specify a callback URL. For Krill # this should be auth/callback (replace with the # actual value set above). # -# When auth_type = "openid-connect" the client details MUST be provided to Krill -# via settings in the [auth_openidconnect] section of the configuration file. +# When auth_type = "openid-connect" the client details MUST be provided to +# Krill via settings in the [auth_openidconnect] section of the configuration +# file. # # ------------------------------------------------------------------------------ # Required OpenID Connect provider capabilities: @@ -384,12 +355,12 @@ # https://openid.net/specs/openid-connect-discovery-1_0.html # https://openid.net/specs/openid-connect-rpinitiated-1_0.html # -# At the issuer_url endpoint the provider MUST announce support for at least the -# following: +# At the issuer_url endpoint the provider MUST announce support for at least +# the following: # # "issuer": ".." # "authorization_endpoint": "..", -# "token_endpoint": "..", ("userinfo_endpoint" is also supported if available) +# "token_endpoint": "..", ("userinfo_endpoint" is supported if available) # "jkws_uri": "..", # "scopes_supported": ["openid"] # "response_types_supported": ["code"] @@ -402,33 +373,33 @@ # A note about HTTPS certificates: # ------------------------------------------------------------------------------ # If the provider URLS are HTTPS URLs (which they should be unless this -# deployment of Krill is only for testing) then the HTTPS certificate must have -# been issued by a CA in the O/S CA certificate store, i.e. either a well known -# authority that is included in the store by default, or a custom CA that you -# have added to the store yourself. Krill will fail to connect to a provider -# that uses a self-signed certificate or a certificate from an unknown root -# certificate authority. For more information see for example: +# deployment of Krill is only for testing) then the HTTPS certificate must +# have been issued by a CA in the O/S CA certificate store, i.e. either a well +# known authority that is included in the store by default, or a custom CA +# that you have added to the store yourself. Krill will fail to connect to a +# provider that uses a self-signed certificate or a certificate from an +# unknown root certificate authority. For more information see for example: # http://manpages.ubuntu.com/manpages/xenial/man8/update-ca-certificates.8.html -# ------------------------------------------------------------------------------ +# ---------------------------------------------------------------------------- # # ------------------------------------------------------------------------------ # A note about end_session_endpoint and revocation_endpoint: # ------------------------------------------------------------------------------ # "end_session_endpoint" is defined by various [*1] OpenID Connect draft -# specifications relating to logout. In Krill it is used for the purpose defined -# in the OpenID Connect RP-Initiated Logout 1.0 spec [*1], namely for Krill as -# the RP (OpenID Connect terms Krill a Relying Party in this context, which is -# particularly confusing given that the term Relying Party also has meaning in -# Krill's native RPKI domain) to be able to initiate logout of the user at the -# provider. Krill also requires that the endpoint either honours the -# "post_logout_redirect_uri" HTTP query parameter (defined as OPTIONAL in the -# spec) or that the provider can be configured with corresponding behaviour, -# i.e. to redirect the end-user user-agent (browser) back to Krill after logout -# is completed at the provider. If support for this is lacking it is undefined -# where the user will end up after logout, which is not an issue if the user -# was finished with Krill, but is annoying if the logout was done in order to -# re-login to Krill as a different user. At least one provider has been observed -# which does NOT support this endpoint. +# specifications relating to logout. In Krill it is used for the purpose +# defined in the OpenID Connect RP-Initiated Logout 1.0 spec [*1], namely for +# Krill as the RP (OpenID Connect terms Krill a Relying Party in this context, +# which is particularly confusing given that the term Relying Party also has +# meaning in Krill's native RPKI domain) to be able to initiate logout of the +# user at the provider. Krill also requires that the endpoint either honours +# the "post_logout_redirect_uri" HTTP query parameter (defined as OPTIONAL in +# the spec) or that the provider can be configured with corresponding +# behaviour, i.e. to redirect the end-user user-agent (browser) back to Krill +# after logout is completed at the provider. If support for this is lacking it +# is undefined where the user will end up after logout, which is not an issue +# if the user was finished with Krill, but is annoying if the logout was done +# in order to re-login to Krill as a different user. At least one provider has +# been observed which does NOT support this endpoint. # # As an alternative Krill also supports "revocation_endpoint" # (see https://tools.ietf.org/html/rfc7009 "OAuth 2.0 Token Revocation") which @@ -462,8 +433,8 @@ # Example Azure Active Directory configuration: # ------------------------------------------------------------------------------ # This example is for a Microsoft Azure cloud Active Directory instance that -# permits only read-only and read-write access to users that login via the Krill -# web UI: +# permits only read-only and read-write access to users that login via the +# Krill web UI: # # [auth_openidconnect] # issuer_url = "https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/v2.0" @@ -471,21 +442,32 @@ # client_secret = "zzzzzzzz" # extra_login_scopes = ["offline_access"] # -# [auth_openidconnect.claims] -# id = { jmespath="name" } -# ro_role = { jmespath="resub(roles[?@ == 'gggggggg-gggg-gggg-gggg-gggggggggggg'] | [0], '^.+$', 'readonly')", dest="role" } -# rw_role = { jmespath="resub(roles[?@ == 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'] | [0], '^.+$', 'readwrite')", dest="role" } -# -# For this to work you must already have configured in the Azure portal your AD -# tenant, app registration and enterprise application settings (with redirect -# URI), users, group assignments and optional claim configuration (in the above -# example AD was configured to expose groups as roles). -# -# The JMESPath expression matches on Azure AD group GUID values, taking the -# first match it finds and then setting the "role" attribute to either readonly -# or readwrite depending on which GUID was matched. The GUIDs for your groups -# will be different than those used in this example, see your Krill log for the -# GUIDs to match on. +# [[auth_openidconnect.claims]] +# dest = "id" +# claim = "name" +# +# [[auth_openidconnect.claims]] +# dest = "role" +# claim = "role" +# match = "^gggggggg-gggg-gggg-gggg-gggggggggggg$" +# subst = "readonly" +# +# [[auth_openidconnect.claims]] +# dest = "role" +# claim = "role" +# match = "^hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh$" +# subst = "readwrite" +# +# For this to work you must already have configured in the Azure portal your +# AD tenant, app registration and enterprise application settings (with +# redirect URI), users, group assignments and optional claim configuration +# (in the above example AD was configured to expose groups as roles). +# +# The 'match' expression matches on Azure AD group GUID values, taking the +# first match it finds and then setting the "role" attribute to either +# "readonly" or "readwrite" depending on which GUID was matched. The GUIDs +# for your groups will be different than those used in this example, see your +# Krill log for the GUIDs to match on. # # The offline_access scope is required in order to trigger Azure Active # Directory to issue a refresh token to Krill. @@ -499,15 +481,23 @@ # client_secret = "zzzzzzzz" # logout_url = "https://dddddddd.auth.eu-central-1.amazoncognito.com/logout?client_id=yyyyyyyy&logout_uri=https://your.krill.domain/" # -# [auth_openidconnect.claims] -# role = { jmespath='''resub("cognito:groups"[?@ == 'KrillAdmins'] | [0], '^.+$', 'admin')''' } +# [[auth_openidconnect.claims]] +# dest = "id" +# claim = "email" +# +# [[auth_openidconnect.claims]] +# dest = role +# claim = "cognito:groups" +# match = "^KrillAdmins$" +# subst = "admin" # # For this to work you must already have configured in the AWS Cognito console # a group called KrillAdmins and have added the logging in user to that group. # Otherwise the "cognito:groups" claim will not be present in the ID token -# response issued by AWS Cognito. You also need to have set a "Sign Out URL" for -# in your AWS Cognito "App client settings" which should match the value you -# use for the "logout_uri" query parameter in the logout_url Krill setting. +# response issued by AWS Cognito. You also need to have set a "Sign Out URL" +# for in your AWS Cognito "App client settings" which should match the value +# you use for the "logout_uri" query parameter in the logout_url Krill +# setting. # # logout_url needs to be set because AWS Cognito doesn't advertise support for # any of the OpenID Connect logout mechanisms that Krill understands. @@ -516,8 +506,6 @@ # specified in hte AWS Cognito "App integration" -> "Domain name" console # setting. The regions in the URLs should also match those that you are using. # -# Note the use of ''' which is needed because the Cognito groups claim contains -# a colon which is a reserved character in JMESPath identifiers. # # ------------------------------------------------------------------------------ # Example Google Cloud Platform configuration: @@ -527,30 +515,99 @@ # client_id = "xxxxxxxx.apps.googleusercontent.com" # client_secret = "yyyyyyyy" # extra_login_scopes = ["profile"] +# +# [[auth_openidconnect.claims]] +# dest = "id" +# claim = "email" # -# [auth_openidconnect.claims] -# role = { jmespath='''recap(resub(picture, '^.+photo\.jpg$', 'admin'), '(admin)')''' } +# [[auth_openidconnect.claims]] +# dest = "role" +# claim = "picture" +# match = "^.+photo\.jpg$" +# subst = "admin" # # For this to work you must already have created Credentials in the Google # developer console and have set the redirect URI to your Krill API # /auth/callback public URL. # -# In this example we have included the ".well-known/..." part of the issuer_url -# to demonstrate that Krill will accept the URL with or without it. -# -# ''' is used to ensure that characters in the regular expression don't conflict -# with JMESPath reserved characters. The JMESPath expression in this example is -# not a useful real world example as it grants "admin" rights to any Google -# account that has an associated picture whose URL ends in photo.jpg. -# -# The JMESPath expression in this example uses an outer recap() call to sanity -# check that the resulting role value is what we expect it to be. Without this -# a URL that doesn't match would pass straight through resub() unchanged. The -# recap() check is needed because you might use resub() to "clean up" values -# that in some cases don't need any cleaning and thus would still be wanted -# even though not modified. -# -# Also note that, while not visible in the configuration above, the GCP OpenID -# Connect provider advertizes an RFC 7009 OAuth 2.0 Token Revocation compatible -# `revocation_endpoint` which Krill will use to revoke the Google login token -# when the user logs out of Krill. \ No newline at end of file +# In this example we have included the ".well-known/..." part of the +# issuer_url to demonstrate that Krill will accept the URL with or without +# it. +# +# The match expression in this example is not a useful real world example as +# it grants "admin" rights to any Google account that has an associated +# picture whose URL ends in photo.jpg. +# +# Note that, while not visible in the configuration above, the GCP OpenID +# Connect provider advertizes an RFC 7009 OAuth 2.0 Token Revocation +# compatible `revocation_endpoint` which Krill will use to revoke the Google +# login token when the user logs out of Krill. + + +# Auth roles (optional) +# +# What an authenticated user has access to is configured through roles. Each +# role contains a set of permissions that are granted to any user having this +# role. Optional, the role allows limiting the CAs that these permissions +# apply to. +# +# Roles are defined through the 'auth_roles' configuration value. +# +# Syntax: +# auth_roles = { "role name": { ... }, ... } +# +# Alternative syntax: +# [auth_roles] +# "role_name" = { ... } +# ... +# +# "role_name" is the name of the role referenced in either the config file +# provider’s user table or the OpenID Connect providers’s role attribute. +# +# The { ... } above can contain the following fields: +# +# Field Mandatory? Notes +# -------------------------------------------------------------------------- +# +# permissions Yes A list of permissions to be granted to the +# role. The following permissions currently +# exist: +# +# login log into the Krill UI +# +# Access to the publication server: +# +# pub-admin, pub-list, pub-read, pub-create, +# pub-delete +# +# Access to CAs +# +# ca-list, ca-read, ca-create, ca-update, +# ca-admin, ca-delete +# +# Access to the ROAs of a CA +# +# routes-read, routes-update, routes-analysis +# +# Access to the ASPAs of a CA +# +# aspas-read, aspas-update, aspas-analysis +# +# Access to the router keys of a CA +# +# bgpsec-read, bgpsec-update +# +# cas No A list of CA handles that the role should +# grant access to. If this field is missing, +# access is granted to all CAs. +# +# If the [auth_roles] section is missing, three default roles will be +# used. These are: +# +# admin Allows full acess to everything +# readonly Allows list and read access to everything. +# readwrite Allows read, create, update, and delete access to everything. +# +### [auth_roles] +### ... + diff --git a/src/daemon/auth/session.rs b/src/daemon/auth/session.rs index acbda68af..e96035cb2 100644 --- a/src/daemon/auth/session.rs +++ b/src/daemon/auth/session.rs @@ -316,10 +316,11 @@ mod tests { // Create a new cache whose items are elligible for eviction after one // second and which does no actual encryption or decryption. - let cache = LoginSessionCache::new() - .with_ttl(1) - .with_encrypter(|_, v, _| Ok(v.to_vec())) - .with_decrypter(|_, v| Ok(v.to_vec())); + let mut cache = LoginSessionCache::new(); + cache.ttl_secs = 1; + cache.encrypt_fn = |_, v, _| Ok(v.to_vec()); + cache.decrypt_fn = |_, v| Ok(v.to_vec()); + let cache = cache; // Add an item to the cache and verify that the cache now has 1 item let item1_token = cache diff --git a/src/daemon/config.rs b/src/daemon/config.rs index 01ee5a108..f30d0c8ae 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -142,16 +142,6 @@ impl ConfigDefaults { } } - #[cfg(feature = "multi-user")] - pub fn auth_policies() -> Vec { - vec![] - } - - #[cfg(feature = "multi-user")] - pub fn auth_private_attributes() -> Vec { - vec![] - } - pub fn ca_refresh_seconds() -> u32 { 24 * 3600 // 24 hours } @@ -564,14 +554,6 @@ pub struct Config { #[serde(default = "ConfigDefaults::auth_type")] pub auth_type: AuthType, - #[cfg(feature = "multi-user")] - #[serde(default = "ConfigDefaults::auth_policies")] - pub auth_policies: Vec, - - #[cfg(feature = "multi-user")] - #[serde(default = "ConfigDefaults::auth_private_attributes")] - pub auth_private_attributes: Vec, - #[cfg(feature = "multi-user")] pub auth_users: Option, @@ -1129,10 +1111,6 @@ impl Config { let auth_type = AuthType::AdminToken; let admin_token = Token::from("secret"); #[cfg(feature = "multi-user")] - let auth_policies = vec![]; - #[cfg(feature = "multi-user")] - let auth_private_attributes = vec![]; - #[cfg(feature = "multi-user")] let auth_users = None; #[cfg(feature = "multi-user")] let auth_openidconnect = None; @@ -1259,10 +1237,6 @@ impl Config { admin_token, auth_type, #[cfg(feature = "multi-user")] - auth_policies, - #[cfg(feature = "multi-user")] - auth_private_attributes, - #[cfg(feature = "multi-user")] auth_users, #[cfg(feature = "multi-user")] auth_openidconnect, From 19181da28067668353a5e5c7db50d3a445f1b7c4 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Mon, 4 Nov 2024 17:23:59 +0100 Subject: [PATCH 13/24] Improve error messages for missing roles. --- src/daemon/auth/authorizer.rs | 2 +- src/daemon/auth/providers/admin_token.rs | 46 +++++++++---- src/daemon/auth/providers/config_file.rs | 49 +++++++++++--- .../auth/providers/openid_connect/provider.rs | 22 ++++++- src/daemon/auth/roles.rs | 66 ++++++++++++++++--- tests/common/mod.rs | 8 --- 6 files changed, 150 insertions(+), 43 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index c38eea0fc..89cbc2cfd 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -65,7 +65,7 @@ impl From for AuthProvider { } impl AuthProvider { - /// Authenticate a user from information included in an HTTP request. + /// Authenticates a user from information included in an HTTP request. /// /// Returns `Ok(None)` to indicate that no authentication information /// was present in the request and the request should thus be treated diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index 89f14f4a8..0bcea037e 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -1,3 +1,5 @@ +//! Auth provider using a pre-defined token. + use std::sync::Arc; use crate::commons::KrillResult; use crate::commons::api::Token; @@ -7,34 +9,51 @@ use crate::daemon::auth::{AuthInfo, LoggedInUser, Role}; use crate::daemon::config::Config; use crate::daemon::http::{HttpResponse, HyperRequest}; -// This is NOT an actual relative path to redirect to. Instead it is the path -// string of an entry in the Vue router routes table to "route" to (in the -// Lagosta single page application). See the routes array in router.js of the -// Lagosta source code. Ideally we could instead return a route name and then -// Lagosta could change this path without requiring that we update to match. + +//------------ Constants ----------------------------------------------------- + +/// The path defined in Krill UI for the login view. const LAGOSTA_LOGIN_ROUTE_PATH: &str = "/login"; + +//------------ AuthProvider -------------------------------------------------- + +/// The admin token auth provider. +/// +/// This auth provider takes a single token from the configuration and +/// only allows requests that carry this token as a bearer token. +/// +/// Currently, the this provider is hard-coded to translate this token into +/// a user named “admin” having the admin special role which allows +/// everything everywhere all at once. pub struct AuthProvider { + /// The configured token to compare with. required_token: Token, + + /// The user name of the actor if authentication succeeds. user_id: Arc, + + /// The role to use if authentication succeeds. role: Arc, } impl AuthProvider { + /// Creates a new admin token auth provider from the given config. pub fn new(config: Arc) -> Self { AuthProvider { required_token: config.admin_token.clone(), - // XXX Get from config. - user_id: "admin".into(), + user_id: "admin-token".into(), role: Role::admin().into(), } } -} - -impl AuthProvider { + + /// Authenticates a user from information included in an HTTP request. + /// + /// If there request has a bearer token, returns `Ok(Some(_))` if it + /// matches the configured token or `Err(_)` otherwise. If there is no + /// bearer token, returns `Ok(None)`. pub fn authenticate( - &self, - request: &HyperRequest, + &self, request: &HyperRequest, ) -> Result, ApiAuthError> { if log_enabled!(log::Level::Trace) { trace!("Attempting to authenticate the request.."); @@ -59,11 +78,13 @@ impl AuthProvider { res } + /// Returns an HTTP text response with the login URL. pub fn get_login_url(&self) -> KrillResult { // Direct Lagosta to show the user the Lagosta API token login form Ok(HttpResponse::text_no_cache(LAGOSTA_LOGIN_ROUTE_PATH.into())) } + /// Establishes a client session from credentials in an HTTP request. pub fn login(&self, request: &HyperRequest) -> KrillResult { match self.authenticate(request)? { Some(_actor) => Ok(LoggedInUser { @@ -76,6 +97,7 @@ impl AuthProvider { } } + /// Returns an HTTP text response with the logout URL. pub fn logout( &self, request: &HyperRequest, diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index ee81bdb7b..d8c327c80 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -1,3 +1,5 @@ +//! Auth provider using user information from the configuration. + use std::collections::HashMap; use std::sync::Arc; use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; @@ -20,19 +22,35 @@ use crate::daemon::http::{HttpResponse, HyperRequest}; /// The location of the login page in Krill UI. const UI_LOGIN_ROUTE_PATH: &str = "/login?withId=true"; +/// A password hash used to prolong operation when a user doesn’t exist. +const FAKE_PASSWORD_HASH: &str = "66616B652070617373776F72642068617368"; + +/// A salt value used to prolong operation when a user doesn’t exist. +const FAKE_SALT: &str = "66616B652073616C74"; + //------------ AuthProvider -------------------------------------------------- +/// The config file auth provider. +/// +/// This auth provider uses user and role information provided via the Krill +/// config and authenticates requests using HTTP Basic Authorization headers. pub struct AuthProvider { + /// The user directory. users: HashMap, + + /// The role directory. roles: Arc, + + /// The session key for encrypting client session information. session_key: crypt::CryptState, + + /// The client session cache. session_cache: SessionCache, - fake_password_hash: String, - fake_salt: String, } impl AuthProvider { + /// Creates an auth provider from the given config. pub fn new( config: &Config, ) -> KrillResult { @@ -47,8 +65,6 @@ impl AuthProvider { roles, session_key, session_cache: SessionCache::new(), - fake_password_hash: hex::encode("fake password hash"), - fake_salt: hex::encode("fake salt"), }) } @@ -77,6 +93,14 @@ impl AuthProvider { ) -> Result { self.roles.get(&session.secrets.role).map(|role| { AuthInfo::user(session.user_id.clone(), role) + }).ok_or_else(|| { + ApiAuthError::ApiAuthPermanentError( + format!( + "user '{}' with undefined role '{}' \ + not caught by config check", + session.user_id, session.secrets.role + ) + ) }) } } @@ -139,12 +163,9 @@ impl AuthProvider { let (user_password_hash, user_salt) = match self.users.get(&auth.username) { Some(user) => { - (user.password_hash.to_string(), user.salt.clone()) + (user.password_hash.as_ref(), user.salt.as_ref()) } - None => ( - self.fake_password_hash.clone(), - self.fake_salt.clone(), - ), + None => (FAKE_PASSWORD_HASH, FAKE_SALT), }; let username = auth.username.trim().nfkc().collect::(); @@ -206,7 +227,15 @@ impl AuthProvider { }; // Check that the user is allowed to log in. - let role = self.roles.get(&user.role)?; + let role = self.roles.get(&user.role).ok_or_else(|| { + ApiAuthError::ApiAuthPermanentError( + format!( + "user '{}' with undefined role '{}' \ + not caught by config check", + username, user.role, + ) + ) + })?; if !role.is_allowed(Permission::Login, None) { let reason = format!( diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index 5fe3e97d2..a0ff35c9b 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -1106,7 +1106,15 @@ impl AuthProvider { ) -> KrillResult { Ok(AuthInfo::user( session.user_id.clone(), - self.config.auth_roles.get(&session.secrets.role)? + self.config.auth_roles.get(&session.secrets.role).ok_or_else(|| { + ApiAuthError::ApiAuthPermanentError( + format!( + "user '{}' with undefined role '{}' \ + not caught during login", + session.user_id, session.secrets.role, + ) + ) + })? )) } } @@ -1657,7 +1665,17 @@ impl AuthProvider { let id = claims.extract_id()?; let role_name = claims.extract_role()?; - let role = self.config.auth_roles.get(&role_name)?; + let role = self.config.auth_roles.get( + &role_name + ).ok_or_else(|| { + let reason = format!( + "Login denied for user '{}': \ + user is assigned undefined role '{}'.", + id, role_name + ); + warn!("{}", reason); + Error::ApiInsufficientRights(reason) + })?; // Step 4 1/2: Check that the user is allowed to log in. if !role.is_allowed(Permission::Login, None) { diff --git a/src/daemon/auth/roles.rs b/src/daemon/auth/roles.rs index c99b7e33f..8544ad0bf 100644 --- a/src/daemon/auth/roles.rs +++ b/src/daemon/auth/roles.rs @@ -2,17 +2,23 @@ use std::collections::HashMap; use std::sync::Arc; use rpki::ca::idexchange::MyHandle; use serde::Deserialize; -use crate::commons::error::ApiAuthError; use super::{Permission, PermissionSet}; //------------ Role ---------------------------------------------------------- -/// The role of actor has. +/// A set of access permissions for resources. /// -/// Permissions aren’t assigned to actors directly but rather to roles to -/// which actors are assigned in turn. +/// Roles provide an intermediary for assigning access permissions to users +/// by managing [permission sets][PermissionSet]. Separete sets can be +/// provided for specific resources, all other resources, and requests that +/// do not operate on resources. +/// +/// Currently, roles are given names and are defined in +/// [Config::auth_roles][crate::daemon::config::Config::auth_roles] and +/// referenced by authorization providers through those names. #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(from = "RoleConf")] pub struct Role { /// Permissions for requests without specific resources. none: PermissionSet, @@ -28,26 +34,42 @@ pub struct Role { } impl Role { + /// Creates the special admin role. + /// + /// This role allows all access to everything. pub fn admin() -> Self { Self::simple(PermissionSet::ANY) } + /// Creates the default read-write role. + /// + /// This role uses `PermissionSet::READWRITE` for everything. pub fn readwrite() -> Self { Self::simple(PermissionSet::READWRITE) } + /// Creates the default read-only role. + /// + /// This role uses `PermissionSet::READONLY` for everything. pub fn readonly() -> Self { Self::simple(PermissionSet::READONLY) } + /// Creates the special testbed role. + /// + /// This role uses `PermissionSet::TESTBED` for everything. pub fn testbed() -> Self { Self::simple(PermissionSet::TESTBED) } + /// Creates the anonymous special role. + /// + /// This role allows nothing. pub fn anonymous() -> Self { Self::simple(PermissionSet::NONE) } + /// Creates a role that uses the provided permission set for all access. pub fn simple(permissions: PermissionSet) -> Self { Self { none: permissions, @@ -56,6 +78,10 @@ impl Role { } } + /// Creates a role that uses the provided set for the given resources. + /// + /// The role will allow access with the set to non-resource requests and + /// all resources provided. Access to all other resources will be denied. pub fn with_resources( permissions: PermissionSet, resources: impl IntoIterator @@ -69,6 +95,12 @@ impl Role { } } + /// Creates a comples role. + /// + /// The permission set `none` will be used for non-resource requests. + /// The `resources` hash map contains special permission sets for the + /// provided resources. The `any` set will be used for all resources + /// not mentioned in the hash map. pub fn complex( none: PermissionSet, any: PermissionSet, @@ -77,6 +109,13 @@ impl Role { Self { none, any, resources } } + /// Returns whether access is allowed. + /// + /// The method whether the role allows access with the provided + /// `permission` to the provided `resource`. If the resource is `None`, + /// access for non-resource requests is checked. + /// + /// Returns `true` if access is allowed or `false` if not. pub fn is_allowed( &self, permission: Permission, @@ -114,38 +153,45 @@ impl From for Role { /// [`Role`] supports. This is on purpose to keep the config format simple. #[derive(Clone, Debug, Deserialize)] struct RoleConf { + /// The permission set to use. permissions: PermissionSet, + /// An optional list of resources to limit access to. + /// + /// If this is `None`, access to all resources will be allowed. cas: Option>, } //------------ RoleMap ------------------------------------------------------- +/// A mapping storing roles under a name. +/// +/// Roles are stored behind an arc to users to keep a keep of the role around. #[derive(Clone, Debug, Default, Deserialize)] pub struct RoleMap(HashMap>); impl RoleMap { + /// Creates a new, empty role map. pub fn new() -> Self { Self::default() } + /// Adds the given role. pub fn add( &mut self, name: impl Into, role: impl Into> ) { self.0.insert(name.into(), role.into()); } + /// Returns whether the map contains a role by the given name. pub fn contains(&self, name: &str) -> bool { self.0.contains_key(name) } - pub fn get(&self, name: &str) -> Result, ApiAuthError> { - self.0.get(name).cloned().ok_or_else(|| { - ApiAuthError::ApiAuthPermanentError( - "user with undefined role not caught by config check".into() - ) - }) + /// Returns the role of the given name if present. + pub fn get(&self, name: &str) -> Option> { + self.0.get(name).cloned() } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 34f31dd1f..7d7fe4957 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -128,10 +128,6 @@ impl TestConfig { let auth_type = AuthType::AdminToken; let admin_token = Token::from("secret"); #[cfg(feature = "multi-user")] - let auth_policies = vec![]; - #[cfg(feature = "multi-user")] - let auth_private_attributes = vec![]; - #[cfg(feature = "multi-user")] let auth_users = None; #[cfg(feature = "multi-user")] let auth_openidconnect = None; @@ -267,10 +263,6 @@ impl TestConfig { admin_token, auth_type, #[cfg(feature = "multi-user")] - auth_policies, - #[cfg(feature = "multi-user")] - auth_private_attributes, - #[cfg(feature = "multi-user")] auth_users, #[cfg(feature = "multi-user")] auth_openidconnect, From 671bdf1d9c9fcce26817035621b26565069a6600 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 6 Nov 2024 12:56:07 +0100 Subject: [PATCH 14/24] Add 'glob' permissions and document roles and permissions. --- doc/manual/source/multi-user.rst | 20 +- .../source/multi-user/authorization.rst | 172 +++++++++++--- .../source/multi-user/customization.rst | 218 ------------------ src/daemon/auth/permission.rs | 93 ++++++-- src/daemon/auth/providers/admin_token.rs | 2 +- src/daemon/auth/roles.rs | 4 + 6 files changed, 230 insertions(+), 279 deletions(-) delete mode 100644 doc/manual/source/multi-user/customization.rst diff --git a/doc/manual/source/multi-user.rst b/doc/manual/source/multi-user.rst index bbf621d66..b799418ba 100644 --- a/doc/manual/source/multi-user.rst +++ b/doc/manual/source/multi-user.rst @@ -12,17 +12,18 @@ Login with Named Users Checking the currently logged in user and user attributes -By default Krill requires users to authenticate using the configured secret token, -and actions in the event history are attributed to a client using the secret token or -to Krill itself. +By default Krill requires users to authenticate using the configured secret +token, and actions in the event history are attributed to a client using the +secret token or to Krill itself. -Krill also supports authenticating users **of the web user interface** with their -own username and credentials. Actions taken by such logged in users are attributed -in the event history to their username. +Krill also supports authenticating users **of the web user interface** with +their own username and credentials. Actions taken by such logged in users are +attributed in the event history to their username. To login users by username Krill must first be configured either with locally -defined user details and credentials, or with the details necessary to interact with -a separate `OpenID Connect `_ compliant identity provider system. +defined user details and credentials, or with the details necessary to +interact with a separate `OpenID Connect `_ +compliant identity provider system. Further reading: @@ -41,5 +42,6 @@ Further reading: .. note:: Clients using the Krill REST API directly or via ``krillc`` cannot authenticate using named users, they can only authenticate using the - secret token. If you need this capability `please let us know `_. + secret token. If you need this capability `please let us know + `_. diff --git a/doc/manual/source/multi-user/authorization.rst b/doc/manual/source/multi-user/authorization.rst index e164772a0..bb5c55599 100644 --- a/doc/manual/source/multi-user/authorization.rst +++ b/doc/manual/source/multi-user/authorization.rst @@ -1,58 +1,156 @@ .. _doc_krill_multi_user_access_control: -Permissions, Roles & Attributes -=============================== +Roles, Permissions and Resources +================================ .. versionadded:: v0.9.0 -This page summarizes the different ways that Krill supports for restricting access -to *named users* that login to Krill. For backward compatibility, users that -authenticate with the secret token are given unrestricted access to Krill. +This page summarizes how Krill supports restricting access for *named users* +that login to Krill. For backward compatibility, users that authenticate with +the secret token are given unrestricted access to Krill. + +Roles +----- + +Rather than restricting access to individual users, Krill adds an +intermediary concept of roles. Each user is assigned a role and these roles +in turn define access restrictions. + +Roles can be defined in the config file through the ``[auth_roles]`` section. +Each role has a name, a set of permissions, and optionally a list of CAs +access is restricted to. + +By default, i.e., if you do not provide your own ``[auth_roles]`` in the +config file, Krill uses three roles: + +.. Glossary:: + + ``admin`` + Grants unrestricted access to all CAs. + + ``readwrite`` + Grants the right to list, view and modify all *existing* CAs. + + ``readonly`` + Grants the right to list and view all CAs. + +If you do provide your own roles, these will *not* be present. + Permissions ----------- -Internally within Krill each REST API endpoint requires the logged in user to have -a specific Krill permission in order to execute the request. +Internally within Krill each REST API endpoint requires the logged in user to +have a specific Krill permission in order to execute the request. When +defining your own roles, you can combine these permissions into a specific +set by listing those you wish to grant to the role. + +Currently, the following permissions are defined: + +.. Glossary:: + + ``login`` + required for logging into the Krill UI, + + ``pub-admin`` + required for access to the built-in publication server, + + ``pub-list`` + required for listing the currently configured publishers of the + publication server, + + ``pub-read`` + required to show details of configured publishers of the + publication server, including the publication response to be returned + to a publisher, + + ``pub-create`` + required to add new publishers to the publication server, + + ``pub-delete`` + required to removed publishers from the publication server, + + ``ca-list`` + required to list existing CAs, + + ``ca-read`` + required to show details of existing CAs, + + ``ca-create`` + required to create new CAs, + + ``ca-update`` + required to update configuration of existing CAs as well as adding + and removing child CAs, + + ``ca-admin`` + required for administrative tasks related to all CAs as well as + importing CAs, also required for access to the trust anchor module, + + ``ca-delete`` + required to remove CAs, + + ``routes-read`` + required to show the ROAs configured for a CA, + + ``routes-update`` + required to update the ROAs configured for a CA, + + ``routes-analysis`` + required to perform BGP route analysis for a CA, + + ``aspas-read`` + required to show the ASPA records configured for a CA, + + ``aspas-update`` + required to update the ASPA records configured for a CA, + + ``bgpsec-read`` + required to show the BGPsec router keys configured for a CA, + + ``bgpsec-update`` + required to update the BGPsec router keys configured for a CA. + +In addition, there two shortcuts that can be used to specify multiple +permission at ones: + +.. Glossary:: + ``any` + grants all permissions, + ``read`` + grants the ``ca-read``, ``routes-read``, ``aspas-read``, and + ``bgpsec-read`` permissions, -User Attributes ---------------- + ``update`` + grants the ``ca-update``, ``routes-update``, ``aspas-update``, and + ``bgpsec-update`` permissions, -User attributes are assigned by the identity provider, either in the -``krill.conf`` file for locally defined users, or in the management interface of -the OpenID Connect provider that manages your users. -.. Warning:: By default, user attributes and their values are shown in the Krill - web user interface and the web user interface stores these - attributes in browser local storage. To prevent sensitive attributes - being revealed in the browser you can mark them as private. One - possible use for this is to restrict access using the ``exc_cas`` - attribute but not reveal the name of the restricted CA by doing - so. See ``auth_private_attributes`` in ``krill.conf`` file for more - information. +Configuring Roles +----------------- -Role Based Access Control -------------------------- +When the default roles are not sufficient, you can create your own set of +roles in the Krill config file. You do so by creating a new block +``[auth_roles]`` which contains a list of all your roles. Each role needs +to have a mapping of one or two fields: -At the highest level Krill can restrict access based on user roles. A role is a -named collection of internal Krill permissions. +* The mandatory field ``permissions`` provides a list of the permissions + to be granted by the role, and -By default Krill supports three roles which can be assigned to users. A user can -only have one role at a time. A role is assigned to a user via the ``role`` -user attribute (see below for more on attributes). +* the optional field ``cas`` is a list of the CAs that the role grants + access to. -The default roles are: +If the ``"cas"`` field is not present, access to all CAs is granted. -- ``admin`` : Grants users unrestricted access. -- ``readwrite``: Grants users the right to list, view and modify *existing* - CAs. -- ``readonly`` : Grants users the right to list and view CAs only. +As an example, here is the definition of the default roles plus a special +role that only allows read access to the ``"example"`` CA. -Attribute Based Access Control ------------------------------- +.. code-block:: toml -Krill supports ``inc_cas`` and ``exc_cas`` user attributes which can be used -to permit or deny access to one or more Certificate Authorities in Krill. User -attributes can also be used to make decisions in :ref:`custom authorization policies `. + [auth_roles] + "admin" = { permissions = [ "any" ] } + "readwrite" = { permissions = [ "pub-list", "pub-read", "pub-create", "pub-delete", "ca-list", "ca-create", "ca-delete", "read", "update" ] } + "readonly" = { permissions = [ "pub-read", "ca-list", "read" ] } + "read-example" = { permissions = [ "read" ], cas = [ "example" ] } diff --git a/doc/manual/source/multi-user/customization.rst b/doc/manual/source/multi-user/customization.rst deleted file mode 100644 index 0e7686ba9..000000000 --- a/doc/manual/source/multi-user/customization.rst +++ /dev/null @@ -1,218 +0,0 @@ -.. _doc_krill_multi_user_custom_policies: - -Custom Authorization Policies -============================= - -.. versionadded:: v0.9.0 - -.. contents:: - :local: - :depth: 2 - -Introduction ------------- - -.. note:: This is an advanced topic, you don't need this feature to - get started with Named Users. If you are considering - implementing a custom authorization policy `we'd love to hear from you `_! - -Custom authorization policies are a way of extending Krill by supplying -one or more files containing rules that will be added to those used by -Krill when deciding if a given action by a user should be permitted or -denied. - -Examples --------- - -Some examples showing the power of this can be seen in `doc/policies `_ -directory in the Krill source code repository. - -`role-per-ca-demo` -"""""""""""""""""" - -By default Krill lets you assign a role to a user that will be enforced -for all of the actions that they take irrespective of the CA being -worked with. The `role-per-ca-demo` example extends Krill so that a -user can be given different roles for different CAs. - -The demo also shows how to use new user attributes to influence -authorization decisions, in this case by looking for a user attribute -by the same name as the CA being worked with, and if found it uses the -attribute value as the role that the user should have when working with -that CA. - -Finally, the demo demonstrates how to add new roles to Krill by adding -two new roles that are more limited in power than the default roles in -Krill: - - - A `readonly`-like role that also has the right to update ROAs. - - A role that only permits a user to login and list CAs. - -`team-based-access-demo` -"""""""""""""""""""""""" - -The `team-based-access-demo` shows how one can define teams in the -policy: - - - Users can optionally belong to a team. - - Users can have a different role in the team than outside of it. - - Being a member of a team grants access to the CAs that the team - works with. - -The example works by defining the team names in the policy file. Each -team is given a name and a list of CAs it works with. Krill is then -extended to understand two new user attributes: - - - `team` - which team a user belongs to - - `teamrole` - which role the user has in the team - -Using custom policies ---------------------- - -To use a custom policies there must be an ``auth_policies`` setting -in ``krill.conf`` specifying the path to one ore more custom policy -files to load on startup. - -.. code-block:: none - - auth_type = "..." - auth_policies = [ "doc/policies/role-per-ca-demo.polar" ] - -.. warning:: Krill will fail to start if a custom authorization - policy file is syntactically invalid or if one of the - self-checks in the policy fails. - -.. warning:: Policy files should only be readable by Krill and - trusted operating system user accounts. - - Krill performs some basic sanity checks on startup to - verify that its authorization policies are working as - expected, but a malicious actor could make more subtle - changes to the policy logic which may go undetected, - like granting their own user elevated rights in Krill. - - If a malicious user is able to write to the policy - file they may however already be able to do much more - significant damage than editing a policy file! - -.. note:: Policy files are not reloaded if changed on disk while - Krill is running. - - For policies that only contain rules this is not a - problem as they would not be expected to change - very often, if ever. - - However, for policies that define configuration in the - policy file, such as the `team-based-access-demo`, - changes to the policy configuration will not take effect - until Krill is restarted. - -Writing custom policies ------------------------ - -Policies are written in the Polar language. The following articles -from the Oso website can help you get started with Polar: - - - `The Polar Language `_ - - `Write Oso Policies (30 min) `_ - - `Polar Syntax Reference `_ - - `Rust Types in Polar `_ - -The core policies and permissions that Krill uses are embedded into -Krill itself and cannot be changed. It is however possible to add -new roles and to add new logic based around the value of custom user -attributes. - -Defining new roles -"""""""""""""""""" - -Krill roles are defined by ``role_allow("rolename", action: Permission)`` -Polar rules. The rule is tested if the role of the current user is -"rolename". The current role definitions test if the requested -action is in a set defined to be valid for that role. - -.. tip:: You can see the built-in `role `_ - and `permission `_ - definitions in the Krill GitHub repository. - -To define a new role that grants read only rights plus the right to -update ROAs one could write the following Polar rule: - -.. code-block:: none - - role_allow("roawrite", action: Permission) - role_allow("readonly", action) or - action = ROUTES_UPDATE; - -This example is actually taken from the `role-per-ca-demo.polar` policy. - -Defining new rules -"""""""""""""""""" - -Let's write a rule that completely prevents the update of ROAs. - -When Oso does a permission check the search for a matching rule -starts by matching rules of the form ``allow(actor, action, resource)``. - -.. tip:: "resource" in this context is a Polar term and should not be - confused with the RPKI term "resource". - -The Krill policy delegates from its `allow` rules immediately to a -special ``disallow(actor, action, resource)`` rule. The only definition -of the ``disallow()`` rule in Krill by default says ``if false``, i.e. -nothing is disallowed. - -While technically you can prevent an action by ``cut`` -ing out of an -``allow()`` rule that is more specific than any other ``allow()`` rules, -it's not always possible to ensure that your rule is the most specific -match. That's where ``disallow()`` comes in handy. - -Let's use ``disallow()`` to implement our rule. - -Create a file called ``no_roa_updates.polar`` containing the following -content: - -.. code-block:: none - - # define our new rule: disallow all ROA updates - disallow(_, ROUTES_UPDATE, _); - - # we could also write this more explicitly like so: - # disallow(_, ROUTES_UPDATE, _) if true; - - # add a test to check that our new rule works by - # showing that an admin user can no longer update - # ROAs! - ?= not allow(new Actor("test", { role: "admin" }), ROUTES_UPDATE, new Handle("some_ca")); - -Let's break this down: - - - The ``_`` character is Polar syntax for "match any". - - Lines starting with ``#`` are comments. - - Lines starting with ``?=`` defines self-test inline queries that - will be executed when Krill starts. If a self-test inline query - fails Krill will exit with an error. - -The rule that we have created says that for any actor trying to update -a ROA on any "resource" (i.e. Certificate Authority), succeed (i.e. -disallow the attempt). - -If we now set ``auth_policies = [ "path/to/no_roa_updates.polar" ]`` -in our ``krill.conf`` file and restart Krill it will no longer be -possible for anyone to update ROAs. - -This is obviously not the most useful policy, but it demonstrates -the idea :-) - -Diagnosing issues -""""""""""""""""" - -If a rule doesn't work as expected a good way to investigate is to -add more self-test inline queries. - -If that fails you can set ``log_level = "debug"`` and set O/S -environment variable ``POLAR_LOG=1`` when runnng Krill. This will -cause a huge amount of internal Polar diagnostic logging which -will show exactly which rules Polar evaluated in which order with -which parameters and what the results were. - diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index 1d4ac9e5e..d47835d79 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -1,4 +1,9 @@ +//! Permissions and permission sets. +//! +//! This is a private module. Its public items are re-exported by the parent. + use std::{fmt, str}; +use std::str::FromStr; use serde::{Deserialize, Serialize}; @@ -72,7 +77,6 @@ define_permission! { (RoutesAnalysis, "routes-analysis"), (AspasRead, "aspas-read"), (AspasUpdate, "aspas-update"), - (AspasAnalysis, "aspas-analyisis"), (BgpsecRead, "bgpsec-read"), (BgpsecUpdate, "bgpsec-update"), (RtaList, "rta-list"), @@ -81,11 +85,60 @@ define_permission! { } +//------------ ConfPermission ------------------------------------------------ + +/// A named permission as given in the config file. +/// +/// This includes all the permissions themselves plus the three “glob” +/// permissions `"list"`, `"read"`, `"create"`, `"delete"`, and `"admin"` +/// which include all the respective permissions for all components. +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(try_from = "&str")] +pub enum ConfPermission { + Single(Permission), + Any + Read, + Update, +} + +impl ConfPermission { + fn add(self, set: PermissionSet) -> PermissionSet { + let self_set = match self { + Self::Single(perm) => { + return set.add(perm) + } + Self::Any => PermissionSet::ANY, + Self::Read => PermissionSet::CONF_READ, + Self::Update => PermissionSet::CONF_UPDATE, + Self::Delete => PermissionSet::CONF_DELETE, + }; + set.add_set(self_set) + } +} + +impl<'a> TryFrom<&'a str> for ConfPermission { + type Error = String; + + fn try_from(src: &'a str) -> Result { + if let Ok(res) = Permission::from_str(src) { + return Ok(Self::Single(res)) + } + + match src { + "any" => Ok(Self::Any), + "read" => Ok(Self::Read), + "update" => Ok(Self::Update), + _ => Err(format!("unknown permission {src}")) + } + } +} + + //------------ PermissionSet ------------------------------------------------- /// A set of permissions. -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -#[serde(from = "Vec", into = "Vec")] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(from = "Vec")] pub struct PermissionSet(u32); impl PermissionSet { @@ -98,6 +151,10 @@ impl PermissionSet { Self(self.0 | Self::mask(permission)) } + pub const fn add_set(self, other: PermissionSet) -> Self { + Self(self.0 | other.0) + } + pub const fn remove(self, permission: Permission) -> Self { Self(self.0 & !Self::mask(permission)) } @@ -120,22 +177,16 @@ impl PermissionSet { } } -impl From> for PermissionSet { - fn from(src: Vec) -> Self { +impl From> for PermissionSet { + fn from(src: Vec) -> Self { let mut res = Self(0); for item in src { - res = res.add(item) + res = item.add(res) } res } } -impl From for Vec { - fn from(src: PermissionSet) -> Self { - src.iter().collect() - } -} - mod policy { use super::PermissionSet; @@ -154,7 +205,6 @@ mod policy { RoutesRead, RoutesAnalysis, AspasRead, - AspasAnalysis, BgpsecRead, RtaList, RtaRead @@ -174,7 +224,6 @@ mod policy { RoutesUpdate, AspasRead, AspasUpdate, - AspasAnalysis, BgpsecRead, BgpsecUpdate, RtaList, @@ -190,6 +239,22 @@ mod policy { PubDelete, PubAdmin ]); + + pub const CONF_READ: Self = Self::from_permissions(&[ + CaRead, RoutesRead, AspasRead, BgpsecRead, RtaRead, + ]); + + pub const CONF_CREATE: Self = Self::from_permissions(&[ + CaCreate, + ]); + + pub const CONF_UPDATE: Self = Self::from_permissions(&[ + RoutesUpdate, BgpsecUpdate, RtaUpdate, + ]); + + pub const CONF_DELETE: Self = Self::from_permissions(&[ + PubDelete, CaDelete, + ]); } } diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index 0bcea037e..7f2faf92a 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -23,7 +23,7 @@ const LAGOSTA_LOGIN_ROUTE_PATH: &str = "/login"; /// This auth provider takes a single token from the configuration and /// only allows requests that carry this token as a bearer token. /// -/// Currently, the this provider is hard-coded to translate this token into +/// Currently, this provider is hard-coded to translate this token into /// a user named “admin” having the admin special role which allows /// everything everywhere all at once. pub struct AuthProvider { diff --git a/src/daemon/auth/roles.rs b/src/daemon/auth/roles.rs index 8544ad0bf..8137ed44d 100644 --- a/src/daemon/auth/roles.rs +++ b/src/daemon/auth/roles.rs @@ -1,3 +1,7 @@ +//! Roles and related types. +//! +//! This is a private module. Its public items are re-exported by the parent. + use std::collections::HashMap; use std::sync::Arc; use rpki::ca::idexchange::MyHandle; From 6609a4dddd0ec345d4e32739429cbaa66e698f8f Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 6 Nov 2024 13:03:19 +0100 Subject: [PATCH 15/24] Maybe try compiling before committing ... --- src/daemon/auth/permission.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index d47835d79..22881e7b2 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -96,7 +96,7 @@ define_permission! { #[serde(try_from = "&str")] pub enum ConfPermission { Single(Permission), - Any + Any, Read, Update, } @@ -110,7 +110,6 @@ impl ConfPermission { Self::Any => PermissionSet::ANY, Self::Read => PermissionSet::CONF_READ, Self::Update => PermissionSet::CONF_UPDATE, - Self::Delete => PermissionSet::CONF_DELETE, }; set.add_set(self_set) } From 693a1f170567f454c51b8d0d9c9f022968265cf8 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 6 Nov 2024 16:22:53 +0100 Subject: [PATCH 16/24] Refactor OpenID Connect claims config yet again. --- defaults/krill-multi-user.conf | 54 +++++--- .../{authorization.rst => roles.rst} | 0 .../auth/providers/openid_connect/claims.rs | 130 ++++++++++++++---- .../auth/providers/openid_connect/config.rs | 37 +++-- .../auth/providers/openid_connect/provider.rs | 9 +- 5 files changed, 155 insertions(+), 75 deletions(-) rename doc/manual/source/multi-user/{authorization.rst => roles.rst} (100%) diff --git a/defaults/krill-multi-user.conf b/defaults/krill-multi-user.conf index 3ed210463..a1220387e 100644 --- a/defaults/krill-multi-user.conf +++ b/defaults/krill-multi-user.conf @@ -128,7 +128,10 @@ # prompt_for_login = false # logout_url = "..." # -# [[auth_openidconnect.claims]] +# [[auth_openidconnect.id_claims]] +# ... +# +# [[auth_openidconnect.role_claims]] # ... # # Where [auth_openidconnect] can contain the following fields: @@ -216,33 +219,37 @@ # page from where the user will be directed to # login again via the OpenID Connect provider. # -# claims No A list used to extract and optionally transform -# claim values from the OpenID Connect provider -# responses. These will typically given as +# id_claims No A list used to extract the user ID from the +# claim values in the OpenID Connect provider +# response. These will typically given as # separate TOML array tables. The fields are # described in the following section. # +# If this field is missing, the default rule +# is used which uses the value of the "email" +# claim as the user ID. # -# Each [[auth_openidconnect.claims]] occurence describes one claim -# transformation rule. Each rule describes a test against the claim values -# contained in the OpenID Connect provider response [*3, *4]. If a tests -# succeeds, the rule is used to set an attribute. For each attribute only the -# first succeeding rule is considered. +# +# role_claims No A list used to extract the user role from the +# claim values in the OpenID Connect provider +# response. These will typically given as +# separate TOML array tables. The fields are +# described in the following section. +# +# If this field is missing, the default rule +# is used which uses the value of the "role" +# claim as the user’s role. +# +# +# Each [[auth_openidconnect.id_claims]] and [[auth_openidconnect.role_claims]] +# occurence describes one claim transformation rule. Each rule describes a +# test against the claim values contained in the OpenID Connect provider +# response [*3, *4]. If a tests succeeds, the value is transformed and used as +# either the user ID or user role. For each attribute of the two fields, only +# the first succeeding rule is considered. # # Field Mandatory? Notes # -------------------------------------------------------------------------- -# dest Yes The attribute that should be set if the rule -# applies. There are currently two attributes: -# -# "id" the user ID shown in the Krill UI and -# as the actor name in the Krill event -# log. -# -# "role" the name of the role that should be -# used for the user to determine -# access permissions. Roles are defined -# via the [auth_roles] config section -# and described below. # # source No If the 'source' subfield is not provided, all # available token and userinfo claim responses @@ -259,7 +266,10 @@ # user-info-standard-claim # user-info-additional-claim # -# claim Yes The name of the field that is being looked at. +# claim No The name of the field that is being looked at. +# If this field is missing, then the 'subst' +# field contains the value to be used for the +# user ID or role, independently of any claims- # # match No A regular expression that is applied to the # value of the claim provided by the 'claim' diff --git a/doc/manual/source/multi-user/authorization.rst b/doc/manual/source/multi-user/roles.rst similarity index 100% rename from doc/manual/source/multi-user/authorization.rst rename to doc/manual/source/multi-user/roles.rst diff --git a/src/daemon/auth/providers/openid_connect/claims.rs b/src/daemon/auth/providers/openid_connect/claims.rs index da633d5d9..9cd5b1d48 100644 --- a/src/daemon/auth/providers/openid_connect/claims.rs +++ b/src/daemon/auth/providers/openid_connect/claims.rs @@ -1,19 +1,16 @@ //! Processing OpenID Connect claims. -use std::sync::Arc; use regex::{Regex, Replacer}; use serde::de::{Deserialize, Deserializer, Error as _}; use serde_json::{Number as JsonNumber, Value as JsonValue}; use crate::commons::KrillResult; use crate::commons::error::Error; use super::util::{FlexibleIdTokenClaims, FlexibleUserInfoClaims}; -use super::config::ConfigAuthOpenIDConnectClaim; //------------ Claims -------------------------------------------------------- pub struct Claims<'a> { - claims_conf: &'a [ConfigAuthOpenIDConnectClaim], id_token_claims: &'a FlexibleIdTokenClaims, user_info_claims: Option, @@ -25,30 +22,27 @@ pub struct Claims<'a> { impl<'a> Claims<'a> { pub fn new( - claims_conf: &'a [ConfigAuthOpenIDConnectClaim], id_token_claims: &'a FlexibleIdTokenClaims, user_info_claims: Option, ) -> Self { Self { - claims_conf, id_token_claims, user_info_claims, id_standard: None, id_additional: None, user_standard: None, user_additional: None, } } - pub fn extract_id(&mut self) -> KrillResult { - self.extract_claim("id") - } - - pub fn extract_role(&mut self) -> KrillResult> { - self.extract_claim("role").map(Into::into) - } - - fn extract_claim(&mut self, dest: &str) -> KrillResult { - for conf in self.claims_conf.iter().filter(|conf| conf.dest == dest) { - if let Some(res) = self.process_claim_conf(conf)? { - return Ok(res) + pub fn extract_claims( + &mut self, dest: &str, conf: &[TransformationRule], + ) -> KrillResult { + for rule in conf { + match rule { + TransformationRule::Fixed(subst) => return Ok(subst.clone()), + TransformationRule::Match(rule) => { + if let Some(res) = self.process_match_rule(rule)? { + return Ok(res) + } + } } } @@ -58,8 +52,8 @@ impl<'a> Claims<'a> { )) } - fn process_claim_conf( - &mut self, conf: &ConfigAuthOpenIDConnectClaim + fn process_match_rule( + &mut self, conf: &MatchRule, ) -> KrillResult> { use self::ClaimSource::*; @@ -174,7 +168,7 @@ impl<'a> Claims<'a> { } fn process_claim_json( - conf: &ConfigAuthOpenIDConnectClaim, + conf: &MatchRule, json: &JsonValue, ) -> KrillResult> { let object = match json { @@ -196,7 +190,7 @@ impl<'a> Claims<'a> { } fn process_claim_array( - conf: &ConfigAuthOpenIDConnectClaim, + conf: &MatchRule, array: &[JsonValue], ) -> KrillResult> { for item in array { @@ -223,14 +217,14 @@ impl<'a> Claims<'a> { } fn process_claim_number( - conf: &ConfigAuthOpenIDConnectClaim, + conf: &MatchRule, num: &JsonNumber ) -> KrillResult> { Self::process_claim_str(conf, &num.to_string()) } fn process_claim_str( - conf: &ConfigAuthOpenIDConnectClaim, + conf: &MatchRule, s: &str, ) -> KrillResult> { if let Some(expr) = conf.match_expr.as_ref() { @@ -299,6 +293,87 @@ impl<'a> Claims<'a> { } +//------------ TransformationRule -------------------------------------------- + +/// Transformation rule for a claim. +#[derive(Clone, Debug, Deserialize)] +#[serde(try_from = "TransformationRuleConf")] +pub enum TransformationRule { + /// Fixed rule. + /// + /// This rule matches always and returns the provided string. + Fixed(String), + + /// Matching rule. + /// + /// This rule tries to match the provided claim and optionally replaces + /// the value with the given subst expression. + /// + /// The rule matches string values, number and boolean values with their + /// JSON representation. It also matches arrays item by item with the + /// first match being used. + Match(MatchRule), +} + + +//------------ MatchRule ----------------------------------------------------- + +#[derive(Clone, Debug)] +pub struct MatchRule { + pub source: Option, + pub claim: String, + pub match_expr: Option, + pub subst: Option, +} + + +//------------ TransformationRuleConf ---------------------------------------- + +#[derive(Clone, Debug, Deserialize)] +pub struct TransformationRuleConf { + pub source: Option, + pub claim: Option, + #[serde(rename = "match")] + pub match_expr: Option, + pub subst: Option, +} + +impl TryFrom for TransformationRule { + type Error = String; + + fn try_from(src: TransformationRuleConf) -> Result { + if let Some(claim) = src.claim { + Ok(TransformationRule::Match(MatchRule { + source: src.source, + claim, + match_expr: src.match_expr, + subst: src.subst.map(Into::into) + })) + } + else { + let subst = match src.subst { + Some(subst) => subst, + None => { + return Err( + "'subst' is mandatory if 'claim' is missing".into() + ) + } + }; + + // Complain if we have 'match' to avoid possible errors. All + // the other things are probably fine. + if src.match_expr.is_some() { + return Err( + "'claim' is mandatory if 'match' is present".into() + ) + } + + Ok(TransformationRule::Fixed(subst)) + } + } +} + + //------------ MatchExpression ----------------------------------------------- #[derive(Clone, Debug)] @@ -323,13 +398,10 @@ pub struct SubstExpression { no_expansion: bool, } -impl<'de> Deserialize<'de> for SubstExpression { - fn deserialize>( - deserializer: D - ) -> Result { - let mut expr = String::deserialize(deserializer)?; +impl From for SubstExpression { + fn from(mut expr: String) -> Self { let no_expansion = expr.no_expansion().is_some(); - Ok(Self { expr, no_expansion }) + Self { expr, no_expansion } } } diff --git a/src/daemon/auth/providers/openid_connect/config.rs b/src/daemon/auth/providers/openid_connect/config.rs index fd1a21be8..2906c6919 100644 --- a/src/daemon/auth/providers/openid_connect/config.rs +++ b/src/daemon/auth/providers/openid_connect/config.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use serde::Deserialize; -use super::claims::{ClaimSource, MatchExpression, SubstExpression}; +use super::claims::{MatchRule, TransformationRule}; #[derive(Clone, Debug, Deserialize)] pub struct ConfigAuthOpenIDConnect { @@ -10,8 +10,11 @@ pub struct ConfigAuthOpenIDConnect { pub client_secret: String, - #[serde(default = "default_claims")] - pub claims: Vec, + #[serde(default = "default_id_claims")] + pub id_claims: Vec, + + #[serde(default = "default_role_claims")] + pub role_claims: Vec, #[serde(default)] pub extra_login_scopes: Vec, @@ -35,33 +38,25 @@ fn default_prompt_for_login() -> bool { true } - -#[derive(Clone, Debug, Deserialize)] -pub struct ConfigAuthOpenIDConnectClaim { - pub dest: String, - pub source: Option, - pub claim: String, - #[serde(rename = "match")] - pub match_expr: Option, - pub subst: Option, -} - -fn default_claims() -> Vec { +fn default_id_claims() -> Vec { vec![ - ConfigAuthOpenIDConnectClaim { - dest: "id".into(), + TransformationRule::Match(MatchRule { source: None, claim: "email".into(), match_expr: None, subst: None, - }, - ConfigAuthOpenIDConnectClaim { - dest: "id".into(), + }), + ] +} + +fn default_role_claims() -> Vec { + vec![ + TransformationRule::Match(MatchRule { source: None, claim: "role".into(), match_expr: None, subst: None, - }, + }), ] } diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index a0ff35c9b..be13c61d8 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -1659,11 +1659,14 @@ impl AuthProvider { // ========================================================================================== let mut claims = Claims::new( - &self.oidc_conf()?.claims, id_token_claims, user_info_claims ); - let id = claims.extract_id()?; - let role_name = claims.extract_role()?; + let id = claims.extract_claims( + "id", &self.oidc_conf()?.id_claims + )?; + let role_name = claims.extract_claims( + "role", &self.oidc_conf()?.role_claims + )?; let role = self.config.auth_roles.get( &role_name From 14fb7f19e17d0d6cd9554f4c0d5ebfde516ba8c8 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 6 Nov 2024 16:23:45 +0100 Subject: [PATCH 17/24] Minor manual fixes. --- doc/manual/source/multi-user.rst | 3 +- .../multi-user/config-file-provider.rst | 132 ++++++++---------- doc/manual/source/multi-user/roles.rst | 9 +- 3 files changed, 64 insertions(+), 80 deletions(-) diff --git a/doc/manual/source/multi-user.rst b/doc/manual/source/multi-user.rst index b799418ba..3bc4a73d2 100644 --- a/doc/manual/source/multi-user.rst +++ b/doc/manual/source/multi-user.rst @@ -31,10 +31,9 @@ Further reading: :maxdepth: 1 :name: toc-multi-user - multi-user/authorization + multi-user/roles multi-user/config-file-provider multi-user/openid-connect-provider - multi-user/customization .. history .. authors diff --git a/doc/manual/source/multi-user/config-file-provider.rst b/doc/manual/source/multi-user/config-file-provider.rst index c62e9ebd0..5c8f38f05 100644 --- a/doc/manual/source/multi-user/config-file-provider.rst +++ b/doc/manual/source/multi-user/config-file-provider.rst @@ -12,9 +12,10 @@ Config File Users Introduction ------------ -By setting ``auth_type = "config-file"`` in ``krill.conf`` you can configure Krill -to require users to enter a username and password in the web user interface when -logging in, rather than the secret token that is usually required: +By setting ``auth_type = "config-file"`` in ``krill.conf`` you can configure +Krill to require users to enter a username and password in the web user +interface when logging in, rather than the secret token that is usually +required: .. figure:: img/config-file-login.png :align: center @@ -23,45 +24,47 @@ logging in, rather than the secret token that is usually required: Using config file user credentials to login to Krill -.. Note:: It is important to realize that Krill is not a complete user management - system and that Config File Users therefore have some :ref:`limitations `. +.. Note:: It is important to realize that Krill is not a complete user + management system and that Config File Users therefore have some + :ref:`limitations `. - While Config File Users are useful as a quick way to test named user - support in Krill and may suffice for simple situations, in larger more - critical settings you are strongly advised to consider using - :ref:`doc_krill_multi_user_openid_connect_provider` instead. + While Config File Users are useful as a quick way to test named + user support in Krill and may suffice for simple situations, in + larger more critical settings you are strongly advised to consider + using :ref:`doc_krill_multi_user_openid_connect_provider` instead. How does it work? ----------------- To add a user to the ``krill.conf`` file an administrator uses the ``krillc`` -command to compute a password *hash* for the user and then adds an entry to the -``[auth_users]`` section including their username, password *hash*, salt and any -:ref:`attributes ` that are relevant for that -user. +command to compute a password *hash* for the user and then adds an entry to +the ``[auth_users]`` section including their username, password *hash*, salt +and :ref:`roll `. When a user enters their username and password into the web user interface a -hash of the password is computed and sent with the username to the Krill server. +hash of the password is computed and sent with the username to the Krill +server. The Krill server will verify that the user logging in provided a correct -password and has the ``LOGIN`` permission. On success Krill will respond with a -token which the web user interface should send on subsequent requests to -authenticate itself with Krill. The web user interface will keep a copy of this -token in browser local storage until the user logs out or is timed out due to -inactivity. - -.. tip:: The actual user password is **NEVER** stored on either the Krill server - nor the client browser and is **NEVER** sent by the client browser to - the Krill server. Only password *hashes* are stored and transmitted. - -.. warning:: Do **NOT** serve the Krill web user interface over unencrypted HTTP. - While the password is never transmitted, the authentication token - that the user is subsequently issued is subject to interception - by malicious parties if sent unencrypted from the Krill server to - the web user interface. Note that this is equally true when using - any credential to authenticate with Krill, whether secret token - or password hash or when Krill is configured to interact with an - OpenID Connect provider. +password and has a role that grants the ``login`` permission. On success +Krill will respond with a token which the web user interface should send on +subsequent requests to authenticate itself with Krill. The web user interface +will keep a copy of this token in browser local storage until the user logs +out or is timed out due to inactivity. + +.. tip:: The actual user password is **NEVER** stored on either the Krill + server nor the client browser and is **NEVER** sent by the client + browser to the Krill server. Only password *hashes* are stored and + transmitted. + +.. warning:: Do **NOT** serve the Krill web user interface over unencrypted + HTTP. While the password is never transmitted, the + authentication token that the user is subsequently issued is + subject to interception by malicious parties if sent unencrypted + from the Krill server to the web user interface. Note that this + is equally true when using any credential to authenticate with + Krill, whether secret token or password hash or when Krill is + configured to interact with an OpenID Connect provider. .. _limitations: @@ -69,24 +72,24 @@ Known limitations ----------------- Config File Users are easy to define and give you complete control over who -has access to your Krill instance and what level of access is granted. However, -Krill is not a complete user management system and so there are some things to -remember when using Config File Users: +has access to your Krill instance and what level of access is granted. +However, Krill is not a complete user management system and so there are some +things to remember when using Config File Users: - Krill has no feature for requiring a user to change their password on first login. As such, by issuing users with passwords you become responsible for delivering the new password to them securely. -- OpenID Connect providers often have support for one-time passwords (OTP) - or other secondary lines of defence to protect an account than just a - username and password. Krill does not have this capability. +- OpenID Connect providers often have support for two-factor authentication + to protect an account better than just with a username and password. Krill + does not have this capability. - Krill has no feature for generating cryptographically strong passwords. You are responsible for choosing sufficiently strong passwords for your users. -- Usernames, password hashes and user attributes are sensitive information. By - adding them to your ``krill.conf`` file you become responsible for protecting - them. +- Usernames, password hashes and user attributes are sensitive information. + By adding them to your ``krill.conf`` file you become responsible for + protecting them. - If you lose your ``krill.conf`` file you will also lose the password hashes and will have to reset your users passwords unless you have a (**secure**) @@ -95,10 +98,10 @@ remember when using Config File Users: - If a user forgets their password you will need to issue them with a new one. Krill does not offer a forgotten password or password reset feature. -- Adding or changing users requires a restart of Krill. There is no support in - Krill at present for reloading the user details while Krill is running. - While Krill is restarting the web user interface will be unavailable for your - users. +- Adding or changing users requires a restart of Krill. There is no support + in Krill at present for reloading the user details while Krill is running. + While Krill is restarting the web user interface will be unavailable for + your users. Setting it up ------------- @@ -108,7 +111,7 @@ The following steps are required to use Config File Users in your Krill setup. 1. Decide on the settings to be configured. """"""""""""""""""""""""""""""""""""""""""" -Decide which usernames you are going to configure, and what :ref:`role ` +Decide which usernames you are going to configure, and what :ref:`role ` and password they should have. For this example let's assume we want to configure the following users: @@ -143,9 +146,9 @@ to ``krill.conf``. The end result should look something like this: auth_type = "config-file" [auth_users] - "joe@example.com" = { attributes={ role="admin" }, password_hash="521e....0529", salt="d539....115e" } - "sally" = { attributes={ role="readonly" }, password_hash="...", salt="..." } - "dave_the_octopus" = { attributes={ role="readwrite" }, password_hash="...", salt="..." } + "joe@example.com" = { role"admin", password_hash="521e....0529", salt="d539....115e" } + "sally" = { role="readonly", password_hash="...", salt="..." } + "dave_the_octopus" = { role="readwrite", password_hash="...", salt="..." } ---- @@ -153,7 +156,8 @@ to ``krill.conf``. The end result should look something like this: """""" Restart Krill and deliver the chosen passwords to the respective users to -whom they belong. The users should now be able to login to your Krill instance. +whom they belong. The users should now be able to login to your Krill +instance. .. Warning:: Take whatever steps you think are necessary to ensure that the passwords are delivered **securely** to your users. @@ -162,30 +166,10 @@ Advanced configuration ---------------------- The information above gives you the basic structure for the configuration -file syntax needed to configure local users in Krill. - -See :ref:`doc_krill_multi_user_access_control` for information about -other user attributes and configuration settings that you might want to -use. - -See :ref:`doc_krill_multi_user_custom_policies` for information about -customizing the configuration even further. - -Below is a slightly modified version of the example above that also -uses the ``inc_cas``, ``exc_cas`` and ``auth_private_attributes`` features -and adds a user that has custom team attributes as well. Notice how the -team user does **NOT** have a ``role`` attribute! - -.. code-block:: bash - - auth_type = "config-file" - auth_private_attributes = [ "exc_cas" ] - - [auth_users] - "joe@example.com" = { attributes={ role="admin" }, password_hash="f45d...b25f", salt="..." } - "sally" = { attributes={ role="readonly", inc_cas="ca1,ca3" }, password_hash="...", salt="..." } - "dave_the_octopus" = { attributes={ role="readwrite" }, exc_cas="some_private_ca" }, password_hash="...", salt="..." } - "rob_from_team_one" = { attributes={ team="t1", teamrole="readwrite" }, password_hash="...", salt="..." } +file syntax needed to configure local users in Krill and uses the default +roles provided by Krill. See :ref:`doc_krill_multi_user_roles` for +information how to configure your own set of roles and limit what users +should have access to. Additional sources of information --------------------------------- diff --git a/doc/manual/source/multi-user/roles.rst b/doc/manual/source/multi-user/roles.rst index bb5c55599..b0d986ca6 100644 --- a/doc/manual/source/multi-user/roles.rst +++ b/doc/manual/source/multi-user/roles.rst @@ -1,4 +1,4 @@ -.. _doc_krill_multi_user_access_control: +.. _doc_krill_multi_user_roles: Roles, Permissions and Resources ================================ @@ -111,11 +111,12 @@ Currently, the following permissions are defined: ``bgpsec-update`` required to update the BGPsec router keys configured for a CA. -In addition, there two shortcuts that can be used to specify multiple -permission at ones: +In addition, there are two shortcuts that can be used to specify multiple +permission at once: .. Glossary:: - ``any` + + ``any`` grants all permissions, ``read`` From 5476b140c0f748c56d75b474f609b0e06ee083b3 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 6 Nov 2024 16:59:41 +0100 Subject: [PATCH 18/24] OpenID Connect manual update. --- .../multi-user/openid-connect-provider.rst | 322 ++++++++---------- 1 file changed, 141 insertions(+), 181 deletions(-) diff --git a/doc/manual/source/multi-user/openid-connect-provider.rst b/doc/manual/source/multi-user/openid-connect-provider.rst index ebfc72528..c2b9a7604 100644 --- a/doc/manual/source/multi-user/openid-connect-provider.rst +++ b/doc/manual/source/multi-user/openid-connect-provider.rst @@ -56,7 +56,8 @@ From the `OpenID Connect FAQ `_: users’ accounts for their own gain.* OpenID Connect takes the lessons learned from earlier identity protocols -and improves on them. It is `widely implemented `_ +and improves on them. It is +`widely implemented `_ and deployed, and for situations where the primary identity provider does not implement OpenID Connect there are OpenID Connect providers that can act as a bridge to systems that implement other identity protocols. @@ -89,8 +90,8 @@ these topics). The user experience """"""""""""""""""" -When an end user visits the Krill website in their browser they will be -redirected to the login page of the OpenID Connect provider. This is +When an end user visits the Krill user interface in their browser they will +be redirected to the login page of the OpenID Connect provider. This is **NOT** part of Krill. For example, when logging in to a Krill instance connected to the OpenID @@ -130,13 +131,15 @@ What the user doesn't see, except perhaps if their network connection is very slow, is that there are "hidden" intermediate steps occuring in the login flow, between the browser and Krill and between Krill and the OpenID Connect provider. These steps implement the OpenID Connect `"Authorizaton -Code Flow" `_. +Code Flow" +`_. If the user logged in correctly at the OpenID Connect provider login page and Krill was correctly registered with the provider and the provider was correctly setup for Krill, then Krill will receive a temporary Authorization -Code which it exchanges for an OAuth 2.0 `Access Token `_ -(and maybe also an OAuth 2.0 Refresh Token) and an OpenID Connect ID Token. +Code which it exchanges for an OAuth 2.0 `Access Token +`_ (and maybe also an +OAuth 2.0 Refresh Token) and an OpenID Connect ID Token. The ID Token includes so-called OAuth 2.0 **claims**, metadata about the user logging in. These claims are the key to whether or not Krill is able @@ -146,29 +149,41 @@ to login. Known limitations ----------------- -OpenID Connect Users avoid the problems with :ref:`Config File Users ` +OpenID Connect Users avoid the problems with :ref:`Config File Users +` but require more effort to setup and maintain: - Requires operating another service or using a 3rd party service. - Confguring Krill and the OpenID Connect provider is more involved than - setting up :ref:`Config File Users `. + setting up :ref:`Config File Users + `. - If Krill cannot contact the OpenID Connect provider, users will be unable to login to Krill with their OpenID Connect credentials. It will however still be possible to authenticate with Krill using its secret token. -.. warning:: If you encounter HTTP 502 Bad Gateway errors from your HTTP proxy - in front of Krill when logging in, or login loops where you are taken - back to the OpenID Connect provider login page but the Krill logs show - a successful login, you may need to increase the HTTP request and/or - response header buffer sizes used by your proxy. +.. warning:: If you encounter HTTP 502 Bad Gateway errors from your HTTP + proxy in front of Krill when logging in, or login loops where + you are taken back to the OpenID Connect provider login page but + the Krill logs show a successful login, you may need to increase + the HTTP request and/or response header buffer sizes used by + your proxy. - With NGINX this can be done by increasing settings such as `proxy_buffer_size `_, - `proxy_buffers `_, `large_client_header_buffers `_ (or `http2_max_field_size `_ and - `http2_max_header_size `_ - before NGINX v1.19.7). Thanks to GitHub user `racompton `_ for the ``large_client_header_buffers`` tip! - If using Kubernetes use the equivalent NGINX ingress controller ConfigMap - settings, e.g. `http2-max-field-size `_. Thanks to GitHub user `TheEnbyperor `_ for the HTTP/2 and Kubernetes tips! + With NGINX this can be done by increasing settings such as + `proxy_buffer_size `_, + `proxy_buffers `_, + `large_client_header_buffers `_ + (or `http2_max_field_size `_ + and `http2_max_header_size `_ + before NGINX v1.19.7). Thanks to GitHub user + `racompton `_ for the + ``large_client_header_buffers`` tip! + If using Kubernetes use the equivalent NGINX ingress controller + ConfigMap settings, e.g. + `http2-max-field-size `_. + Thanks to GitHub user + `TheEnbyperor `_ for the HTTP/2 + and Kubernetes tips! These issues occur because the size of the HTTP request & response headers on login to Krill when using OpenID Connect @@ -183,19 +198,30 @@ online services that you can create an account with. Any OpenID Connect provider that you choose must implement the following standards: -- `OpenID Connect Core 1.0 `_ -- `OpenID Connect Discovery 1.0 `_ -- `OpenID Connect RP-Initiated Logout 1.0 `_ *(optional)* -- `RFC 7009 OAuth 2.0 Token Revocation `_ *(optional)* - -Krill has been tested with the following OpenID Connect providers (in alphabetical order): - -- `Amazon Cognito `_ -- `Keycloak `_ -- `Microsoft Azure Active Directory `_ -- `Micro Focus NetIQ Access Manager 4.5 `_ - -.. warning:: Krill has been verified to be able to login and logout with `Google Cloud `_ +- `OpenID Connect Core 1.0 + `_ +- `OpenID Connect Discovery 1.0 + `_ +- `OpenID Connect RP-Initiated Logout 1.0 + `_ + *(optional)* +- `RFC 7009 OAuth 2.0 Token Revocation + `_ *(optional)* + +Krill has been tested with the following OpenID Connect providers (in +alphabetical order): + +- `Amazon Cognito + `_ +- `Keycloak + `_ +- `Microsoft Azure Active Directory + `_ +- `Micro Focus NetIQ Access Manager 4.5 + `_ + +.. warning:: Krill has been verified to be able to login and logout with + `Google Cloud `_ accounts. However, it is not advisable to grant access to Google accounts in general. Instead you should use a Google product that permits you to manage your own pool of @@ -246,7 +272,8 @@ steps must be taken: \ - - Is this property available by default as part of the `standard claims `_ + - Is this property available by default as part of the `standard claims + `_ sent by the provider to the client, or is it a provider specific claim or will it need to be configured in the provider as a custom claim? [1]_ @@ -257,10 +284,6 @@ steps must be taken: and `here `__), Amazon Cognito (`here `_) - - If no suitable claim values can be arranged with the provider, - consider using :ref:`hybrid mode ` instead. - - \ 2. **Gain access to the provider** @@ -294,7 +317,8 @@ steps must be taken: requests to other locations. .. [3] A correct URL will either end in /.well-known/openid-configuration - or should have that appended to it, e.g. the Google issuer URL is: https://accounts.google.com/.well-known/openid-configuration + or should have that appended to it, e.g. the Google issuer URL is: + https://accounts.google.com/.well-known/openid-configuration 4. **Create users, groups and/or claims in the provider** @@ -352,7 +376,8 @@ steps must be taken: Using Keycloak """""""""""""" -In this section you will see how to setup `Keycloak `__ +In this section you will see how to setup +`Keycloak `__ as an OpenID Connect provider for Krill. The following steps are required to use OpenID Connect Users in your Krill setup. @@ -374,9 +399,9 @@ sally sally@example.com wdGypnx5 readonly dave_the_octopus dave@example.com qnky8Zuj readwrite ================= ================= ========= ========= -And let's assume that we are going to use a local Docker `Keycloak `__ -container as our OpenID Connect provider which will be running at -https://localhost:8443/. +And let's assume that we are going to use a local Docker +`Keycloak `__ container as our OpenID Connect +provider which will be running at https://localhost:8443/. ---- @@ -445,7 +470,7 @@ Create a realm =================== ====================================== Field Value =================== ====================================== - Name `krill` + Name ``krill`` =================== ====================================== Create a client application @@ -461,7 +486,7 @@ Continuing in the KeyCloak web UI with realm set to `krill`: =================== ====================================== Field Value =================== ====================================== - Client ID `krill` + Client ID ``krill`` =================== ====================================== - On the `Settings` tab that is shown next set the field values as @@ -471,7 +496,7 @@ Continuing in the KeyCloak web UI with realm set to `krill`: Field Value =================== ====================================== Access Type `confidential` [4]_ - Valid Redirect URIs `https://localhost:3000/*` [5]_ + Valid Redirect URIs ``https://localhost:3000/*`` [5]_ =================== ====================================== - Generate credentials for Krill to use: @@ -644,17 +669,31 @@ data. The resulting claims look something like this: Source: https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample -Thus if you were to configure Krill to use the "given_name" claim -as the ID of the user in Krill, like so: +Krill uses claims to determine two things: the user ID – which is both +shown in the UI and logged in the Krill audit logs –, and the +:ref:`roll ` which determines access +permissions. + +For each rules can be defined in their own section, +``[[auth_openidconnect.id_claims]]`` for the user ID and +``[[auth_openidconnect.role_claims]]`` for the role. + +For instance, if you want to configure Krill to use the "given_name" claim +as the ID of the user in Krill, you can do this like so: .. code-block:: none - [auth_openidconnect.claims] - id = { jmespath="given_name" } + [[auth_openidconnect.id_claims]] + claim = "given_name" -Then in this example Krill would use the value "Jane" as the ID of the +Given the example claims above, would use the value "Jane" as the ID of the user logged in to Krill. +By default, Krill uses the value of the “email” claim as the user ID and +the value the “role” for the role. Given that the example claims above don’t +contain a “role” claim, Krill would reject a login with the defaults since +it doesn’t know what role to use. + Matching claims by name ~~~~~~~~~~~~~~~~~~~~~~~ @@ -667,26 +706,13 @@ This can be achieved using a config section that looks like this in .. code-block:: none - [auth_openidconnect.claims] - id = { jmespath="name" } + [[auth_openidconnect.id_claims]] + claim = "name" This tells Krill to search all of the claim data it receives for a field called `name` and use that as the ID for the user in Krill. This ID will also be logged in the Krill event history as the actor responsible for -any events that they caused.h - -What is JMESPath? According to `https://jmespath.org/ `_: - - *"JMESPath is a query language for JSON."* - -JSON is the format that OpenID Connect claim data is provided in by the -provider. JMESPath can therefore be used to tell Krill which particular -part from within the JSON it should use. - -This is a very trivial example of the power of JMESPath. You can find -out more about it at the `https://jmespath.org/ `_ -website and in ``krill.conf``. Krill comes with a couple of extensions -to JMESPath syntax which are also documented in ``krill.conf``. +any events that they caused. Matching claims by value ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -701,139 +727,73 @@ How do you tell Krill which users should have readonly access and which users should be have readwrite access? This is actually a real situation you can encounter with Azure Active -Directory. JMESPath can also be used to handle this scenario, albeit -with a much more complicated expression: +Directory. The rules in this case are a little more complicated: + .. code-block:: none - [auth_openidconnect.claims] - ro_role = { jmespath="resub(groups[?@ == 'gggggggg-gggg-gggg-gggg-gggggggggggg'] | [0], '^.+$', 'readonly')", dest="role" } - rw_role = { jmespath="resub(groups[?@ == 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'] | [0], '^.+$', 'readwrite')", dest="role" } - -Let's break the `ro_role` claim mapping rule down: - - - `gggg` and `hhhh` values represent the UUIDs of the groups to find in a - claim array called `groups`. - - The `resub` JMESPath function is a Krill extension to JMESPath that performs - regular expression based substitution. - - `groups[?@ == '...']` finds all entries in the `groups` array that match the - specified UUID. - - We then assume that there is only ever zero or one matches and just use the - first match `| [0]` found. - - Then we instruct Krill to take the entire value with `^.+$`. - - And to replace it with the value `readonly`. - - Finally, instead of assigning the value `readonly` to the user attribute - `ro_role`, `dest` is used to instead store `readonly` in a user attribute - called `role`. - -As `role` is the user attribute that the Krill authorization policy engine looks -at by default this will cause the user to be assigned the readonly role if their -user is a member of the group with the UUID value that represents the "readonly" -group! - -If we had only one rule we could write `role` on the left, but as we have two -rules that both try to provide a value for the same user attribute and the keys -on the left of the `=` must be unique, we use the `dest` trick to map any value -found to the `role` user attribute. + [[auth_openidconnect.role_claims]] + claim = "groups" + match = "^gggggggg-gggg-gggg-gggg-gggggggggggg$" + subst = "readonly" + + [[auth_openidconnect.role_claims]] + claim = "groups" + match = "^hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh$" + subst = "readwrite" + +We define two rules for the role claims. These are processed in order and +the first matching rule is used. Let’s break them down: + + - The ``claim`` field is the name of the claim to look for. In both cases + we are looking at the ``"groups"`` claim. + - The ``match`` field contains a regular expression matching the UUIDs of + the groups. Because regular expressions happily match partially, we need + the hat and dollar symbols to force a match of a complete value. + - The ``subst`` field contains a value to substitute the match with. While + you can refer to match groups in the regular expression, we don’t need + this here and just want to replace the value with the names of the roles. + +The ``"groups"`` claim is an array with multiple groups. Each rule will go +over all the values in the array and try and match them. Only if that doesn’t +succeed is the next rule tried. Thus, if a user has both the “g” group and +the “h” group, the first rule will apply and the user will be assigned the +``"readonly"`` role. It is important to keep this ordering in mind when +writing the configuration. Matching claims by partial value ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now imagine that the group membership is instead expressed not as array elements -that each exactly match some group name or UUID that we can look for, but that -each array element is a long string composed of `key=value` comma separated pairs. +Now imagine that the group membership is instead expressed not as array +elements that each exactly match some group name or UUID that we can look +for, but that each array element is a long string composed of `key=value` +comma separated pairs. -This can happen when the identity provider expresses group memberships in LDAP -X.500 format (see `RFC 2253 Lightweight Directory Access Protocol (v3): +This can happen when the identity provider expresses group memberships in +LDAP X.500 format (see `RFC 2253 Lightweight Directory Access Protocol (v3): UTF-8 String Representation of Distinguished Names `_). -For example you might see something like ``CN=Joe Bloggs,OU=NetworkTeam-Admins,DC=mycorp.com``, +For example you might see something like +``CN=Joe Bloggs,OU=NetworkTeam-Admins,DC=mycorp.com``, representing a user called Joe who is in the administrators group of the networking team of a company called mycorp.com. -Hopefully you'll only need simple rules but also equally hopefully if you need -more powerful matching Krill will be up to the task. For example, here's a more -complicated rule: - -.. code-block:: none - - dynamic_role = { jmespath="resub(memberof[?starts_with(@, 'CN=DL-Krill-')] | [0], '^CN=DL-Krill-(?P[^-,]+).+', '$role')" } - -This rule will match elements of an array called `memberof` whose value starts -with ``CN=DL-Krill-``, and wlll then extract just the part after that upto a -comma or dash, and will use that captured value as the Krill ``role`` user -attribute! - -.. _hybrid-mode: - -Matching claims to config values (aka 'hybrid' mode) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Usually when defining a claim mapping there is no need to define the source of -the claim. Krill will search all of the different OpenID Connect provider -claim sources that it supports (standard and additional claims in both the ID -Token and User Info responses) for a matching claim. - -However, if needed you can specify the claim source explicitly on a per claim -basis. Possible uses for this include: - - - Selecting the right claim when the same claim name exists in more than one - claim source but with different values. - - - Defining user attributes in the Krill configuration when the claim values - cannot be configured in the provider (perhaps due to lack of support by or - access to the provider). This is known as hybrid mode because it causes - Krill to use a hybrid of OpenID Connect provider for authentication and - config file defined user attributes for authorization. - -When defining a claim mapping we have so far seen ``jmespath`` and ``dest`` -settings, but there is also a ``source`` setting. The source can be set to one -of the following values: - - - ``config-file`` - - ``id-token-standard-claim`` - - ``id-token-additional-claim`` - - ``user-info-standard-claim`` - - ``user-info-additional-claim`` - -The first one is the really interesting one. The rest should hopefully never -be needed as by default Krill searches all of the possible OpenID Connect -provider claim sources that it supports. - -When using the ``config-file`` source there are two changes in the way that -Krill looks up the claim value: - - 1. The ``jmespath`` setting is not used. Instead an attribute with the - same name as the TOML key of the claim mapping is looked for on the - user. - - 2. The user attributes are taken from a config file entry with the ``id`` - of the current user is looked up in the ``[auth_users]`` config file - section. - -Note that the ``id`` of the current user is still determined by a normal -OpenID Connect claim lookup, i.e. by default the ``email`` value reported -by the provider for the user is used unless you define a claim mapping for -``id`` explicitly. - -For example, to identify users by the given name reported by the OpenID -Connect provider, and to set their role using entries in ``krill.conf`` -instead of basing the role on provider claim values, you could do something -like this: +Hopefully you'll only need simple rules but also equally hopefully if you +need more powerful matching Krill will be up to the task. For example, here's +a more complicated rule: .. code-block:: none - [auth_users] - "Joe Bloggs" = { attributes={ role="admin" } } - "Sally Alley" = { attributes={ role="readonly" } } + [[auth_openidconnect.role_claims]] + claim = "memberof" + match = "^CN=DL-Krill-(?P[^-,]+).+" + subst = "$role" - [auth_openidconnect.claims] - id = { jmespath="given_name" } - role = { source="config-file" } +This rule will match elements of an array called ``"memberof"`` whose value +starts with ``CN=DL-Krill-``, and wlll then extract just the part after that +upto a comma or dash, and will use that captured value as the role for the +user. -This will cause a user that logs in via the OpenID Connect provider who -has a ``given_name`` claim value of ``Joe Bloggs`` to be granted the -``admin`` role in Krill. Requesting missing claims ~~~~~~~~~~~~~~~~~~~~~~~~~ From 85c7b42acb11d5ee1aaf0bbade1115194cecda46 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 7 Nov 2024 14:52:06 +0100 Subject: [PATCH 19/24] Fix the user information sent to the UI after login. --- src/daemon/auth/authorizer.rs | 36 +++++++++++++++++-- src/daemon/auth/providers/admin_token.rs | 9 ++--- src/daemon/auth/providers/config_file.rs | 11 +++--- .../auth/providers/openid_connect/claims.rs | 21 +++++------ .../auth/providers/openid_connect/provider.rs | 4 +-- src/daemon/http/auth.rs | 4 +-- 6 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 89cbc2cfd..95643db5e 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -309,11 +309,41 @@ impl Authorizer { #[derive(Serialize, Debug)] pub struct LoggedInUser { /// The API token to use in subsequent calls. - pub token: Token, + token: Token, /// The user ID. - // XXX Swith to using Arc. May require Serialize shenanigans. - pub id: String, + id: Arc, + + /// The user attributes. + /// + /// This used to be a hash map with values decided upon by the auth + /// provider but we now only and always have a role attribute. However, + /// in order to serialize into the JSON expected by the UI, this still + /// needs to be a struct. + attributes: LoggedInUserAttributes, +} + +#[derive(Serialize, Debug)] +pub struct LoggedInUserAttributes { + role: Arc, +} + +impl LoggedInUser { + pub fn new(token: Token, id: Arc, role: Arc) -> Self { + LoggedInUser { + token, + id, + attributes: LoggedInUserAttributes { role } + } + } + + pub fn token(&self) -> &Token { + &self.token + } + + pub fn id(&self) -> &str { + &self.id + } } diff --git a/src/daemon/auth/providers/admin_token.rs b/src/daemon/auth/providers/admin_token.rs index 7f2faf92a..da5ef7d3e 100644 --- a/src/daemon/auth/providers/admin_token.rs +++ b/src/daemon/auth/providers/admin_token.rs @@ -87,10 +87,11 @@ impl AuthProvider { /// Establishes a client session from credentials in an HTTP request. pub fn login(&self, request: &HyperRequest) -> KrillResult { match self.authenticate(request)? { - Some(_actor) => Ok(LoggedInUser { - token: self.required_token.clone(), - id: self.user_id.as_ref().into(), - }), + Some(_actor) => Ok(LoggedInUser::new( + self.required_token.clone(), + self.user_id.as_ref().into(), + "admin".into(), + )), None => Err(Error::ApiInvalidCredentials( "Missing bearer token".to_string(), )), diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index d8c327c80..0c2cac43e 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -83,7 +83,7 @@ impl AuthProvider { let (username, password) = auth.split_once(':')?; Some(Auth { - username: username.to_string(), + username: username.to_string().into(), password: password.to_string(), }) } @@ -247,18 +247,17 @@ impl AuthProvider { return Err(Error::ApiInsufficientRights(reason)); } + let username = Arc::::from(username); + // All good: create a token and return. let api_token = self.session_cache.encode( - username.clone().into(), + username.clone(), SessionSecret { role: user.role.clone() }, &self.session_key, None, )?; - Ok(LoggedInUser { - token: api_token, - id: username, - }) + Ok(LoggedInUser::new(api_token, username, user.role.clone())) } pub fn logout( diff --git a/src/daemon/auth/providers/openid_connect/claims.rs b/src/daemon/auth/providers/openid_connect/claims.rs index 9cd5b1d48..d8d4199a5 100644 --- a/src/daemon/auth/providers/openid_connect/claims.rs +++ b/src/daemon/auth/providers/openid_connect/claims.rs @@ -1,5 +1,6 @@ //! Processing OpenID Connect claims. +use std::sync::Arc; use regex::{Regex, Replacer}; use serde::de::{Deserialize, Deserializer, Error as _}; use serde_json::{Number as JsonNumber, Value as JsonValue}; @@ -34,7 +35,7 @@ impl<'a> Claims<'a> { pub fn extract_claims( &mut self, dest: &str, conf: &[TransformationRule], - ) -> KrillResult { + ) -> KrillResult> { for rule in conf { match rule { TransformationRule::Fixed(subst) => return Ok(subst.clone()), @@ -54,7 +55,7 @@ impl<'a> Claims<'a> { fn process_match_rule( &mut self, conf: &MatchRule, - ) -> KrillResult> { + ) -> KrillResult>> { use self::ClaimSource::*; match conf.source { @@ -170,7 +171,7 @@ impl<'a> Claims<'a> { fn process_claim_json( conf: &MatchRule, json: &JsonValue, - ) -> KrillResult> { + ) -> KrillResult>> { let object = match json { JsonValue::Object(object) => object, _ => return Ok(None) @@ -192,7 +193,7 @@ impl<'a> Claims<'a> { fn process_claim_array( conf: &MatchRule, array: &[JsonValue], - ) -> KrillResult> { + ) -> KrillResult>> { for item in array { let res = match item { JsonValue::Bool(true) => { @@ -219,14 +220,14 @@ impl<'a> Claims<'a> { fn process_claim_number( conf: &MatchRule, num: &JsonNumber - ) -> KrillResult> { + ) -> KrillResult>> { Self::process_claim_str(conf, &num.to_string()) } fn process_claim_str( conf: &MatchRule, s: &str, - ) -> KrillResult> { + ) -> KrillResult>> { if let Some(expr) = conf.match_expr.as_ref() { match conf.subst.as_ref() { Some(subst) => { @@ -237,7 +238,7 @@ impl<'a> Claims<'a> { res.push_str(&s[..m.start()]); res.push_str(&subst.expr); res.push_str(&s[m.end()..]); - Ok(Some(res)) + Ok(Some(res.into())) } None => Ok(None) } @@ -249,7 +250,7 @@ impl<'a> Claims<'a> { subst.expr.len() ); c.expand(&subst.expr, &mut res); - Ok(Some(res)) + Ok(Some(res.into())) } None => Ok(None) } @@ -302,7 +303,7 @@ pub enum TransformationRule { /// Fixed rule. /// /// This rule matches always and returns the provided string. - Fixed(String), + Fixed(Arc), /// Matching rule. /// @@ -368,7 +369,7 @@ impl TryFrom for TransformationRule { ) } - Ok(TransformationRule::Fixed(subst)) + Ok(TransformationRule::Fixed(subst.into())) } } } diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index be13c61d8..1cb8bfff0 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -1719,12 +1719,12 @@ impl AuthProvider { // ========================================================================================== let token = self.session_cache.encode( id.clone().into(), - SessionSecrets::new(role_name, &token_response), + SessionSecrets::new(role_name.clone(), &token_response), &self.session_key, token_response.expires_in(), )?; - Ok(LoggedInUser { token, id, }) + Ok(LoggedInUser::new(token, id, role_name)) } None => Err(Error::ApiInvalidCredentials( diff --git a/src/daemon/http/auth.rs b/src/daemon/http/auth.rs index 1da0b42ba..809150386 100644 --- a/src/daemon/http/auth.rs +++ b/src/daemon/http/auth.rs @@ -26,8 +26,8 @@ pub fn url_encode>(s: S) -> Result { fn build_auth_redirect_location(user: LoggedInUser) -> Result { Ok(format!( "/ui/login?token={}&id={}", - &url_encode(user.token)?, - &url_encode(user.id.as_str())?, + &url_encode(user.token())?, + &url_encode(user.id())?, )) } From f87ecf9308d0095fb3b3fffd851fd9d2cc32c039 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 7 Nov 2024 14:57:29 +0100 Subject: [PATCH 20/24] Remove unnecessary .into(). --- src/daemon/auth/providers/config_file.rs | 2 +- src/daemon/auth/providers/openid_connect/provider.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/daemon/auth/providers/config_file.rs b/src/daemon/auth/providers/config_file.rs index 0c2cac43e..935a9824b 100644 --- a/src/daemon/auth/providers/config_file.rs +++ b/src/daemon/auth/providers/config_file.rs @@ -83,7 +83,7 @@ impl AuthProvider { let (username, password) = auth.split_once(':')?; Some(Auth { - username: username.to_string().into(), + username: username.to_string(), password: password.to_string(), }) } diff --git a/src/daemon/auth/providers/openid_connect/provider.rs b/src/daemon/auth/providers/openid_connect/provider.rs index 1cb8bfff0..2720300b3 100644 --- a/src/daemon/auth/providers/openid_connect/provider.rs +++ b/src/daemon/auth/providers/openid_connect/provider.rs @@ -1718,7 +1718,7 @@ impl AuthProvider { // after that much time would also fail. // ========================================================================================== let token = self.session_cache.encode( - id.clone().into(), + id.clone(), SessionSecrets::new(role_name.clone(), &token_response), &self.session_key, token_response.expires_in(), From d1961e07cd84c569ee2e244ffd4a860b453916a4 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Mon, 11 Nov 2024 14:27:32 +0100 Subject: [PATCH 21/24] Include attributes also in redirect URI. --- src/daemon/auth/authorizer.rs | 10 +++++++++- src/daemon/auth/permission.rs | 2 ++ src/daemon/http/auth.rs | 22 ++++++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/daemon/auth/authorizer.rs b/src/daemon/auth/authorizer.rs index 95643db5e..9f443de8d 100644 --- a/src/daemon/auth/authorizer.rs +++ b/src/daemon/auth/authorizer.rs @@ -272,7 +272,7 @@ impl Authorizer { if log_enabled!(log::Level::Trace) { trace!("User logged in: {:?}", &user); } else { - info!("User logged in: {}", &user.id); + info!("User logged in: {}, role: {}", user.id(), user.role()); } Ok(user) @@ -344,6 +344,14 @@ impl LoggedInUser { pub fn id(&self) -> &str { &self.id } + + pub fn role(&self) -> &str { + self.attributes.role.as_ref() + } + + pub fn attributes(&self) -> &impl Serialize { + &self.attributes + } } diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index 22881e7b2..c1f165d02 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -197,6 +197,7 @@ mod policy { pub const NONE: Self = Self(0); pub const READONLY: Self = Self::from_permissions(&[ + Login, CaList, CaRead, PubList, @@ -210,6 +211,7 @@ mod policy { ]); pub const READWRITE: Self = Self::from_permissions(&[ + Login, CaList, CaRead, CaCreate, diff --git a/src/daemon/http/auth.rs b/src/daemon/http/auth.rs index 809150386..e2f91230a 100644 --- a/src/daemon/http/auth.rs +++ b/src/daemon/http/auth.rs @@ -8,7 +8,8 @@ use crate::{ #[cfg(feature = "multi-user")] use { crate::daemon::{ - auth::LoggedInUser, http::server::render_error_redirect, + auth::LoggedInUser, + http::server::render_error_redirect, }, urlparse::quote, }; @@ -24,10 +25,27 @@ pub fn url_encode>(s: S) -> Result { #[cfg(feature = "multi-user")] fn build_auth_redirect_location(user: LoggedInUser) -> Result { + fn b64_encode_attributes_with_mapped_error( + a: &impl serde::Serialize, + ) -> Result { + use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; + use base64::engine::Engine as _; + + Ok(BASE64_ENGINE.encode( + serde_json::to_string(a) + .map_err(|err| Error::custom(err.to_string()))?, + )) + } + + let attributes = b64_encode_attributes_with_mapped_error( + user.attributes() + )?; + Ok(format!( - "/ui/login?token={}&id={}", + "/ui/login?token={}&id={}&attributes={}", &url_encode(user.token())?, &url_encode(user.id())?, + &url_encode(attributes)?, )) } From 43d6ab551147c854ae642f778c03e135432e012d Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 13 Nov 2024 11:44:59 +0100 Subject: [PATCH 22/24] Deserialize permission from String instead of str. --- src/daemon/auth/permission.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index c1f165d02..e89808a54 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -93,7 +93,7 @@ define_permission! { /// permissions `"list"`, `"read"`, `"create"`, `"delete"`, and `"admin"` /// which include all the respective permissions for all components. #[derive(Clone, Copy, Debug, Deserialize)] -#[serde(try_from = "&str")] +#[serde(try_from = "String")] pub enum ConfPermission { Single(Permission), Any, @@ -115,15 +115,15 @@ impl ConfPermission { } } -impl<'a> TryFrom<&'a str> for ConfPermission { +impl<'a> TryFrom for ConfPermission { type Error = String; - fn try_from(src: &'a str) -> Result { - if let Ok(res) = Permission::from_str(src) { + fn try_from(src: String) -> Result { + if let Ok(res) = Permission::from_str(&src) { return Ok(Self::Single(res)) } - match src { + match src.as_str() { "any" => Ok(Self::Any), "read" => Ok(Self::Read), "update" => Ok(Self::Update), @@ -242,7 +242,11 @@ mod policy { ]); pub const CONF_READ: Self = Self::from_permissions(&[ - CaRead, RoutesRead, AspasRead, BgpsecRead, RtaRead, + CaRead, + RoutesRead, RoutesAnalysis, + AspasRead, + BgpsecRead, + RtaRead, ]); pub const CONF_CREATE: Self = Self::from_permissions(&[ From 2eaf35652f9fa0b24cb104563c1b0e14327dc940 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 13 Nov 2024 11:51:47 +0100 Subject: [PATCH 23/24] Minor fixes. --- doc/manual/source/multi-user/config-file-provider.rst | 2 +- doc/manual/source/multi-user/roles.rst | 9 +++++---- src/daemon/auth/providers/openid_connect/claims.rs | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/manual/source/multi-user/config-file-provider.rst b/doc/manual/source/multi-user/config-file-provider.rst index 5c8f38f05..e61c96faf 100644 --- a/doc/manual/source/multi-user/config-file-provider.rst +++ b/doc/manual/source/multi-user/config-file-provider.rst @@ -146,7 +146,7 @@ to ``krill.conf``. The end result should look something like this: auth_type = "config-file" [auth_users] - "joe@example.com" = { role"admin", password_hash="521e....0529", salt="d539....115e" } + "joe@example.com" = { role="admin", password_hash="521e....0529", salt="d539....115e" } "sally" = { role="readonly", password_hash="...", salt="..." } "dave_the_octopus" = { role="readwrite", password_hash="...", salt="..." } diff --git a/doc/manual/source/multi-user/roles.rst b/doc/manual/source/multi-user/roles.rst index b0d986ca6..3a500aa08 100644 --- a/doc/manual/source/multi-user/roles.rst +++ b/doc/manual/source/multi-user/roles.rst @@ -50,7 +50,8 @@ Currently, the following permissions are defined: .. Glossary:: ``login`` - required for logging into the Krill UI, + required for logging into the Krill UI and for accessing any + resources, ``pub-admin`` required for access to the built-in publication server, @@ -151,7 +152,7 @@ role that only allows read access to the ``"example"`` CA. [auth_roles] "admin" = { permissions = [ "any" ] } - "readwrite" = { permissions = [ "pub-list", "pub-read", "pub-create", "pub-delete", "ca-list", "ca-create", "ca-delete", "read", "update" ] } - "readonly" = { permissions = [ "pub-read", "ca-list", "read" ] } - "read-example" = { permissions = [ "read" ], cas = [ "example" ] } + "readwrite" = { permissions = [ "login", "pub-list", "pub-read", "pub-create", "pub-delete", "ca-list", "ca-create", "ca-delete", "read", "update" ] } + "readonly" = { permissions = [ "login", "pub-read", "ca-list", "read" ] } + "read-example" = { permissions = [ "login", "read" ], cas = [ "example" ] } diff --git a/src/daemon/auth/providers/openid_connect/claims.rs b/src/daemon/auth/providers/openid_connect/claims.rs index d8d4199a5..7409597ff 100644 --- a/src/daemon/auth/providers/openid_connect/claims.rs +++ b/src/daemon/auth/providers/openid_connect/claims.rs @@ -11,6 +11,7 @@ use super::util::{FlexibleIdTokenClaims, FlexibleUserInfoClaims}; //------------ Claims -------------------------------------------------------- +#[derive(Debug)] pub struct Claims<'a> { id_token_claims: &'a FlexibleIdTokenClaims, user_info_claims: Option, From 9f0fabac95d93ff7f07189a2232d1d4095f65d34 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 13 Nov 2024 11:54:17 +0100 Subject: [PATCH 24/24] Remove unnecessary things. --- src/daemon/auth/permission.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/daemon/auth/permission.rs b/src/daemon/auth/permission.rs index e89808a54..0140ff745 100644 --- a/src/daemon/auth/permission.rs +++ b/src/daemon/auth/permission.rs @@ -115,7 +115,7 @@ impl ConfPermission { } } -impl<'a> TryFrom for ConfPermission { +impl TryFrom for ConfPermission { type Error = String; fn try_from(src: String) -> Result { @@ -249,17 +249,9 @@ mod policy { RtaRead, ]); - pub const CONF_CREATE: Self = Self::from_permissions(&[ - CaCreate, - ]); - pub const CONF_UPDATE: Self = Self::from_permissions(&[ RoutesUpdate, BgpsecUpdate, RtaUpdate, ]); - - pub const CONF_DELETE: Self = Self::from_permissions(&[ - PubDelete, CaDelete, - ]); } }