openauth_plugins/admin/
mod.rs1mod 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}