Skip to main content

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