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.
|
||||
'';
|
||||
};
|
||||
allow_user_classes = mkOption {
|
||||
allow_user_roles = mkOption {
|
||||
type = types.listOf types.str;
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ rsa_keypair = "keypair.pem"
|
||||
[oidc.clients.x]
|
||||
name = "some service"
|
||||
redirect_uris = ["http://localhost:9876/callback"]
|
||||
allow_user_classes = ["user"]
|
||||
allow_user_roles = ["*"]
|
||||
|
||||
[postgres]
|
||||
connect = "postgres:"
|
||||
|
@ -109,12 +109,14 @@ pub struct OidcClientConfiguration {
|
||||
/// Friendly name for the service. Will be shown in the user interface.
|
||||
pub name: String,
|
||||
|
||||
/// TODO User classes to allow
|
||||
/// 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).
|
||||
/// (Design subject to change)
|
||||
/// TODO not sure this design is current
|
||||
pub allow_user_classes: Vec<String>,
|
||||
/// User roles to allow to access this application.
|
||||
///
|
||||
/// The `*` 'role' can be used to allow all users to access this application.
|
||||
///
|
||||
/// Must always be explicitly specified.
|
||||
///
|
||||
/// Warning: This setting does not currently apply retrospectively / to existing sessions.
|
||||
pub allow_user_roles: Vec<String>,
|
||||
|
||||
/// The shared secret for the client.
|
||||
/// 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.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::Utc;
|
||||
@ -412,4 +414,20 @@ impl IdCoopStoreTxn<'_, '_> {
|
||||
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::{
|
||||
config::{Configuration, OidcClientConfiguration},
|
||||
store::IdCoopStore,
|
||||
utils::{Clock, RandGen},
|
||||
web::{
|
||||
ambient::Ambient,
|
||||
login::LoginSession,
|
||||
make_login_redirect,
|
||||
oauth_openid::ext_codes::{AuthCode, AuthCodeBinding},
|
||||
DesiredLocale, Rendered, TEMPLATING,
|
||||
DesiredLocale, Rendered, WebResult, TEMPLATING,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ext_codes::VolatileCodeStore;
|
||||
|
||||
pub const EVERYONE_ROLE: &str = "*";
|
||||
|
||||
/// Query string parameters for the OIDC authorisation request.
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AuthorisationQuery {
|
||||
@ -75,29 +78,30 @@ pub(crate) async fn oidc_authorisation(
|
||||
Extension(config): Extension<Arc<Configuration>>,
|
||||
Extension(code_store): Extension<VolatileCodeStore>,
|
||||
Extension(clock): Extension<Clock>,
|
||||
Extension(store): Extension<Arc<IdCoopStore>>,
|
||||
Extension(mut randgen): Extension<RandGen>,
|
||||
DesiredLocale(locale): DesiredLocale,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Response {
|
||||
) -> WebResult<Response> {
|
||||
let Query(query) = match query {
|
||||
Ok(query) => query,
|
||||
Err(err) => {
|
||||
// TODO(ui) this should be a pretty page
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("TODO bad authorisation request: {err:?}"),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(nonce) = &query.nonce {
|
||||
if nonce.len() > MAX_NONCE_LENGTH {
|
||||
return (
|
||||
return Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
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) {
|
||||
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.
|
||||
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 !client_config.skip_consent {
|
||||
return show_consent_page(
|
||||
return Ok(show_consent_page(
|
||||
ambient,
|
||||
login_session,
|
||||
client_config,
|
||||
@ -126,12 +163,12 @@ pub(crate) async fn oidc_authorisation(
|
||||
&config,
|
||||
&query.redirect_uri,
|
||||
)
|
||||
.await;
|
||||
.await);
|
||||
}
|
||||
|
||||
// No consent needed: process the authorisation.
|
||||
|
||||
process_authorisation(
|
||||
Ok(process_authorisation(
|
||||
query,
|
||||
login_session,
|
||||
client_id,
|
||||
@ -141,7 +178,7 @@ pub(crate) async fn oidc_authorisation(
|
||||
&clock,
|
||||
&code_store,
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
|
||||
/// 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