From 699dd200bcd9ba053872df2e2b3d172b1b024b0a Mon Sep 17 00:00:00 2001 From: jbtrystram Date: Wed, 23 Nov 2022 12:01:30 +0100 Subject: [PATCH 1/9] [refactor] drop unused whoami path and drop redundant endpoints info resources --- console-backend/api/index.yaml | 24 ------------------------ console-backend/src/admin.rs | 9 --------- console-backend/src/info.rs | 8 ++------ console-backend/src/lib.rs | 13 ------------- 4 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 console-backend/src/admin.rs diff --git a/console-backend/api/index.yaml b/console-backend/api/index.yaml index eb99f1dce..c82c7b285 100644 --- a/console-backend/api/index.yaml +++ b/console-backend/api/index.yaml @@ -360,30 +360,6 @@ paths: # ## Admin # - /api/admin/v1alpha1/user/whoami: - get: - tags: - - User administration - description: Get information about the current user. - responses: - 200: - description: Information about the current user. - content: - 'application/json': - schema: - type: object - properties: - id: - type: string - description: | - The ID of the current user. - - NOTE: This ID may be different to the name of the user. - required: - - id - 403: - description: In case the user is not logged in. - /api/admin/v1alpha1/apps/{application}/transfer-ownership: parameters: - $ref: '#/components/parameters/ApplicationName' diff --git a/console-backend/src/admin.rs b/console-backend/src/admin.rs deleted file mode 100644 index 25c5509fa..000000000 --- a/console-backend/src/admin.rs +++ /dev/null @@ -1,9 +0,0 @@ -use actix_web::HttpResponse; -use drogue_cloud_service_api::auth::user::UserInformation; -use serde_json::json; - -pub async fn whoami(user: UserInformation) -> Result { - Ok(HttpResponse::Ok().json(json!({ - "id": user.user_id(), - }))) -} diff --git a/console-backend/src/info.rs b/console-backend/src/info.rs index 13d9eb25a..6bbf63307 100644 --- a/console-backend/src/info.rs +++ b/console-backend/src/info.rs @@ -21,7 +21,8 @@ impl DemoFetcher { } } -pub async fn get_info( +#[get("/drogue-endpoints")] +pub async fn get_public_endpoints( endpoints: web::Data, demos: web::Data, ) -> impl Responder { @@ -32,11 +33,6 @@ pub async fn get_info( HttpResponse::Ok().json(info) } -#[get("/drogue-endpoints")] -pub async fn get_public_endpoints(endpoints: web::Data) -> impl Responder { - HttpResponse::Ok().json(endpoints) -} - #[get("/drogue-version")] pub async fn get_drogue_version() -> impl Responder { HttpResponse::Ok().json(json!({ diff --git a/console-backend/src/lib.rs b/console-backend/src/lib.rs index cdc22f777..9ee7dc348 100644 --- a/console-backend/src/lib.rs +++ b/console-backend/src/lib.rs @@ -1,4 +1,3 @@ -mod admin; mod api; mod demos; mod info; @@ -222,19 +221,7 @@ pub async fn configurator( >), )), ) - .service( - web::scope("/api/admin/v1alpha1") - .wrap(auth.clone()) - .service(web::resource("/user/whoami").route(web::get().to(admin::whoami))), - ) // everything from here on is unauthenticated or not using the middleware - .service( - web::scope("/api/console/v1alpha1").service( - web::resource("/info") - .wrap(auth) - .route(web::get().to(info::get_info)), - ), - ) .service(index) .service( web::scope("/.well-known") From 222469aa3b19f1d6f35ad83030276a60e6f21573 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Trystram Date: Mon, 14 Nov 2022 12:26:10 +0100 Subject: [PATCH 2/9] WIP: start reworking roles and permissions --- Cargo.toml | 4 +- database-common/src/auth.rs | 69 ++++++++++++---------- websocket-integration/src/lib.rs | 4 +- websocket-integration/src/wshandler/mod.rs | 4 +- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f0070493..6d9479b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,8 +48,8 @@ testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs", drogue-bazaar = { git = "https://github.com/drogue-iot/drogue-bazaar", rev = "d19ad32f200938aeb5d7081ee3385ee40c5ae0ff" } # 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 = "78aaf1b0ab7524f428efec5a9de75a1242701951" } # FIXME: awaiting release 0.11.0 +drogue-client = { path = "../drogue-client" } operator-framework = { git = "https://github.com/ctron/operator-framework", rev = "8366506a3ed44b638f899dcce4a82ac32fcaff9e" } # FIXME: awaiting release 0.7.0 diff --git a/database-common/src/auth.rs b/database-common/src/auth.rs index 63acec784..69cba29fd 100644 --- a/database-common/src/auth.rs +++ b/database-common/src/auth.rs @@ -1,9 +1,9 @@ //! Common authn/authz logic use crate::{error::ServiceError, models::app::MemberEntry}; -use drogue_client::user::v1::authz::{Outcome, Permission}; +use drogue_client::user::v1::authz::{Outcome, Permission, ResourcePermission}; +use drogue_client::admin::v1::Role; use drogue_cloud_service_api::{ - admin::Role, auth::user::{IsAdmin, UserInformation}, }; use indexmap::map::IndexMap; @@ -39,37 +39,44 @@ pub fn authorize( return Outcome::Allow; } - // check the owner - match (resource.owner(), identity.user_id()) { - // If there is no owner -> allow access - (None, _) => Outcome::Allow, - // If there is an owner and an authenticated user and both match -> allow access - (Some(owner), Some(user)) if owner == user => Outcome::Allow, - // We must be owner, but are not -> deny - _ if permission == Permission::Owner => Outcome::Deny, - // Check the member list - (Some(_), user) => { - // If we don't have a user, look for the anonymous mapping - let user = user.unwrap_or_default(); - // If there is a member in the list which matches the user ... - if let Some(member) = resource.members().get(user) { - match permission { - // this should already be covered be the rule above - Permission::Owner => Outcome::Deny, - Permission::Admin => match member.role { - Role::Admin => Outcome::Allow, - _ => Outcome::Deny, - }, - Permission::Write => match member.role { - Role::Admin | Role::Manager => Outcome::Allow, - _ => Outcome::Deny, - }, - Permission::Read => Outcome::Allow, + match permission { + Permission::Resource(permission) => { + + // check the owner + match (resource.owner(), identity.user_id()) { + // If there is no owner -> allow access + (None, _) => Outcome::Allow, + // If there is an owner and an authenticated user and both match -> allow access + (Some(owner), Some(user)) if owner == user => Outcome::Allow, + // We must be owner, but are not -> deny + _ if permission == ResourcePermission::Own => Outcome::Deny, + // Check the member list + (Some(_), user) => { + // If we don't have a user, look for the anonymous mapping + let user = user.unwrap_or_default(); + // If there is a member in the list which matches the user ... + if let Some(member) = resource.members().get(user) { + match permission { + // this should already be covered be the rule above + ResourcePermission::Own => Outcome::Deny, + ResourcePermission::Read => match member.role { + Role::Reader | Role::Manager => Outcome::Allow, + _ => Outcome::Deny, + }, + ResourcePermission::Write => match member.role { + Role::Manager => Outcome::Allow, + _ => Outcome::Deny, + }, + Permission::Read => Outcome::Allow, + } + } else { + Outcome::Deny + } } - } else { - Outcome::Deny } - } + }, + // TODO: implement permission check for access tokens operations + Permission::Token(t) => todo!() } } diff --git a/websocket-integration/src/lib.rs b/websocket-integration/src/lib.rs index cead11f16..179670682 100644 --- a/websocket-integration/src/lib.rs +++ b/websocket-integration/src/lib.rs @@ -6,7 +6,7 @@ mod wshandler; use crate::service::Service; use actix::Actor; use actix_web::web; -use drogue_client::user::v1::authz::Permission; +use drogue_client::user::v1::authz::{Permission, ResourcePermission}; use drogue_cloud_service_api::{ kafka::KafkaClientConfig, webapp::{self as actix_web}, @@ -91,7 +91,7 @@ pub async fn run(config: Config, startup: &mut dyn Startup) -> anyhow::Result<() web::scope("/{application}") .wrap(ApplicationAuthorizer::wrapping( user_auth.clone(), - Permission::Read, + Permission::Resource(ResourcePermission::Subscribe), )) .wrap(AuthN::from(( authenticator.clone(), diff --git a/websocket-integration/src/wshandler/mod.rs b/websocket-integration/src/wshandler/mod.rs index 44af1062c..8ab2f0d57 100644 --- a/websocket-integration/src/wshandler/mod.rs +++ b/websocket-integration/src/wshandler/mod.rs @@ -12,7 +12,7 @@ use chrono::{DateTime, TimeZone, Utc}; use cloudevents::AttributesReader; use drogue_client::{ integration::ws::v1::client, - user::{self, v1::authz}, + user::{self, v1::authz, v1::authz::ResourcePermission}, }; use drogue_cloud_service_api::{auth::user::UserInformation, webapp::http::ws::CloseCode}; use drogue_cloud_service_common::auth::openid::{self, CustomClaims}; @@ -60,7 +60,7 @@ impl AuthContext { .user_auth .authorize(authz::AuthorizationRequest { application: self.application.clone(), - permission: authz::Permission::Read, + permission: authz::Permission::Resource(ResourcePermission::Subscribe), user_id: user.user_id().map(ToString::to_string), roles: user.roles().clone(), }) From 14a67ab7b12754844c729ae1e5bf88b70330b857 Mon Sep 17 00:00:00 2001 From: jbtrystram Date: Mon, 28 Nov 2022 18:42:15 +0100 Subject: [PATCH 3/9] Update the permission model with more granularity --- Cargo.lock | 2 - Cargo.toml | 4 +- command-endpoint/src/lib.rs | 3 +- database-common/src/auth.rs | 269 +++++----------- database-common/src/models/app.rs | 10 +- database-common/tests/auth.rs | 296 ++++++++++++++++++ deploy/helm | 2 +- .../src/service/admin/mod.rs | 61 +++- .../src/service/management/mod.rs | 54 +++- device-management-service/src/service/mod.rs | 7 +- mqtt-integration/src/service/session.rs | 5 +- service-api/src/admin/mod.rs | 49 +-- websocket-integration/src/lib.rs | 4 +- websocket-integration/src/wshandler/mod.rs | 4 +- 14 files changed, 490 insertions(+), 280 deletions(-) create mode 100644 database-common/tests/auth.rs diff --git a/Cargo.lock b/Cargo.lock index a02245c1d..d36ac4675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,7 +1416,6 @@ checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" [[package]] name = "drogue-bazaar" version = "0.3.0" -source = "git+https://github.com/drogue-iot/drogue-bazaar?rev=d19ad32f200938aeb5d7081ee3385ee40c5ae0ff#d19ad32f200938aeb5d7081ee3385ee40c5ae0ff" dependencies = [ "actix-cors", "actix-http", @@ -1465,7 +1464,6 @@ dependencies = [ [[package]] name = "drogue-client" version = "0.12.0" -source = "git+https://github.com/drogue-iot/drogue-client?rev=798c968f0a63a0debcff9965c66b361e85946458#798c968f0a63a0debcff9965c66b361e85946458" dependencies = [ "async-trait", "base64 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index 6d9479b3b..9aaa2c05b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,8 @@ 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 = "e74ee3f01a0ab1cc55825abefb0f12b82eee67d9" } # FIXME: awaiting release 0.3.0 +drogue-bazaar = { path = "../drogue-bazaar" } #drogue-client = { git = "https://github.com/drogue-iot/drogue-client", rev = "78aaf1b0ab7524f428efec5a9de75a1242701951" } # FIXME: awaiting release 0.11.0 drogue-client = { path = "../drogue-client" } diff --git a/command-endpoint/src/lib.rs b/command-endpoint/src/lib.rs index 70c7a16f6..c6123c7a6 100644 --- a/command-endpoint/src/lib.rs +++ b/command-endpoint/src/lib.rs @@ -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}, @@ -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(), diff --git a/database-common/src/auth.rs b/database-common/src/auth.rs index 69cba29fd..2f67aa594 100644 --- a/database-common/src/auth.rs +++ b/database-common/src/auth.rs @@ -1,11 +1,11 @@ //! Common authn/authz logic -use crate::{error::ServiceError, models::app::MemberEntry}; -use drogue_client::user::v1::authz::{Outcome, Permission, ResourcePermission}; -use drogue_client::admin::v1::Role; -use drogue_cloud_service_api::{ - auth::user::{IsAdmin, UserInformation}, +use crate::error::ServiceError; +use drogue_client::admin::v1::{MemberEntry, Role}; +use drogue_client::user::v1::authz::{ + ApplicationPermission, DevicePermission, Outcome, Permission, }; +use drogue_cloud_service_api::auth::user::{IsAdmin, UserInformation}; use indexmap::map::IndexMap; use std::fmt::Debug; @@ -26,7 +26,7 @@ pub fn authorize( identity: &UserInformation, permission: Permission, ) -> Outcome { - log::debug!( + log::warn!( "authorizing - resource: {:?}, identity: {:?}, permission: {:?}", resource, identity, @@ -39,44 +39,79 @@ pub fn authorize( return Outcome::Allow; } - match permission { - Permission::Resource(permission) => { + // check the owner + match (resource.owner(), identity.user_id()) { + // If there is no owner -> allow access + (None, _) => return Outcome::Allow, + // If there is an owner and an authenticated user and both match -> allow access + // as the owner have all permissions on a resource + (Some(owner), Some(user)) if owner == user => return Outcome::Allow, + // The current user is not the owner, carry on.. + _ => {} + } + + // Check the member list + // If we don't have a user, look for the anonymous mapping + let user = identity.user_id().unwrap_or_default(); + // If there is a member in the list which matches the user ... + if let Some(member) = resource.members().get(user) { + match permission { + Permission::App(permission) => { + match permission { + // this should already be covered be the rule above + ApplicationPermission::Transfer => Outcome::Deny, + // short of transferring the app, the admin can do anything + _ if member.roles.contains(&Role::Admin) => Outcome::Allow, + // Read is allowed for Reader, Writer and Manager + ApplicationPermission::Read + if member.roles.contains(&Role::Reader) + || member.roles.contains(&Role::Manager) => + { + Outcome::Allow + } + // Write is allowed for Admin and Manager + ApplicationPermission::Write if member.roles.contains(&Role::Manager) => { + Outcome::Allow + } - // check the owner - match (resource.owner(), identity.user_id()) { - // If there is no owner -> allow access - (None, _) => Outcome::Allow, - // If there is an owner and an authenticated user and both match -> allow access - (Some(owner), Some(user)) if owner == user => Outcome::Allow, - // We must be owner, but are not -> deny - _ if permission == ResourcePermission::Own => Outcome::Deny, - // Check the member list - (Some(_), user) => { - // If we don't have a user, look for the anonymous mapping - let user = user.unwrap_or_default(); - // If there is a member in the list which matches the user ... - if let Some(member) = resource.members().get(user) { - match permission { - // this should already be covered be the rule above - ResourcePermission::Own => Outcome::Deny, - ResourcePermission::Read => match member.role { - Role::Reader | Role::Manager => Outcome::Allow, - _ => Outcome::Deny, - }, - ResourcePermission::Write => match member.role { - Role::Manager => Outcome::Allow, - _ => Outcome::Deny, - }, - Permission::Read => Outcome::Allow, - } - } else { - Outcome::Deny + ApplicationPermission::Command if member.roles.contains(&Role::Publisher) => { + Outcome::Allow + } + ApplicationPermission::Subscribe + if member.roles.contains(&Role::Subscriber) => + { + Outcome::Allow } + _ => Outcome::Deny, } } - }, - // TODO: implement permission check for access tokens operations - Permission::Token(t) => todo!() + Permission::Device(permission) => { + // When it comes to devices the admin can do anything + if member.roles.contains(&Role::Admin) { + return Outcome::Allow; + } + match permission { + DevicePermission::Read + if member.roles.contains(&Role::Reader) + || member.roles.contains(&Role::Manager) => + { + Outcome::Allow + } + DevicePermission::Write + | DevicePermission::Create + | DevicePermission::Delete + if member.roles.contains(&Role::Manager) => + { + Outcome::Allow + } + _ => Outcome::Deny, + } + } + // TODO: implement permission check for access tokens operations + Permission::Token(permission) => todo!(), + } + } else { + Outcome::Deny } } @@ -112,157 +147,3 @@ where Outcome::Deny => Err(f()), } } - -#[cfg(test)] -mod test { - use super::*; - use drogue_cloud_service_api::auth::user::UserDetails; - - #[derive(Debug)] - struct MockResource { - owner: String, - members: IndexMap, - } - - impl Resource for MockResource { - fn owner(&self) -> Option<&str> { - Some(&self.owner) - } - - fn members(&self) -> &IndexMap { - &self.members - } - } - - macro_rules! resource { - ($owner:literal, [$( $id:literal => $role:expr),*]) => { - { - #[allow(unused_mut)] - let mut members = IndexMap::new(); - $( - members.insert($id.into(), MemberEntry { role: $role }); - )* - MockResource { - owner: $owner.into(), - members, - } - } - - }; - } - - macro_rules! test_auth { - ($user:expr, $resource:expr, [$( $perm:expr => $outcome:expr ),*]) => { - { - let user = $user; - let resource = $resource; - $(assert_eq!(authorize(&user, &resource, $perm), $outcome, "Expected outcome '{:?}' for permission '{:?}'", $outcome, $perm);)* - } - }; - } - - fn user(id: &str, roles: &[&str]) -> UserInformation { - UserInformation::Authenticated(UserDetails { - user_id: id.into(), - roles: roles.iter().map(ToString::to_string).collect(), - }) - } - - #[test] - fn test_auth_owner() { - test_auth!( - resource!("foo", []), - user("foo", &[]), - [ - Permission::Owner => Outcome::Allow, - Permission::Admin => Outcome::Allow, - Permission::Write => Outcome::Allow, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_auth_sys_admin() { - test_auth!( - resource!("foo", []), - user("bar", &["drogue-admin"]), - [ - Permission::Owner => Outcome::Allow, - Permission::Admin => Outcome::Allow, - Permission::Write => Outcome::Allow, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_auth_resource_admin() { - test_auth!( - resource!("foo", ["bar" => Role::Admin]), - user("bar", &[]), - [ - Permission::Owner => Outcome::Deny, - Permission::Admin => Outcome::Allow, - Permission::Write => Outcome::Allow, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_auth_resource_manager() { - test_auth!( - resource!("foo", ["bar" => Role::Manager]), - user("bar", &[]), - [ - Permission::Owner => Outcome::Deny, - Permission::Admin => Outcome::Deny, - Permission::Write => Outcome::Allow, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_auth_resource_reader() { - test_auth!( - resource!("foo", ["bar" => Role::Reader]), - user("bar", &[]), - [ - Permission::Owner => Outcome::Deny, - Permission::Admin => Outcome::Deny, - Permission::Write => Outcome::Deny, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_auth_anon() { - test_auth!( - resource!("foo", ["" => Role::Reader]), - UserInformation::Anonymous, - [ - Permission::Owner => Outcome::Deny, - Permission::Admin => Outcome::Deny, - Permission::Write => Outcome::Deny, - Permission::Read => Outcome::Allow - ] - ) - } - - #[test] - fn test_reject_anon() { - test_auth!( - resource!("foo", ["bar" => Role::Reader]), - UserInformation::Anonymous, - [ - Permission::Owner => Outcome::Deny, - Permission::Admin => Outcome::Deny, - Permission::Write => Outcome::Deny, - Permission::Read => Outcome::Deny - ] - ) - } -} diff --git a/database-common/src/models/app.rs b/database-common/src/models/app.rs index aba2f7358..02f198e0f 100644 --- a/database-common/src/models/app.rs +++ b/database-common/src/models/app.rs @@ -14,10 +14,11 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use core::pin::Pin; use drogue_client::{meta, registry}; -use drogue_cloud_service_api::{admin::Role, auth::user::UserInformation, labels::LabelSelector}; +use drogue_cloud_service_api::{ + admin::MemberEntry, auth::user::UserInformation, labels::LabelSelector, +}; use futures::{future, Stream, TryStreamExt}; use indexmap::map::IndexMap; -use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::{hash_map::RandomState, HashMap, HashSet}; use tokio_postgres::{ @@ -70,11 +71,6 @@ impl Resource for Application { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MemberEntry { - pub role: Role, -} - /// Extract a section from the application data. Prevents cloning the whole struct. fn extract_sect(mut app: Application, key: &str) -> (Application, Option>) { let sect = app diff --git a/database-common/tests/auth.rs b/database-common/tests/auth.rs new file mode 100644 index 000000000..26559877c --- /dev/null +++ b/database-common/tests/auth.rs @@ -0,0 +1,296 @@ +use drogue_cloud_database_common::auth::{authorize, Resource}; +use drogue_cloud_service_api::auth::user::UserDetails; +use drogue_cloud_service_common::auth::UserInformation; + +use drogue_client::admin::v1::{MemberEntry, Role}; +use drogue_client::user::v1::authz::{ + ApplicationPermission, DevicePermission, Outcome, Permission, +}; + +use indexmap::IndexMap; + +#[derive(Debug)] +struct MockResource { + owner: String, + members: IndexMap, +} + +impl Resource for MockResource { + fn owner(&self) -> Option<&str> { + Some(&self.owner) + } + + fn members(&self) -> &IndexMap { + &self.members + } +} + +macro_rules! resource { + ($owner:literal, [$( $id:literal => $role:expr),*]) => { + { + #[allow(unused_mut)] + let mut members = IndexMap::new(); + $( + members.insert($id.into(), MemberEntry { roles: $role }); + )* + MockResource { + owner: $owner.into(), + members, + } + } + + }; + } + +macro_rules! test_auth { + ($user:expr, $resource:expr, [$( $perm:expr => $outcome:expr ),*]) => { + { + let user = $user; + let resource = $resource; + $(assert_eq!(authorize(&user, &resource, $perm), $outcome, "Expected outcome '{:?}' for permission '{:?}'", $outcome, $perm);)* + } + }; + } + +fn user(id: &str, roles: &[&str]) -> UserInformation { + UserInformation::Authenticated(UserDetails { + user_id: id.into(), + roles: roles.iter().map(ToString::to_string).collect(), + }) +} + +#[test] +fn test_auth_owner() { + test_auth!( + resource!("foo", []), + user("foo", &[]), + [ + Permission::App(ApplicationPermission::Create) => Outcome::Allow, + Permission::App(ApplicationPermission::Delete) => Outcome::Allow, + Permission::App(ApplicationPermission::Write) => Outcome::Allow, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Transfer) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Allow, + Permission::App(ApplicationPermission::Members) => Outcome::Allow, + Permission::Device(DevicePermission::Create) => Outcome::Allow, + Permission::Device(DevicePermission::Delete) => Outcome::Allow, + Permission::Device(DevicePermission::Write) => Outcome::Allow, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_sys_admin() { + test_auth!( + resource!("foo", []), + user("bar", &["drogue-admin"]), + [ + Permission::App(ApplicationPermission::Create) => Outcome::Allow, + Permission::App(ApplicationPermission::Delete) => Outcome::Allow, + Permission::App(ApplicationPermission::Write) => Outcome::Allow, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Transfer) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Allow, + Permission::App(ApplicationPermission::Members) => Outcome::Allow, + Permission::Device(DevicePermission::Create) => Outcome::Allow, + Permission::Device(DevicePermission::Delete) => Outcome::Allow, + Permission::Device(DevicePermission::Write) => Outcome::Allow, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_resource_admin() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Admin]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Create) => Outcome::Allow, + Permission::App(ApplicationPermission::Delete) => Outcome::Allow, + Permission::App(ApplicationPermission::Write) => Outcome::Allow, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Allow, + Permission::App(ApplicationPermission::Members) => Outcome::Allow, + Permission::Device(DevicePermission::Create) => Outcome::Allow, + Permission::Device(DevicePermission::Delete) => Outcome::Allow, + Permission::Device(DevicePermission::Write) => Outcome::Allow, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_resource_manager() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Manager]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Allow, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Deny, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Allow, + Permission::Device(DevicePermission::Delete) => Outcome::Allow, + Permission::Device(DevicePermission::Write) => Outcome::Allow, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_resource_reader_subscriber() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Reader, Role::Subscriber]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_resource_reader() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Reader]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Deny, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_auth_resource_subscriber() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Subscriber]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Deny, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Deny + ] + ) +} + +#[test] +fn test_auth_resource_publisher() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Publisher]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Deny, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Deny, + Permission::App(ApplicationPermission::Command) => Outcome::Allow, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Deny + ] + ) +} + +#[test] +fn test_auth_resource_publisher_subscriber() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Publisher, Role::Subscriber]]), + user("bar", &[]), + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Deny, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Allow, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Deny + ] + ) +} + +#[test] +fn test_auth_anon() { + test_auth!( + resource!("foo", ["" => vec![Role::Subscriber, Role::Reader]]), + UserInformation::Anonymous, + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Create) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Allow, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Allow, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Allow + ] + ) +} + +#[test] +fn test_reject_anon() { + test_auth!( + resource!("foo", ["bar" => vec![Role::Reader]]), + UserInformation::Anonymous, + [ + Permission::App(ApplicationPermission::Transfer) => Outcome::Deny, + Permission::App(ApplicationPermission::Create) => Outcome::Deny, + Permission::App(ApplicationPermission::Delete) => Outcome::Deny, + Permission::App(ApplicationPermission::Write) => Outcome::Deny, + Permission::App(ApplicationPermission::Read) => Outcome::Deny, + Permission::App(ApplicationPermission::Subscribe) => Outcome::Deny, + Permission::App(ApplicationPermission::Command) => Outcome::Deny, + Permission::App(ApplicationPermission::Members) => Outcome::Deny, + Permission::Device(DevicePermission::Create) => Outcome::Deny, + Permission::Device(DevicePermission::Delete) => Outcome::Deny, + Permission::Device(DevicePermission::Write) => Outcome::Deny, + Permission::Device(DevicePermission::Read) => Outcome::Deny + ] + ) +} diff --git a/deploy/helm b/deploy/helm index 205f8c585..d6b340ea2 160000 --- a/deploy/helm +++ b/deploy/helm @@ -1 +1 @@ -Subproject commit 205f8c585207b348ffbe756662e2168a10fdc5b3 +Subproject commit d6b340ea28f16f71149465c8f2f5c58d07ec3f9d diff --git a/device-management-service/src/service/admin/mod.rs b/device-management-service/src/service/admin/mod.rs index 3efcebf96..9c511ee41 100644 --- a/device-management-service/src/service/admin/mod.rs +++ b/device-management-service/src/service/admin/mod.rs @@ -1,6 +1,6 @@ use crate::service::{error::PostgresManagementServiceError, PostgresManagementService}; use async_trait::async_trait; -use drogue_client::user::v1::authz::Permission; +use drogue_client::user::v1::authz::{ApplicationPermission, Permission}; use drogue_cloud_admin_service::apps::AdminService; use drogue_cloud_database_common::{ auth::ensure_with, @@ -49,7 +49,12 @@ where // ensure we are permitted to do the change - ensure_with(&app, identity, Permission::Owner, || ServiceError::NotFound)?; + ensure_with( + &app, + identity, + Permission::App(ApplicationPermission::Own), + || ServiceError::NotFound, + )?; // retrieve the new user ID from keycloak let new_user = match self @@ -219,17 +224,40 @@ where // ensure we are permitted to perform the operation - ensure_with(&app, identity, Permission::Admin, || ServiceError::NotFound)?; + log::warn!( + "get members - identity: {:?} - app_owner: {:?}", + &identity, + &app.owner + ); + println!("i try to get members and I should not get 404 because i am the app owner."); + + // FIXME : this is just to have always allow ? + let _ = ensure_with( + &app, + identity, + Permission::App(ApplicationPermission::Members), + || ServiceError::NotFound, + ); // get operation let mut members: IndexMap = IndexMap::new(); for (k, v) in &app.members { // empty values are allowed. (e.g. to share an app with the whole word) if k.is_empty() { - members.insert(k.clone(), MemberEntry { role: v.role }); + members.insert( + k.clone(), + MemberEntry { + roles: v.roles.clone(), + }, + ); } else { match self.keycloak.username_from_id(k).await { - Ok(u) => members.insert(u, MemberEntry { role: v.role }), + Ok(u) => members.insert( + u, + MemberEntry { + roles: v.roles.clone(), + }, + ), // If the id does not exist in keycloak we skip it Err(_) => None, }; @@ -267,16 +295,26 @@ where // ensure we are permitted to perform the operation - ensure_with(&app, identity, Permission::Admin, || ServiceError::NotFound)?; + ensure_with( + &app, + identity, + Permission::App(ApplicationPermission::Members), + || ServiceError::NotFound, + )?; // get users id from usernames - let mut id_members: IndexMap = IndexMap::new(); + let mut id_members: IndexMap = IndexMap::new(); for (k, v) in &members.members { if !k.is_empty() { match self.keycloak.id_from_username(k.as_str()).await { Ok(u) => { - id_members.insert(u, app::MemberEntry { role: v.role }); + id_members.insert( + u, + MemberEntry { + roles: v.roles.clone(), + }, + ); } // If the username does not exist in keycloak it's an error ! Err(_) => { @@ -289,7 +327,12 @@ where }; // empty values are allowed. (e.g. to share an app with the whole word) } else { - id_members.insert(k.clone(), app::MemberEntry { role: v.role }); + id_members.insert( + k.clone(), + MemberEntry { + roles: v.roles.clone(), + }, + ); } } diff --git a/device-management-service/src/service/management/mod.rs b/device-management-service/src/service/management/mod.rs index 0d4ee3bb1..8b6eff51e 100644 --- a/device-management-service/src/service/management/mod.rs +++ b/device-management-service/src/service/management/mod.rs @@ -6,6 +6,7 @@ use crate::{ use async_trait::async_trait; use chrono::Utc; use core::pin::Pin; +use drogue_client::user::v1::authz::{ApplicationPermission, DevicePermission}; use drogue_client::{registry, user::v1::authz::Permission}; use drogue_cloud_database_common::{ auth::{ensure, ensure_with}, @@ -172,7 +173,7 @@ where .await?; if let Some(app) = &app { - ensure(app, identity, Permission::Read)?; + ensure(app, identity, Permission::App(ApplicationPermission::Read))?; } Ok(app.map(Into::into)) @@ -206,7 +207,11 @@ where // Using ensure call here is just a safeguard! The list operation must only return // entries the user has access to. Otherwise the limit/offset functionality // won't work - let result = match ensure(&app, &identity, Permission::Read) { + let result = match ensure( + &app, + &identity, + Permission::App(ApplicationPermission::Read), + ) { Ok(_) => Some(app.into()), Err(_) => None, }; @@ -275,7 +280,11 @@ where )); } - ensure(¤t, identity, Permission::Admin)?; + ensure( + ¤t, + identity, + Permission::App(ApplicationPermission::Delete), + )?; utils::check_preconditions(¶ms.preconditions, ¤t)?; // there is no need to use the provided constraints, we as locked the entry "for update" @@ -363,9 +372,12 @@ where }; // ensure we have access to the application, but don't confirm the device if we don't - ensure_with(&app, identity, Permission::Write, || { - ServiceError::ReferenceNotFound - })?; + ensure_with( + &app, + identity, + Permission::Device(DevicePermission::Create), + || ServiceError::ReferenceNotFound, + )?; let name = device.name.clone(); // assign a new UID @@ -427,7 +439,12 @@ where .ok_or(ServiceError::NotFound)?; // ensure we have access, but don't confirm the device if we don't - ensure_with(&app, identity, Permission::Read, || ServiceError::NotFound)?; + ensure_with( + &app, + identity, + Permission::Device(DevicePermission::Read), + || ServiceError::NotFound, + )?; let device = PostgresDeviceAccessor::new(&c) .get(app_id, device_id, Lock::None) @@ -455,7 +472,12 @@ where .ok_or(ServiceError::NotFound)?; // ensure we have access, but don't confirm the device if we don't - ensure_with(&app, &identity, Permission::Read, || ServiceError::NotFound)?; + ensure_with( + &app, + &identity, + Permission::Device(DevicePermission::Read), + || ServiceError::NotFound, + )?; Ok(Box::pin( PostgresDeviceAccessor::new(&c) @@ -491,9 +513,12 @@ where }?; // ensure we have access, but don't confirm the device if we don't - ensure_with(¤t, identity, Permission::Write, || { - ServiceError::NotFound - })?; + ensure_with( + ¤t, + identity, + Permission::Device(DevicePermission::Write), + || ServiceError::NotFound, + )?; let accessor = PostgresDeviceAccessor::new(&t); @@ -601,7 +626,12 @@ where .ok_or(ServiceError::NotFound)?; // ensure we have access, but don't confirm the device if we don't - ensure_with(&app, identity, Permission::Write, || ServiceError::NotFound)?; + ensure_with( + &app, + identity, + Permission::Device(DevicePermission::Delete), + || ServiceError::NotFound, + )?; // check the preconditions utils::check_preconditions(¶ms.preconditions, ¤t)?; diff --git a/device-management-service/src/service/mod.rs b/device-management-service/src/service/mod.rs index cbae60199..38c172faf 100644 --- a/device-management-service/src/service/mod.rs +++ b/device-management-service/src/service/mod.rs @@ -6,6 +6,7 @@ mod x509; use crate::{service::error::PostgresManagementServiceError, utils::epoch}; use deadpool_postgres::{Pool, Transaction}; +use drogue_client::user::v1::authz::ApplicationPermission; use drogue_client::{registry, user::v1::authz::Permission, Translator}; use drogue_cloud_database_common::{ auth::ensure, @@ -232,7 +233,11 @@ where }?; if let Some(identity) = identity { - ensure(¤t, identity, Permission::Write)?; + ensure( + ¤t, + identity, + Permission::App(ApplicationPermission::Write), + )?; } utils::check_versions(expected_uid, expected_resource_version, ¤t)?; diff --git a/mqtt-integration/src/service/session.rs b/mqtt-integration/src/service/session.rs index 077877db7..b0db6a3c2 100644 --- a/mqtt-integration/src/service/session.rs +++ b/mqtt-integration/src/service/session.rs @@ -8,6 +8,7 @@ use crate::{ use async_trait::async_trait; use drogue_client::registry; use drogue_client::user; +use drogue_client::user::v1::authz::ApplicationPermission; use drogue_cloud_endpoint_common::sender::UpstreamSender; use drogue_cloud_event_common::stream::CustomAck; use drogue_cloud_integration_common::{ @@ -164,7 +165,7 @@ impl Session { self.authorize( app.to_string(), user_auth, - user::v1::authz::Permission::Read, + user::v1::authz::Permission::App(ApplicationPermission::Subscribe), ) .await .map_err(|_| v5::codec::SubscribeAckReason::NotAuthorized)?; @@ -273,7 +274,7 @@ impl mqtt::Session for Session { self.authorize( app.to_string(), user_auth, - user::v1::authz::Permission::Write, + user::v1::authz::Permission::App(ApplicationPermission::Command), ) .await .map_err(|_| PublishError::NotAuthorized)?; diff --git a/service-api/src/admin/mod.rs b/service-api/src/admin/mod.rs index b2d26e447..794681146 100644 --- a/service-api/src/admin/mod.rs +++ b/service-api/src/admin/mod.rs @@ -1,45 +1,4 @@ -use core::fmt::{Display, Formatter}; -use indexmap::IndexMap; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TransferOwnership { - pub new_user: String, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Members { - #[serde(skip_serializing_if = "Option::is_none")] - pub resource_version: Option, - #[serde(default)] - pub members: IndexMap, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct MemberEntry { - pub role: Role, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum Role { - /// Allow everything, including changing members - Admin, - /// Allow reading and writing, but not changing members. - Manager, - /// Allow reading only. - Reader, -} - -impl Display for Role { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Admin => write!(f, "Administrator"), - Self::Manager => write!(f, "Manager"), - Self::Reader => write!(f, "Reader"), - } - } -} +pub use drogue_client::admin::v1::MemberEntry; +pub use drogue_client::admin::v1::Members; +pub use drogue_client::admin::v1::Role; +pub use drogue_client::admin::v1::TransferOwnership; diff --git a/websocket-integration/src/lib.rs b/websocket-integration/src/lib.rs index 179670682..cf6cddc3c 100644 --- a/websocket-integration/src/lib.rs +++ b/websocket-integration/src/lib.rs @@ -6,7 +6,7 @@ mod wshandler; use crate::service::Service; use actix::Actor; use actix_web::web; -use drogue_client::user::v1::authz::{Permission, ResourcePermission}; +use drogue_client::user::v1::authz::{ApplicationPermission, Permission}; use drogue_cloud_service_api::{ kafka::KafkaClientConfig, webapp::{self as actix_web}, @@ -91,7 +91,7 @@ pub async fn run(config: Config, startup: &mut dyn Startup) -> anyhow::Result<() web::scope("/{application}") .wrap(ApplicationAuthorizer::wrapping( user_auth.clone(), - Permission::Resource(ResourcePermission::Subscribe), + Permission::App(ApplicationPermission::Subscribe), )) .wrap(AuthN::from(( authenticator.clone(), diff --git a/websocket-integration/src/wshandler/mod.rs b/websocket-integration/src/wshandler/mod.rs index 8ab2f0d57..36325094f 100644 --- a/websocket-integration/src/wshandler/mod.rs +++ b/websocket-integration/src/wshandler/mod.rs @@ -12,7 +12,7 @@ use chrono::{DateTime, TimeZone, Utc}; use cloudevents::AttributesReader; use drogue_client::{ integration::ws::v1::client, - user::{self, v1::authz, v1::authz::ResourcePermission}, + user::{self, v1::authz, v1::authz::ApplicationPermission}, }; use drogue_cloud_service_api::{auth::user::UserInformation, webapp::http::ws::CloseCode}; use drogue_cloud_service_common::auth::openid::{self, CustomClaims}; @@ -60,7 +60,7 @@ impl AuthContext { .user_auth .authorize(authz::AuthorizationRequest { application: self.application.clone(), - permission: authz::Permission::Resource(ResourcePermission::Subscribe), + permission: authz::Permission::App(ApplicationPermission::Subscribe), user_id: user.user_id().map(ToString::to_string), roles: user.roles().clone(), }) From 30476c749d940b1639ab539916705fdc4c7d2ffb Mon Sep 17 00:00:00 2001 From: jbtrystram Date: Fri, 2 Dec 2022 11:17:53 +0100 Subject: [PATCH 4/9] initial changes for personnal access token scopes --- Cargo.lock | 2 + Cargo.toml | 8 +- access-token-service/src/endpoints.rs | 2 +- access-token-service/src/mock.rs | 4 +- access-token-service/src/rng.rs | 8 +- access-token-service/src/service.rs | 15 ++- console-frontend/Cargo.lock | 9 +- console-frontend/Cargo.toml | 10 +- .../src/pages/access_tokens/create.rs | 8 +- .../src/pages/access_tokens/success.rs | 2 +- console-frontend/src/pages/apps/create.rs | 2 +- .../src/pages/apps/details/admin.rs | 55 +++++--- console-frontend/src/pages/devices/clone.rs | 2 +- console-frontend/src/pages/devices/create.rs | 2 +- database-common/src/auth.rs | 120 ++++++++++-------- database-common/src/models/app.rs | 4 + database-common/tests/auth.rs | 4 + .../src/service/admin/mod.rs | 4 +- service-api/src/token.rs | 20 +-- user-auth-service/src/service.rs | 2 + 20 files changed, 156 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d36ac4675..db83c418d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,6 +1416,7 @@ checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" [[package]] name = "drogue-bazaar" version = "0.3.0" +source = "git+https://github.com/drogue-iot/drogue-bazaar?rev=8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa#8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa" dependencies = [ "actix-cors", "actix-http", @@ -1464,6 +1465,7 @@ dependencies = [ [[package]] name = "drogue-client" version = "0.12.0" +source = "git+https://github.com/drogue-iot/drogue-client?rev=fdebb42a6cbaa872a779e892fefe0f687b34fa4b#fdebb42a6cbaa872a779e892fefe0f687b34fa4b" dependencies = [ "async-trait", "base64 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index 9aaa2c05b..17ab4016c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "e74ee3f01a0ab1cc55825abefb0f12b82eee67d9" } # FIXME: awaiting release 0.3.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 = "78aaf1b0ab7524f428efec5a9de75a1242701951" } # FIXME: awaiting release 0.11.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 diff --git a/access-token-service/src/endpoints.rs b/access-token-service/src/endpoints.rs index 2d57f56dc..a91f0a682 100644 --- a/access-token-service/src/endpoints.rs +++ b/access-token-service/src/endpoints.rs @@ -22,7 +22,7 @@ impl Deref for WebData { pub async fn create( user: UserInformation, service: web::Data>, - opts: web::Query, + opts: web::Json, ) -> Result where S: AccessTokenService + 'static, diff --git a/access-token-service/src/mock.rs b/access-token-service/src/mock.rs index 0cdc805ce..644a64703 100644 --- a/access-token-service/src/mock.rs +++ b/access-token-service/src/mock.rs @@ -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; @@ -29,7 +29,7 @@ impl AccessTokenService for MockAccessTokenService { &self, _: &UserInformation, _: AccessTokenCreationOptions, - ) -> Result { + ) -> Result { todo!() } diff --git a/access-token-service/src/rng.rs b/access-token-service/src/rng.rs index b78d4eef1..578928e31 100644 --- a/access-token-service/src/rng.rs +++ b/access-token-service/src/rng.rs @@ -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; @@ -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()); @@ -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(); diff --git a/access-token-service/src/service.rs b/access-token-service/src/service.rs index 148753932..e9a933ec0 100644 --- a/access-token-service/src/service.rs +++ b/access-token-service/src/service.rs @@ -3,7 +3,7 @@ use chrono::Utc; use drogue_cloud_service_api::webapp::ResponseError; use drogue_cloud_service_api::{ auth::user::{UserDetails, UserInformation}, - token::{AccessToken, AccessTokenCreated, AccessTokenCreationOptions, AccessTokenData}, + token::{AccessToken, AccessTokenCreationOptions, AccessTokenData, CreatedAccessToken}, }; use drogue_cloud_service_common::keycloak::{error::Error, KeycloakClient}; use serde_json::Value; @@ -19,7 +19,7 @@ pub trait AccessTokenService: Clone { &self, identity: &UserInformation, opts: AccessTokenCreationOptions, - ) -> Result; + ) -> Result; async fn delete(&self, identity: &UserInformation, prefix: String) -> Result<(), Self::Error>; async fn list(&self, identity: &UserInformation) -> Result, Self::Error>; @@ -77,7 +77,7 @@ where &self, identity: &UserInformation, opts: AccessTokenCreationOptions, - ) -> Result { + ) -> Result { let user_id = match identity.user_id() { Some(user_id) => user_id, None => return Err(Error::NotAuthorized), @@ -94,6 +94,7 @@ where hashed_token: token.1, created: Utc::now(), description: opts.description, + scopes: opts.scopes, }; let prefix = &token.0.prefix; @@ -165,6 +166,7 @@ where prefix: prefix.into(), created: data.created, description: data.description, + scopes: data.scopes, }); } or => log::debug!("Value: {:?}", or), @@ -234,9 +236,9 @@ where log::debug!("Looking for attribute: {}", key); - let expected_hash = match user.attributes.and_then(|mut a| a.remove(&key)) { + let (expected_hash, scopes) = 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.scopes), Err(_) => return Ok(None), }, None => return Ok(None), @@ -256,7 +258,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, + scopes, }; Some(details) } diff --git a/console-frontend/Cargo.lock b/console-frontend/Cargo.lock index 65cce8b1c..11b6e2502 100644 --- a/console-frontend/Cargo.lock +++ b/console-frontend/Cargo.lock @@ -538,7 +538,7 @@ checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" [[package]] name = "drogue-bazaar" version = "0.3.0" -source = "git+https://github.com/drogue-iot/drogue-bazaar?rev=d19ad32f200938aeb5d7081ee3385ee40c5ae0ff#d19ad32f200938aeb5d7081ee3385ee40c5ae0ff" +source = "git+https://github.com/drogue-iot/drogue-bazaar?rev=8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa#8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa" dependencies = [ "anyhow", "async-trait", @@ -567,7 +567,7 @@ dependencies = [ [[package]] name = "drogue-client" version = "0.12.0" -source = "git+https://github.com/drogue-iot/drogue-client?rev=798c968f0a63a0debcff9965c66b361e85946458#798c968f0a63a0debcff9965c66b361e85946458" +source = "git+https://github.com/drogue-iot/drogue-client?rev=fdebb42a6cbaa872a779e892fefe0f687b34fa4b#fdebb42a6cbaa872a779e892fefe0f687b34fa4b" dependencies = [ "async-trait", "base64 0.13.1", @@ -1983,9 +1983,8 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "patternfly-yew" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfe177a94ab0fe2db19bc3b86389555b4fdb6acad44443fdeddf6f3f32b3630" +version = "0.2.3" +source = "git+https://github.com/ctron/patternfly-yew?rev=331782836f0ed67bc6ab41ef71497c970176e9d4#331782836f0ed67bc6ab41ef71497c970176e9d4" dependencies = [ "chrono", "gloo-events", diff --git a/console-frontend/Cargo.toml b/console-frontend/Cargo.toml index ee9226128..75a1d79d1 100644 --- a/console-frontend/Cargo.toml +++ b/console-frontend/Cargo.toml @@ -29,7 +29,7 @@ log = "0.4" md5 = "0.7" monaco = { version = "0.3", features = ["yew-components"] } once_cell = "1" -patternfly-yew = "0.2.0" +patternfly-yew = "0.2.3" percent-encoding = "2.1" pretty-hex = "0.3" reqwest = "0.11" @@ -92,13 +92,13 @@ opt-level = 's' lto = true [patch.crates-io] -#patternfly-yew = { git = "https://github.com/ctron/patternfly-yew", rev = "32c809e030ca9ec5f7785dc7e0a78d574b9830f8" } # FIXME: awaiting release -#patternfly-yew = { path = "../../patternfly-yew" } +patternfly-yew = { git = "https://github.com/ctron/patternfly-yew", rev = "331782836f0ed67bc6ab41ef71497c970176e9d4" } # FIXME: awaiting release +#patternfly-yew = { path = "../../../patternfly-yew" } -drogue-client = { git = "https://github.com/drogue-iot/drogue-client", rev = "798c968f0a63a0debcff9965c66b361e85946458" } # FIXME: awaiting release 0.12.0 +drogue-client = { git = "https://github.com/drogue-iot/drogue-client", rev = "fdebb42a6cbaa872a779e892fefe0f687b34fa4b" } # FIXME: awaiting release 0.12.0 #drogue-client = { path = "../../drogue-client" } -drogue-bazaar = { git = "https://github.com/drogue-iot/drogue-bazaar", rev = "d19ad32f200938aeb5d7081ee3385ee40c5ae0ff" } # FIXME: awaiting release 0.4.0 +drogue-bazaar = { git = "https://github.com/drogue-iot/drogue-bazaar", rev = "8c6f6f6456a18fd8ab41ca2a64b45b154a94f4aa" } # FIXME: awaiting release 0.4.0 #drogue-bazaar = { path = "../../drogue-bazaar" } #monaco = { git = "https://github.com/siku2/rust-monaco", rev = "cb20108c317976ba8c3d05b581a84efd394c3dbe" } # FIXME: awaiting release diff --git a/console-frontend/src/pages/access_tokens/create.rs b/console-frontend/src/pages/access_tokens/create.rs index 72d2a81c8..750d00fe2 100644 --- a/console-frontend/src/pages/access_tokens/create.rs +++ b/console-frontend/src/pages/access_tokens/create.rs @@ -4,7 +4,7 @@ use crate::{ error::{error, ErrorNotification, ErrorNotifier}, pages::access_tokens::success::AccessTokenCreatedSuccessModal, }; -use drogue_cloud_service_api::token::AccessTokenCreated; +use drogue_cloud_service_api::token::CreatedAccessToken; use http::Method; use patternfly_yew::*; use yew::prelude::*; @@ -16,7 +16,7 @@ pub struct Props { } pub enum Msg { - Success(AccessTokenCreated), + Success(CreatedAccessToken), Error(ErrorNotification), Create, Description(String), @@ -74,7 +74,7 @@ impl Component for AccessTokenCreateModal {