Skip to main content

rustauth_plugins/admin/
mod.rs

1//! Server-side admin plugin.
2
3mod access;
4mod cookies;
5mod errors;
6mod handlers;
7mod models;
8mod openapi;
9mod options;
10mod response;
11mod routes;
12mod schema;
13mod store;
14
15pub use access::{has_permission, AdminRole, PermissionMap};
16pub use errors::ADMIN_ERROR_CODES;
17pub use models::{AdminSession, AdminUser};
18pub use options::{AdminOptions, AdminOptionsBuilder, AdminSchemaOptions};
19
20use rustauth_core::error::RustAuthError;
21use rustauth_core::plugin::{
22    AuthPlugin, PluginAfterHookAction, PluginDatabaseBeforeAction, PluginDatabaseBeforeInput,
23    PluginDatabaseHook,
24};
25use serde_json::Value;
26use time::OffsetDateTime;
27
28pub mod access_control {
29    pub use super::access::{
30        default_access_control, default_roles, default_statements, has_permission, AdminRole,
31        PermissionMap,
32    };
33}
34
35pub const UPSTREAM_PLUGIN_ID: &str = "admin";
36
37pub fn admin(options: AdminOptions) -> Result<AuthPlugin, RustAuthError> {
38    let options = options.with_defaults();
39    options.validate().map_err(RustAuthError::InvalidConfig)?;
40    Ok(build_admin_plugin(options))
41}
42
43fn build_admin_plugin(options: AdminOptions) -> AuthPlugin {
44    AuthPlugin::new(UPSTREAM_PLUGIN_ID)
45        .with_version(crate::VERSION)
46        .with_options(options.to_json())
47        .with_schema(schema::user_role_field(&options.schema))
48        .with_schema(schema::user_banned_field(&options.schema))
49        .with_schema(schema::user_ban_reason_field(&options.schema))
50        .with_schema(schema::user_ban_expires_field(&options.schema))
51        .with_schema(schema::session_impersonated_by_field(&options.schema))
52        .with_database_hook(default_role_hook(options.default_role.clone()))
53        .with_database_hook(banned_session_hook(options.banned_user_message.clone()))
54        .with_async_after_hook("/list-sessions", filter_impersonated_sessions_hook)
55        .with_endpoint(routes::set_role(options.clone()))
56        .with_endpoint(routes::get_user(options.clone()))
57        .with_endpoint(routes::create_user(options.clone()))
58        .with_endpoint(routes::update_user(options.clone()))
59        .with_endpoint(routes::list_users(options.clone()))
60        .with_endpoint(routes::list_user_sessions(options.clone()))
61        .with_endpoint(routes::ban_user(options.clone()))
62        .with_endpoint(routes::unban_user(options.clone()))
63        .with_endpoint(routes::impersonate_user(options.clone()))
64        .with_endpoint(routes::stop_impersonating())
65        .with_endpoint(routes::revoke_user_session(options.clone()))
66        .with_endpoint(routes::revoke_user_sessions(options.clone()))
67        .with_endpoint(routes::remove_user(options.clone()))
68        .with_endpoint(routes::set_user_password(options.clone()))
69        .with_endpoint(routes::has_permission_endpoint(options.clone()))
70        .with_error_code(errors::failed_to_create_user())
71        .with_error_code(errors::user_already_exists())
72        .with_error_code(errors::user_already_exists_use_another_email())
73        .with_error_code(errors::cannot_ban_yourself())
74        .with_error_code(errors::not_allowed_to_change_role())
75        .with_error_code(errors::not_allowed_to_create_users())
76        .with_error_code(errors::not_allowed_to_list_users())
77        .with_error_code(errors::not_allowed_to_list_sessions())
78        .with_error_code(errors::not_allowed_to_ban_users())
79        .with_error_code(errors::not_allowed_to_impersonate_users())
80        .with_error_code(errors::not_allowed_to_revoke_sessions())
81        .with_error_code(errors::not_allowed_to_delete_users())
82        .with_error_code(errors::not_allowed_to_set_password())
83        .with_error_code(errors::banned_user(&options.banned_user_message))
84        .with_error_code(errors::not_allowed_to_get_user())
85        .with_error_code(errors::no_data_to_update())
86        .with_error_code(errors::not_allowed_to_update_users())
87        .with_error_code(errors::cannot_remove_yourself())
88        .with_error_code(errors::not_allowed_to_set_unknown_role())
89        .with_error_code(errors::cannot_impersonate_admins())
90        .with_error_code(errors::invalid_role_type())
91}
92
93fn default_role_hook(default_role: String) -> PluginDatabaseHook {
94    PluginDatabaseHook::before_create("admin_default_user_role", move |_context, mut query| {
95        if query.model == "user" && !query.data.contains_key("role") {
96            query.data.insert(
97                "role".to_owned(),
98                rustauth_core::db::DbValue::String(default_role.clone()),
99            );
100        }
101        Ok(PluginDatabaseBeforeAction::Continue(
102            PluginDatabaseBeforeInput::Create(query),
103        ))
104    })
105}
106
107fn banned_session_hook(message: String) -> PluginDatabaseHook {
108    PluginDatabaseHook::before_create_async(
109        "admin_block_banned_user_session",
110        move |context, query| {
111            let message = message.clone();
112            Box::pin(async move {
113                if query.model != "session" {
114                    return Ok(PluginDatabaseBeforeAction::Continue(
115                        PluginDatabaseBeforeInput::Create(query),
116                    ));
117                }
118                let Some(rustauth_core::db::DbValue::String(user_id)) = query.data.get("user_id")
119                else {
120                    return Ok(PluginDatabaseBeforeAction::Continue(
121                        PluginDatabaseBeforeInput::Create(query),
122                    ));
123                };
124                let store = store::AdminStore::new(context.adapter);
125                let Some(user) = store.find_user_by_id(user_id).await? else {
126                    return Ok(PluginDatabaseBeforeAction::Continue(
127                        PluginDatabaseBeforeInput::Create(query),
128                    ));
129                };
130                if !user.banned {
131                    return Ok(PluginDatabaseBeforeAction::Continue(
132                        PluginDatabaseBeforeInput::Create(query),
133                    ));
134                }
135                if user
136                    .ban_expires
137                    .is_some_and(|expires| expires < OffsetDateTime::now_utc())
138                {
139                    store.unban_user(user_id).await?;
140                    return Ok(PluginDatabaseBeforeAction::Continue(
141                        PluginDatabaseBeforeInput::Create(query),
142                    ));
143                }
144                if context.request_path.as_deref().is_some_and(|path| {
145                    path.starts_with("/callback") || path.starts_with("/oauth2/callback")
146                }) {
147                    return Ok(PluginDatabaseBeforeAction::Cancel(RustAuthError::Api(
148                        format!("BANNED_USER: {message}"),
149                    )));
150                }
151                Ok(PluginDatabaseBeforeAction::Cancel(RustAuthError::Api(
152                    format!("BANNED_USER: {message}"),
153                )))
154            })
155        },
156    )
157}
158
159fn filter_impersonated_sessions_hook<'a>(
160    context: &'a rustauth_core::context::AuthContext,
161    _request: &'a rustauth_core::api::ApiRequest,
162    response: rustauth_core::api::ApiResponse,
163) -> rustauth_core::plugin::PluginAfterHookFuture<'a> {
164    Box::pin(async move {
165        let Some(adapter) = context.adapter() else {
166            return Ok(PluginAfterHookAction::Continue(response));
167        };
168        let (parts, body) = response.into_parts();
169        let Ok(Value::Array(sessions)) = serde_json::from_slice::<Value>(&body) else {
170            return Ok(PluginAfterHookAction::Continue(http::Response::from_parts(
171                parts, body,
172            )));
173        };
174        let store = store::AdminStore::new(adapter.as_ref());
175        let mut filtered = Vec::new();
176        for session in sessions {
177            let Some(token) = session.get("token").and_then(Value::as_str) else {
178                filtered.push(session);
179                continue;
180            };
181            match store.find_session(token).await? {
182                Some((admin_session, _)) if admin_session.impersonated_by.is_none() => {
183                    filtered.push(session);
184                }
185                None => filtered.push(session),
186                Some(_) => {}
187            }
188        }
189        let body = serde_json::to_vec(&filtered).map_err(|error| {
190            RustAuthError::Api(format!("failed to serialize filtered sessions: {error}"))
191        })?;
192        Ok(PluginAfterHookAction::Continue(http::Response::from_parts(
193            parts, body,
194        )))
195    })
196}