Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More granularity in roles and permissions #364

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs",
#reqwest = { git = "https://github.com/ctron/reqwest", branch = "feature/basic_auth_wasm_1" }
#drogue-ttn = { git = "https://github.com/drogue-iot/drogue-ttn", rev = "cf0338a344309815f0f05e0d7d76acb712445175" } # FIXME: awaiting release

drogue-bazaar = { git = "https://github.com/drogue-iot/drogue-bazaar", rev = "d19ad32f200938aeb5d7081ee3385ee40c5ae0ff" } # FIXME: awaiting release 0.4.0
#drogue-bazaar = { path = "../drogue-bazaar" }
#drogue-bazaar = { git = "https://github.com/drogue-iot/drogue-bazaar", rev = "8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa" } # FIXME: awaiting release 0.4.0
drogue-bazaar = { path = "../drogue-bazaar" }

drogue-client = { git = "https://github.com/drogue-iot/drogue-client", rev = "798c968f0a63a0debcff9965c66b361e85946458" } # FIXME: awaiting release 0.12.0
#drogue-client = { path = "../drogue-client" }
#drogue-client = { git = "https://github.com/drogue-iot/drogue-client", rev = "fdebb42a6cbaa872a779e892fefe0f687b34fa4b" } # FIXME: awaiting release 0.12.0
drogue-client = { path = "../drogue-client" }

operator-framework = { git = "https://github.com/ctron/operator-framework", rev = "8366506a3ed44b638f899dcce4a82ac32fcaff9e" } # FIXME: awaiting release 0.7.0

Expand Down
1 change: 1 addition & 0 deletions access-token-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ env_logger = "0.9"
futures = "0.3"
futures-core = "0.3"
futures-util = "0.3"
indexmap = { version = "1", features = ["serde"] }
log = "0.4"
native-tls = "0.2"
rand = "0.8"
Expand Down
38 changes: 38 additions & 0 deletions access-token-service/src/authz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use async_trait::async_trait;
use drogue_client::user::v1::authz::{Outcome, TokenPermission};
use drogue_cloud_service_api::webapp::http::Method;
use drogue_cloud_service_common::actix_auth::authorization::{Authorizer, Context};
use drogue_cloud_service_common::auth::AuthError;

#[derive(Clone, Debug)]
pub struct TokenOperationAuthorizer;

#[async_trait(?Send)]
impl Authorizer for TokenOperationAuthorizer {
async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
let outcome = if let Some(claims) = context.identity.token_claims() {
// Middlewares cannot be registered to routes so we have to determine what type of permission
// to apply here
let permission_required = match *context.request.method() {
Method::GET => TokenPermission::List,
Method::POST => TokenPermission::Create,
Method::DELETE => TokenPermission::Delete,
// this should be unreachable if actix does its job
_ => {
return Err(AuthError::InvalidRequest(
"Method not defined in the API. This should never happen.".to_string(),
))
}
};

if claims.tokens.contains(&permission_required) {
Outcome::Allow
} else {
Outcome::Deny
}
} else {
Outcome::Allow
};
Ok(Some(outcome))
}
}
6 changes: 4 additions & 2 deletions access-token-service/src/endpoints.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::service::AccessTokenService;
use drogue_client::registry::v1::Client;
use drogue_client::user::v1::authn::{AuthenticationRequest, AuthenticationResponse, Outcome};
use drogue_cloud_service_api::{
auth::user::UserInformation,
Expand All @@ -22,12 +23,13 @@ impl<S: AccessTokenService> Deref for WebData<S> {
pub async fn create<S>(
user: UserInformation,
service: web::Data<WebData<S>>,
opts: web::Query<AccessTokenCreationOptions>,
registry: web::Data<Client>,
opts: web::Json<AccessTokenCreationOptions>,
) -> Result<HttpResponse, actix_web::Error>
where
S: AccessTokenService + 'static,
{
let result = match service.create(&user, opts.0).await {
let result = match service.create(&user, opts.0, &registry.into_inner()).await {
Ok(key) => Ok(HttpResponse::Ok().json(key)),
Err(e) => Err(e.into()),
};
Expand Down
1 change: 1 addition & 0 deletions access-token-service/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod authz;
pub mod endpoints;
pub mod mock;
mod rng;
Expand Down
5 changes: 3 additions & 2 deletions access-token-service/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use async_trait::async_trait;
use drogue_cloud_service_api::webapp::ResponseError;
use drogue_cloud_service_api::{
auth::user::{UserDetails, UserInformation},
token::{AccessToken, AccessTokenCreated, AccessTokenCreationOptions},
token::{AccessToken, AccessTokenCreationOptions, CreatedAccessToken},
};
use std::fmt::Formatter;

Expand All @@ -29,7 +29,8 @@ impl AccessTokenService for MockAccessTokenService {
&self,
_: &UserInformation,
_: AccessTokenCreationOptions,
) -> Result<AccessTokenCreated, Self::Error> {
_: &drogue_client::registry::v1::Client,
) -> Result<CreatedAccessToken, Self::Error> {
todo!()
}

Expand Down
8 changes: 4 additions & 4 deletions access-token-service/src/rng.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use drogue_cloud_service_api::token::AccessTokenCreated;
use drogue_cloud_service_api::token::CreatedAccessToken;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sha3::Digest;

Expand Down Expand Up @@ -32,7 +32,7 @@ pub fn hash_token(token: &str) -> String {
format!("{:x}", sha3::Sha3_512::digest(token.as_bytes()))
}

fn serialize_token(prefix: String, key: String) -> (AccessTokenCreated, String) {
fn serialize_token(prefix: String, key: String) -> (CreatedAccessToken, String) {
let token = format!("{}_{}", prefix, key);

let crc = crc::crc32::checksum_ieee(token.as_bytes());
Expand All @@ -42,13 +42,13 @@ fn serialize_token(prefix: String, key: String) -> (AccessTokenCreated, String)

let hashed = hash_token(&token);

(AccessTokenCreated { prefix, token }, hashed)
(CreatedAccessToken { prefix, token }, hashed)
}

/// Create a new (random) AccessToken.
///
/// It will return a tuple, consisting of the actual Access Token as well as the hashed version.
pub fn generate_access_token() -> (AccessTokenCreated, String) {
pub fn generate_access_token() -> (CreatedAccessToken, String) {
let prefix = generate_prefix();
let raw_key = generate_key();

Expand Down
78 changes: 72 additions & 6 deletions access-token-service/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use async_trait::async_trait;
use chrono::Utc;
use drogue_cloud_service_api::webapp::ResponseError;
use drogue_cloud_service_api::{
admin::Roles,
auth::user::{UserDetails, UserInformation},
token::{AccessToken, AccessTokenCreated, AccessTokenCreationOptions, AccessTokenData},
token::{AccessToken, AccessTokenCreationOptions, AccessTokenData, CreatedAccessToken},
};
use drogue_cloud_service_common::keycloak::{error::Error, KeycloakClient};
use indexmap::IndexMap;
use serde_json::Value;
use std::collections::HashMap;

Expand All @@ -19,7 +21,8 @@ pub trait AccessTokenService: Clone {
&self,
identity: &UserInformation,
opts: AccessTokenCreationOptions,
) -> Result<AccessTokenCreated, Self::Error>;
registry: &drogue_client::registry::v1::Client,
) -> Result<CreatedAccessToken, Self::Error>;
async fn delete(&self, identity: &UserInformation, prefix: String) -> Result<(), Self::Error>;
async fn list(&self, identity: &UserInformation) -> Result<Vec<AccessToken>, Self::Error>;

Expand Down Expand Up @@ -77,7 +80,8 @@ where
&self,
identity: &UserInformation,
opts: AccessTokenCreationOptions,
) -> Result<AccessTokenCreated, Self::Error> {
registry: &drogue_client::registry::v1::Client,
) -> Result<CreatedAccessToken, Self::Error> {
let user_id = match identity.user_id() {
Some(user_id) => user_id,
None => return Err(Error::NotAuthorized),
Expand All @@ -90,10 +94,70 @@ where
.realm_users_with_id_get(&self.client.realm(), user_id)
.await?;

// if there are no active claims attached to the current identity
// there is no need to limit these new claims
let claims = if let Some(roles) = identity.token_claims() {
if let Some(mut claims) = opts.claims {
// 1 - Prevent tokens claims escalation
// i.e. creating a token with more permissions than the current token

// if the current token is not allowed to create apps
// that new one should not either
if roles.create != claims.create {
return Err(Error::NotAuthorized);
}
// if the current token is not allowed to do some tokens operations
// that new one should not either
for token_permission in claims.tokens.iter() {
if !roles.tokens.contains(token_permission) {
return Err(Error::NotAuthorized);
}
}

let mut mapped_claims: IndexMap<String, Roles> = IndexMap::new();
// 2 - prevent claims escalation for applications
for (app_name, claims) in claims.applications.iter() {
// 2a - retrieve the app object
let app = registry
.get_app(app_name)
.await
.map_err(|e| {
Error::Internal(format!("Error with the registry client {}", e))
})?
.ok_or(Error::NotFound)?;

// 2b - get the app uuid from the app name
let app_id = app.metadata.uid.clone();

// 2c - if the used tokens does not have claims for this app
// we cannot create a new token that does
match roles.applications.get(&app_id) {
Some(roles) => {
for claimed_role in claims.0.iter() {
if !roles.contains(claimed_role) {
return Err(Error::NotAuthorized);
}
}
}
None => return Err(Error::NotAuthorized),
}

mapped_claims.insert(app_id, claims.clone());
}
claims.applications = mapped_claims;
Some(claims)
} else {
None
}
} else {
opts.claims
};

let insert = AccessTokenData {
hashed_token: token.1,
created: Utc::now(),
description: opts.description,
claims,
};

let prefix = &token.0.prefix;
Expand Down Expand Up @@ -165,6 +229,7 @@ where
prefix: prefix.into(),
created: data.created,
description: data.description,
claims: data.claims,
});
}
or => log::debug!("Value: {:?}", or),
Expand Down Expand Up @@ -234,9 +299,9 @@ where

log::debug!("Looking for attribute: {}", key);

let expected_hash = match user.attributes.and_then(|mut a| a.remove(&key)) {
let (expected_hash, claims) = match user.attributes.and_then(|mut a| a.remove(&key)) {
Some(value) => match Self::decode_data(value) {
Ok(data) => data.hashed_token,
Ok(data) => (data.hashed_token, data.claims),
Err(_) => return Ok(None),
},
None => return Ok(None),
Expand All @@ -256,7 +321,8 @@ where
true => {
let details = UserDetails {
user_id,
roles: vec![], // FIXME: we should be able to store scopes/roles as well
roles: vec![], // FIXME: we should be able to store scopes/roles as well,
claims,
};
Some(details)
}
Expand Down
3 changes: 2 additions & 1 deletion command-endpoint/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod v1alpha1;

use actix_web::{web, HttpResponse, Responder};
use drogue_client::user::v1::authz::ApplicationPermission;
use drogue_client::{registry, user::v1::authz::Permission};
use drogue_cloud_endpoint_common::{
sender::{ExternalClientPoolConfig, UpstreamSender},
Expand Down Expand Up @@ -94,7 +95,7 @@ pub async fn configurator(
web::scope("/api/command/v1alpha1/apps/{application}/devices/{deviceId}")
.wrap(ApplicationAuthorizer::wrapping(
user_auth.clone(),
Permission::Read,
Permission::App(ApplicationPermission::Command),
))
.wrap(AuthN::from((
authenticator.clone(),
Expand Down
Loading