Skip to content

Commit

Permalink
feat: implement auth API
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jul 12, 2024
1 parent 65e4eb6 commit eb10760
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/ic_oss_bucket/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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));
}
}

Expand Down
19 changes: 13 additions & 6 deletions src/ic_oss_cluster/ic_oss_cluster.did
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
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;
ecdsa_key_name : text;
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;
}
27 changes: 26 additions & 1 deletion src/ic_oss_cluster/src/api_admin.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -51,3 +54,25 @@ async fn admin_sign_access_token(token: Token) -> Result<ByteBuf, String> {
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(())
}
46 changes: 46 additions & 0 deletions src/ic_oss_cluster/src/api_auth.rs
Original file line number Diff line number Diff line change
@@ -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<ByteBuf, String> {
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))
}
}
}
1 change: 1 addition & 0 deletions src/ic_oss_cluster/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde_bytes::ByteBuf;
use std::collections::BTreeSet;

mod api_admin;
mod api_auth;
mod ecdsa;
mod init;
mod store;
Expand Down
86 changes: 83 additions & 3 deletions src/ic_oss_cluster/src/store.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -35,7 +39,46 @@ impl Storable for State {
}
}

#[derive(Clone, Default, Deserialize, Serialize)]
pub struct PoliciesTable(pub BTreeMap<Principal, String>);

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<State> = RefCell::new(State::default());
Expand All @@ -50,6 +93,12 @@ thread_local! {
).expect("failed to init STATE store")
);

static AUTH_STORE: RefCell<StableBTreeMap<Principal, PoliciesTable, Memory>> = RefCell::new(
StableBTreeMap::init(
MEMORY_MANAGER.with_borrow(|m| m.get(AUTH_MEMORY_ID)),
)
);

}

pub mod state {
Expand Down Expand Up @@ -107,3 +156,34 @@ pub mod state {
});
}
}

pub mod auth {
use super::*;

pub fn get_all_policies(subject: &Principal) -> Option<PoliciesTable> {
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);
}
}
});
}
}
8 changes: 4 additions & 4 deletions src/ic_oss_types/src/cwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())],
}
}

Expand Down Expand Up @@ -172,7 +172,7 @@ impl TryFrom<ClaimsSet> 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(),
})
}
}
Expand Down Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions src/ic_oss_types/src/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit eb10760

Please sign in to comment.