Skip to main content

systemprompt_users/repository/
role_directory.rs

1//! The users domain's answer to "does anybody hold this role?" and "which
2//! roles does this user hold?", registered with the security crate at link
3//! time.
4//!
5//! Copyright (c) systemprompt.io — Business Source License 1.1.
6//! See <https://systemprompt.io> for licensing details.
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use sqlx::PgPool;
12use systemprompt_governance::authz::{
13    AuthzError, AuthzHookContext, RoleDirectory, SharedRoleDirectory,
14};
15use systemprompt_identifiers::UserId;
16
17#[derive(Debug)]
18pub struct UsersRoleDirectory {
19    pool: Arc<PgPool>,
20}
21
22impl UsersRoleDirectory {
23    #[must_use]
24    pub fn shared(ctx: &AuthzHookContext) -> SharedRoleDirectory {
25        Arc::new(Self {
26            pool: Arc::clone(&ctx.pool),
27        })
28    }
29}
30
31#[async_trait]
32impl RoleDirectory for UsersRoleDirectory {
33    async fn unknown_roles(&self, candidates: &[String]) -> Result<Vec<String>, AuthzError> {
34        let rows = sqlx::query!(
35            r#"
36            SELECT candidate AS "candidate!"
37            FROM UNNEST($1::text[]) AS candidate
38            WHERE NOT EXISTS (
39                SELECT 1 FROM users WHERE candidate = ANY(users.roles)
40            )
41            "#,
42            candidates,
43        )
44        .fetch_all(&*self.pool)
45        .await?;
46        Ok(rows.into_iter().map(|row| row.candidate).collect())
47    }
48
49    async fn roles_of(&self, user: &UserId) -> Result<Vec<String>, AuthzError> {
50        let roles = sqlx::query_scalar!("SELECT roles FROM users WHERE id=$1", user.as_str())
51            .fetch_optional(&*self.pool)
52            .await?;
53        Ok(roles.unwrap_or_default())
54    }
55}
56
57systemprompt_governance::register_role_directory!(UsersRoleDirectory::shared);