Support access control based on user roles
This commit is contained in:
parent
82ae441cd6
commit
301302f1d0
22
.sqlx/query-3a0c05d42c2f5cb868649a5c2a216d8a3548e0a90adaf27ba526cb3894b56a6b.json
generated
Normal file
22
.sqlx/query-3a0c05d42c2f5cb868649a5c2a216d8a3548e0a90adaf27ba526cb3894b56a6b.json
generated
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT role_id\n FROM users_roles\n WHERE user_id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "role_id",
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3a0c05d42c2f5cb868649a5c2a216d8a3548e0a90adaf27ba526cb3894b56a6b"
|
||||||
|
}
|
@ -26,10 +26,10 @@ let
|
|||||||
Consult the documentation for the other service if you aren't sure.
|
Consult the documentation for the other service if you aren't sure.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
allow_user_classes = mkOption {
|
allow_user_roles = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
description = ''
|
description = ''
|
||||||
List of user classes which are authorised (allowed) to use this client (access this service).
|
List of user roles which are authorised (allowed) to use this client (access this service).
|
||||||
As of idCoop v0.0.1, this setting is unimplemented and has no effect.
|
As of idCoop v0.0.1, this setting is unimplemented and has no effect.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@ rsa_keypair = "keypair.pem"
|
|||||||
[oidc.clients.x]
|
[oidc.clients.x]
|
||||||
name = "some service"
|
name = "some service"
|
||||||
redirect_uris = ["http://localhost:9876/callback"]
|
redirect_uris = ["http://localhost:9876/callback"]
|
||||||
allow_user_classes = ["user"]
|
allow_user_roles = ["*"]
|
||||||
|
|
||||||
[postgres]
|
[postgres]
|
||||||
connect = "postgres:"
|
connect = "postgres:"
|
||||||
|
@ -109,12 +109,14 @@ pub struct OidcClientConfiguration {
|
|||||||
/// Friendly name for the service. Will be shown in the user interface.
|
/// Friendly name for the service. Will be shown in the user interface.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// TODO User classes to allow
|
/// User roles to allow to access this application.
|
||||||
/// Must be explicit because it is security sensitive and we don't want a typo to fail open.
|
///
|
||||||
/// User classes are defined by the admin but at the very least includes 'active' and 'not active' (implied if no 'active' class set).
|
/// The `*` 'role' can be used to allow all users to access this application.
|
||||||
/// (Design subject to change)
|
///
|
||||||
/// TODO not sure this design is current
|
/// Must always be explicitly specified.
|
||||||
pub allow_user_classes: Vec<String>,
|
///
|
||||||
|
/// Warning: This setting does not currently apply retrospectively / to existing sessions.
|
||||||
|
pub allow_user_roles: Vec<String>,
|
||||||
|
|
||||||
/// The shared secret for the client.
|
/// The shared secret for the client.
|
||||||
/// Must be populated (this is checked on startup).
|
/// Must be populated (this is checked on startup).
|
||||||
|
18
src/store.rs
18
src/store.rs
@ -2,6 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! This file contains PostgreSQL queries.
|
//! This file contains PostgreSQL queries.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@ -412,4 +414,20 @@ impl IdCoopStoreTxn<'_, '_> {
|
|||||||
user_id: row.user_id,
|
user_id: row.user_id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches all role_ids of roles that the given user has been granted.
|
||||||
|
pub async fn get_user_role_ids(&mut self, user_id: Uuid) -> eyre::Result<BTreeSet<String>> {
|
||||||
|
sqlx::query_scalar!(
|
||||||
|
"
|
||||||
|
SELECT role_id
|
||||||
|
FROM users_roles
|
||||||
|
WHERE user_id = $1
|
||||||
|
",
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_all(&mut **self.txn)
|
||||||
|
.await
|
||||||
|
.context("failed to fetch roleset of user")
|
||||||
|
.map(|v| v.into_iter().collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,18 +17,21 @@ use tracing::{error, warn};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Configuration, OidcClientConfiguration},
|
config::{Configuration, OidcClientConfiguration},
|
||||||
|
store::IdCoopStore,
|
||||||
utils::{Clock, RandGen},
|
utils::{Clock, RandGen},
|
||||||
web::{
|
web::{
|
||||||
ambient::Ambient,
|
ambient::Ambient,
|
||||||
login::LoginSession,
|
login::LoginSession,
|
||||||
make_login_redirect,
|
make_login_redirect,
|
||||||
oauth_openid::ext_codes::{AuthCode, AuthCodeBinding},
|
oauth_openid::ext_codes::{AuthCode, AuthCodeBinding},
|
||||||
DesiredLocale, Rendered, TEMPLATING,
|
DesiredLocale, Rendered, WebResult, TEMPLATING,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ext_codes::VolatileCodeStore;
|
use super::ext_codes::VolatileCodeStore;
|
||||||
|
|
||||||
|
pub const EVERYONE_ROLE: &str = "*";
|
||||||
|
|
||||||
/// Query string parameters for the OIDC authorisation request.
|
/// Query string parameters for the OIDC authorisation request.
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct AuthorisationQuery {
|
pub struct AuthorisationQuery {
|
||||||
@ -75,29 +78,30 @@ pub(crate) async fn oidc_authorisation(
|
|||||||
Extension(config): Extension<Arc<Configuration>>,
|
Extension(config): Extension<Arc<Configuration>>,
|
||||||
Extension(code_store): Extension<VolatileCodeStore>,
|
Extension(code_store): Extension<VolatileCodeStore>,
|
||||||
Extension(clock): Extension<Clock>,
|
Extension(clock): Extension<Clock>,
|
||||||
|
Extension(store): Extension<Arc<IdCoopStore>>,
|
||||||
Extension(mut randgen): Extension<RandGen>,
|
Extension(mut randgen): Extension<RandGen>,
|
||||||
DesiredLocale(locale): DesiredLocale,
|
DesiredLocale(locale): DesiredLocale,
|
||||||
OriginalUri(uri): OriginalUri,
|
OriginalUri(uri): OriginalUri,
|
||||||
) -> Response {
|
) -> WebResult<Response> {
|
||||||
let Query(query) = match query {
|
let Query(query) = match query {
|
||||||
Ok(query) => query,
|
Ok(query) => query,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// TODO(ui) this should be a pretty page
|
// TODO(ui) this should be a pretty page
|
||||||
return (
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("TODO bad authorisation request: {err:?}"),
|
format!("TODO bad authorisation request: {err:?}"),
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(nonce) = &query.nonce {
|
if let Some(nonce) = &query.nonce {
|
||||||
if nonce.len() > MAX_NONCE_LENGTH {
|
if nonce.len() > MAX_NONCE_LENGTH {
|
||||||
return (
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("Bad authorisation request: Nonce too long (> {MAX_NONCE_LENGTH})"),
|
format!("Bad authorisation request: Nonce too long (> {MAX_NONCE_LENGTH})"),
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,17 +111,50 @@ pub(crate) async fn oidc_authorisation(
|
|||||||
|
|
||||||
let (client_id, client_config) = match validate_authorisation_basics(&query, &config) {
|
let (client_id, client_config) = match validate_authorisation_basics(&query, &config) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(resp) => return resp,
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the user isn't logged in, we need to get them to do that first and then come back here.
|
// If the user isn't logged in, we need to get them to do that first and then come back here.
|
||||||
let Some(login_session) = login_session else {
|
let Some(login_session) = login_session else {
|
||||||
return make_login_redirect(uri);
|
return Ok(make_login_redirect(uri));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if the user has the correct role to access this application
|
||||||
|
if !client_config
|
||||||
|
.allow_user_roles
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == EVERYONE_ROLE)
|
||||||
|
{
|
||||||
|
// The application isn't available to the EVERYONE (*) role, so we need to
|
||||||
|
// consider the user's specific roles and match them against the list.
|
||||||
|
let user_roles = store
|
||||||
|
.txn(|mut txn| {
|
||||||
|
Box::pin(async move { txn.get_user_role_ids(login_session.user_id).await })
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !client_config
|
||||||
|
.allow_user_roles
|
||||||
|
.iter()
|
||||||
|
.any(|role| user_roles.contains(role))
|
||||||
|
{
|
||||||
|
// User doesn't have the right role
|
||||||
|
return Ok((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Rendered(render_template_string!(
|
||||||
|
TEMPLATING,
|
||||||
|
access_wrong_role,
|
||||||
|
locale,
|
||||||
|
{ ambient, client_name: client_config.name.clone() }
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the application requires consent, then we should ask for that.
|
// If the application requires consent, then we should ask for that.
|
||||||
if !client_config.skip_consent {
|
if !client_config.skip_consent {
|
||||||
return show_consent_page(
|
return Ok(show_consent_page(
|
||||||
ambient,
|
ambient,
|
||||||
login_session,
|
login_session,
|
||||||
client_config,
|
client_config,
|
||||||
@ -126,12 +163,12 @@ pub(crate) async fn oidc_authorisation(
|
|||||||
&config,
|
&config,
|
||||||
&query.redirect_uri,
|
&query.redirect_uri,
|
||||||
)
|
)
|
||||||
.await;
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No consent needed: process the authorisation.
|
// No consent needed: process the authorisation.
|
||||||
|
|
||||||
process_authorisation(
|
Ok(process_authorisation(
|
||||||
query,
|
query,
|
||||||
login_session,
|
login_session,
|
||||||
client_id,
|
client_id,
|
||||||
@ -141,7 +178,7 @@ pub(crate) async fn oidc_authorisation(
|
|||||||
&clock,
|
&clock,
|
||||||
&code_store,
|
&code_store,
|
||||||
)
|
)
|
||||||
.await
|
.await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body parameters for OIDC authorisation consent.
|
/// Body parameters for OIDC authorisation consent.
|
||||||
|
13
templates/pages/access_wrong_role.hnb
Normal file
13
templates/pages/access_wrong_role.hnb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CentredPage {$ambient}
|
||||||
|
:title
|
||||||
|
@access_wrong_role_title
|
||||||
|
|
||||||
|
:main
|
||||||
|
h1
|
||||||
|
@access_wrong_role_title
|
||||||
|
|
||||||
|
article
|
||||||
|
@access_wrong_role_main1
|
||||||
|
strong
|
||||||
|
"${$client_name}"
|
||||||
|
@access_wrong_role_main2
|
4
translations/en/oidc_auth.ftl
Normal file
4
translations/en/oidc_auth.ftl
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
access_wrong_role_title = Access denied
|
||||||
|
|
||||||
|
access_wrong_role_main1 = You don't have the right role to access{" "}
|
||||||
|
access_wrong_role_main2 = . Contact your administrator for assistance if this is unexpected.
|
Loading…
x
Reference in New Issue
Block a user