diff --git a/src/ic_oss_bucket/src/store.rs b/src/ic_oss_bucket/src/store.rs index 70f60dd..8c8be60 100644 --- a/src/ic_oss_bucket/src/store.rs +++ b/src/ic_oss_bucket/src/store.rs @@ -84,7 +84,7 @@ impl Bucket { ) .map_err(|err| (401, err))?; if &token.subject == caller && &token.audience == canister { - return Policies::try_from(token.scope.as_str()).map_err(|err| (403u16, err)); + return Policies::try_from(token.policies.as_str()).map_err(|err| (403u16, err)); } } @@ -116,7 +116,7 @@ impl Bucket { ) .map_err(|err| (401, err))?; if &token.subject == caller && &token.audience == canister { - return Policies::try_from(token.scope.as_str()).map_err(|err| (403u16, err)); + return Policies::try_from(token.policies.as_str()).map_err(|err| (403u16, err)); } } diff --git a/src/ic_oss_cluster/ic_oss_cluster.did b/src/ic_oss_cluster/ic_oss_cluster.did index f8cebf5..1ce9191 100644 --- a/src/ic_oss_cluster/ic_oss_cluster.did +++ b/src/ic_oss_cluster/ic_oss_cluster.did @@ -1,7 +1,7 @@ type ChainArgs = variant { Upgrade : record {}; Init : InitArgs }; type InitArgs = record { ecdsa_key_name : text; token_expiration : nat64 }; -type Result = variant { Ok; Err : text }; -type Result_1 = variant { Ok : blob; Err : text }; +type Result = variant { Ok : blob; Err : text }; +type Result_1 = variant { Ok; Err : text }; type Result_2 = variant { Ok : State; Err }; type State = record { ecdsa_token_public_key : text; @@ -9,10 +9,17 @@ type State = record { managers : vec principal; token_expiration : nat64; }; -type Token = record { subject : principal; audience : principal; scope : text }; +type Token = record { + subject : principal; + audience : principal; + policies : text; +}; service : (opt ChainArgs) -> { - admin_set_managers : (vec principal) -> (Result); - admin_sign_access_token : (Token) -> (Result_1); + access_token : (principal) -> (Result); + admin_attach_policies : (Token) -> (Result_1); + admin_detach_policies : (Token) -> (Result_1); + admin_set_managers : (vec principal) -> (Result_1); + admin_sign_access_token : (Token) -> (Result); get_state : (opt blob) -> (Result_2) query; - validate_admin_set_managers : (vec principal) -> (Result) query; + validate_admin_set_managers : (vec principal) -> (Result_1) query; } diff --git a/src/ic_oss_cluster/src/api_admin.rs b/src/ic_oss_cluster/src/api_admin.rs index 5bf8c74..701f865 100644 --- a/src/ic_oss_cluster/src/api_admin.rs +++ b/src/ic_oss_cluster/src/api_admin.rs @@ -1,6 +1,9 @@ use candid::Principal; use coset::CborSerializable; -use ic_oss_types::cwt::{cose_sign1, sha256, Token, BUCKET_TOKEN_AAD, CLUSTER_TOKEN_AAD, ES256K}; +use ic_oss_types::{ + cwt::{cose_sign1, sha256, Token, BUCKET_TOKEN_AAD, CLUSTER_TOKEN_AAD, ES256K}, + permission::Policies, +}; use serde_bytes::ByteBuf; use std::collections::BTreeSet; @@ -51,3 +54,25 @@ async fn admin_sign_access_token(token: Token) -> Result { let token = sign1.to_vec().map_err(|err| err.to_string())?; Ok(ByteBuf::from(token)) } + +#[ic_cdk::update] +async fn admin_attach_policies(args: Token) -> Result<(), String> { + if !store::state::is_manager(&ic_cdk::caller()) { + Err("user is not a manager".to_string())?; + } + + let policies = Policies::try_from(args.policies.as_str())?; + store::auth::attach_policies(args.subject, args.audience, policies); + Ok(()) +} + +#[ic_cdk::update] +async fn admin_detach_policies(args: Token) -> Result<(), String> { + if !store::state::is_manager(&ic_cdk::caller()) { + Err("user is not a manager".to_string())?; + } + + let policies = Policies::try_from(args.policies.as_str())?; + store::auth::detach_policies(args.subject, args.audience, policies); + Ok(()) +} diff --git a/src/ic_oss_cluster/src/api_auth.rs b/src/ic_oss_cluster/src/api_auth.rs new file mode 100644 index 0000000..0d27243 --- /dev/null +++ b/src/ic_oss_cluster/src/api_auth.rs @@ -0,0 +1,46 @@ +use candid::Principal; +use coset::CborSerializable; +use ic_oss_types::cwt::{cose_sign1, sha256, Token, BUCKET_TOKEN_AAD, CLUSTER_TOKEN_AAD, ES256K}; +use serde_bytes::ByteBuf; + +use crate::{ecdsa, store, SECONDS}; + +#[ic_cdk::update] +async fn access_token(audience: Principal) -> Result { + if !store::state::is_manager(&ic_cdk::caller()) { + Err("user is not a manager".to_string())?; + } + let subject = ic_cdk::caller(); + + match store::auth::get_all_policies(&subject) { + None => Err("no policies found".to_string()), + Some(pt) => { + let policies = pt.0.get(&audience).ok_or("no policies found")?; + let token = Token { + subject, + audience, + policies: policies.to_owned(), + }; + + let now_sec = ic_cdk::api::time() / SECONDS; + let (ecdsa_key_name, token_expiration) = + store::state::with(|r| (r.ecdsa_key_name.clone(), r.token_expiration)); + + let mut claims = token.to_cwt(now_sec as i64, token_expiration as i64); + claims.issuer = Some(ic_cdk::id().to_text()); + let mut sign1 = cose_sign1(claims, ES256K, None)?; + let tbs_data = sign1.tbs_data(BUCKET_TOKEN_AAD); + let message_hash = sha256(&tbs_data); + + let sig = ecdsa::sign_with( + &ecdsa_key_name, + vec![CLUSTER_TOKEN_AAD.to_vec()], + message_hash, + ) + .await?; + sign1.signature = sig; + let token = sign1.to_vec().map_err(|err| err.to_string())?; + Ok(ByteBuf::from(token)) + } + } +} diff --git a/src/ic_oss_cluster/src/lib.rs b/src/ic_oss_cluster/src/lib.rs index 9b704af..08b8567 100644 --- a/src/ic_oss_cluster/src/lib.rs +++ b/src/ic_oss_cluster/src/lib.rs @@ -4,6 +4,7 @@ use serde_bytes::ByteBuf; use std::collections::BTreeSet; mod api_admin; +mod api_auth; mod ecdsa; mod init; mod store; diff --git a/src/ic_oss_cluster/src/store.rs b/src/ic_oss_cluster/src/store.rs index 1aafba2..dd63ff5 100644 --- a/src/ic_oss_cluster/src/store.rs +++ b/src/ic_oss_cluster/src/store.rs @@ -1,13 +1,17 @@ use candid::{CandidType, Principal}; use ciborium::{from_reader, into_writer}; -use ic_oss_types::cwt::CLUSTER_TOKEN_AAD; +use ic_oss_types::{cwt::CLUSTER_TOKEN_AAD, permission::Policies}; use ic_stable_structures::{ memory_manager::{MemoryId, MemoryManager, VirtualMemory}, storable::Bound, - DefaultMemoryImpl, StableCell, Storable, + DefaultMemoryImpl, StableBTreeMap, StableCell, Storable, }; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, cell::RefCell, collections::BTreeSet}; +use std::{ + borrow::Cow, + cell::RefCell, + collections::{BTreeMap, BTreeSet}, +}; use crate::ecdsa; @@ -35,7 +39,46 @@ impl Storable for State { } } +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct PoliciesTable(pub BTreeMap); + +impl PoliciesTable { + pub fn attach(&mut self, audience: Principal, mut policies: Policies) { + self.0 + .entry(audience) + .and_modify(|e| { + let mut p = Policies::try_from(e.as_str()).expect("failed to parse policies"); + p.append(&mut policies); + *e = p.to_string(); + }) + .or_insert_with(|| policies.to_string()); + } + + pub fn detach(&mut self, audience: Principal, policies: Policies) { + self.0.entry(audience).and_modify(|e| { + let mut p = Policies::try_from(e.as_str()).expect("failed to parse policies"); + p.remove(&policies); + *e = p.to_string(); + }); + } +} + +impl Storable for PoliciesTable { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode Policies data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode Policies data") + } +} + const STATE_MEMORY_ID: MemoryId = MemoryId::new(0); +const AUTH_MEMORY_ID: MemoryId = MemoryId::new(0); thread_local! { static STATE: RefCell = RefCell::new(State::default()); @@ -50,6 +93,12 @@ thread_local! { ).expect("failed to init STATE store") ); + static AUTH_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(AUTH_MEMORY_ID)), + ) + ); + } pub mod state { @@ -107,3 +156,34 @@ pub mod state { }); } } + +pub mod auth { + use super::*; + + pub fn get_all_policies(subject: &Principal) -> Option { + AUTH_STORE.with(|r| r.borrow().get(subject)) + } + + pub fn attach_policies(subject: Principal, audience: Principal, policies: Policies) { + AUTH_STORE.with(|r| { + let mut m = r.borrow_mut(); + let mut pt = m.get(&subject).unwrap_or_default(); + pt.attach(audience, policies); + m.insert(subject, pt); + }); + } + + pub fn detach_policies(subject: Principal, audience: Principal, policies: Policies) { + AUTH_STORE.with(|r| { + let mut m = r.borrow_mut(); + if let Some(mut pt) = m.get(&subject) { + pt.detach(audience, policies); + if pt.0.is_empty() { + m.remove(&subject); + } else { + m.insert(subject, pt); + } + } + }); + } +} diff --git a/src/ic_oss_types/src/cwt.rs b/src/ic_oss_types/src/cwt.rs index 676dda6..1ea0c10 100644 --- a/src/ic_oss_types/src/cwt.rs +++ b/src/ic_oss_types/src/cwt.rs @@ -26,7 +26,7 @@ pub static CLUSTER_TOKEN_AAD: &[u8] = b"ic_oss_cluster"; pub struct Token { pub subject: Principal, pub audience: Principal, - pub scope: String, + pub policies: String, } impl Token { @@ -64,7 +64,7 @@ impl Token { not_before: Some(Timestamp::WholeSeconds(now_sec)), issued_at: Some(Timestamp::WholeSeconds(now_sec)), cwt_id: None, - rest: vec![(SCOPE_NAME.clone(), self.scope.into())], + rest: vec![(SCOPE_NAME.clone(), self.policies.into())], } } @@ -172,7 +172,7 @@ impl TryFrom for Token { .map_err(|err| format!("invalid subject: {}", err))?, audience: Principal::from_text(claims.audience.as_ref().ok_or("missing audience")?) .map_err(|err| format!("invalid audience: {}", err))?, - scope: scope.to_string(), + policies: scope.to_string(), }) } } @@ -219,7 +219,7 @@ mod test { ) .unwrap(), audience: Principal::from_text("mmrxu-fqaaa-aaaap-ahhna-cai").unwrap(), - scope: ps.to_string(), + policies: ps.to_string(), }; println!("token: {:?}", &token); diff --git a/src/ic_oss_types/src/permission.rs b/src/ic_oss_types/src/permission.rs index ca532d9..28d16a6 100644 --- a/src/ic_oss_types/src/permission.rs +++ b/src/ic_oss_types/src/permission.rs @@ -367,6 +367,14 @@ impl Policies { pub fn all() -> Self { Self(BTreeSet::from([Policy::default()])) } + + pub fn append(&mut self, policies: &mut Policies) { + self.0.append(&mut policies.0); + } + + pub fn remove(&mut self, policies: &Policies) { + self.0.retain(|p| !policies.0.contains(p)); + } } impl Deref for Policies {