Skip to main content

rustauth_cli/
plugins.rs

1use rustauth_core::error::RustAuthError;
2use rustauth_core::plugin::{AuthPlugin, PluginMigration};
3#[cfg(feature = "oauth-provider")]
4use rustauth_oauth_provider::{oauth_provider, OAuthProviderOptions};
5#[cfg(feature = "passkey")]
6use rustauth_passkey::{passkey, PasskeyOptions};
7#[cfg(feature = "plugins")]
8use rustauth_plugins::official_schema_plugin;
9#[cfg(feature = "scim")]
10use rustauth_scim::{scim, ScimOptions};
11#[cfg(feature = "sso")]
12use rustauth_sso::{sso, SsoOptions};
13#[cfg(feature = "stripe")]
14use rustauth_stripe::{stripe, OrganizationStripeOptions, StripeOptions, SubscriptionOptions};
15use serde::Serialize;
16
17const CLI_SCHEMA_EXTENSION_PLUGIN_IDS: &[&str] =
18    &["oauth-provider", "passkey", "scim", "sso", "stripe"];
19
20/// Keep in sync with `rustauth_plugins::PLUGIN_IDS` in `rustauth-plugins/src/lib.rs`.
21#[cfg(not(feature = "plugins"))]
22const STATIC_OFFICIAL_PLUGIN_IDS: &[&str] = &[
23    "access",
24    "additional-fields",
25    "admin",
26    "anonymous",
27    "api-key",
28    "bearer",
29    "captcha",
30    "custom-session",
31    "device-authorization",
32    "email-otp",
33    "generic-oauth",
34    "have-i-been-pwned",
35    "jwt",
36    "last-login-method",
37    "magic-link",
38    "multi-session",
39    "oauth-proxy",
40    "one-tap",
41    "one-time-token",
42    "open-api",
43    "organization",
44    "phone-number",
45    "siwe",
46    "two-factor",
47    "username",
48];
49
50/// Plugin ids that [`official_schema_plugin`] materializes when the `plugins` feature is on.
51const STATIC_OFFICIAL_SCHEMA_PLUGIN_IDS: &[&str] = &[
52    "admin",
53    "anonymous",
54    "api-key",
55    "device-authorization",
56    "jwt",
57    "last-login-method",
58    "organization",
59    "phone-number",
60    "siwe",
61    "two-factor",
62    "username",
63];
64
65#[derive(Debug, Clone, Serialize)]
66pub struct PluginInfo {
67    pub id: &'static str,
68    pub official: bool,
69    pub schema_supported: bool,
70    pub snippet_supported: bool,
71    pub migration_impact: bool,
72}
73
74pub fn official_plugins() -> Vec<PluginInfo> {
75    official_plugin_ids()
76        .iter()
77        .map(|id| plugin_info(id))
78        .chain(
79            CLI_SCHEMA_EXTENSION_PLUGIN_IDS
80                .iter()
81                .map(|id| plugin_info(id)),
82        )
83        .collect()
84}
85
86fn official_plugin_ids() -> &'static [&'static str] {
87    #[cfg(feature = "plugins")]
88    {
89        rustauth_plugins::PLUGIN_IDS
90    }
91    #[cfg(not(feature = "plugins"))]
92    {
93        STATIC_OFFICIAL_PLUGIN_IDS
94    }
95}
96
97fn plugin_info(id: &'static str) -> PluginInfo {
98    let schema_supported = supports_schema_planning(id);
99    PluginInfo {
100        id,
101        official: true,
102        schema_supported,
103        snippet_supported: rust_snippet(id).is_some(),
104        migration_impact: schema_supported,
105    }
106}
107
108pub fn is_official_plugin(plugin: &str) -> bool {
109    official_plugin_ids().contains(&plugin) || CLI_SCHEMA_EXTENSION_PLUGIN_IDS.contains(&plugin)
110}
111
112pub fn schema_plugin(plugin: &str) -> Option<AuthPlugin> {
113    cli_schema_plugin(plugin).and_then(|result| result.ok())
114}
115
116/// Returns true when `rustauth db` can derive schema for this plugin id.
117pub fn supports_schema_planning(plugin: &str) -> bool {
118    matches!(cli_schema_plugin(plugin), Some(Ok(_)))
119}
120
121/// Plugin ids that intentionally have no fixed CLI schema (or are app-configured).
122pub(crate) fn is_schema_planning_exception(plugin: &str) -> bool {
123    #[cfg(feature = "plugins")]
124    {
125        use rustauth_plugins::{APP_CONFIGURED_SCHEMA_PLUGIN_IDS, NO_FIXED_SCHEMA_PLUGIN_IDS};
126        NO_FIXED_SCHEMA_PLUGIN_IDS.contains(&plugin)
127            || APP_CONFIGURED_SCHEMA_PLUGIN_IDS.contains(&plugin)
128    }
129    #[cfg(not(feature = "plugins"))]
130    {
131        const NO_FIXED_SCHEMA_PLUGIN_IDS: &[&str] = &[
132            "bearer",
133            "captcha",
134            "custom-session",
135            "email-otp",
136            "generic-oauth",
137            "have-i-been-pwned",
138            "magic-link",
139            "multi-session",
140            "oauth-proxy",
141            "one-tap",
142            "one-time-token",
143            "open-api",
144        ];
145        const APP_CONFIGURED_SCHEMA_PLUGIN_IDS: &[&str] = &["additional-fields"];
146        NO_FIXED_SCHEMA_PLUGIN_IDS.contains(&plugin)
147            || APP_CONFIGURED_SCHEMA_PLUGIN_IDS.contains(&plugin)
148    }
149}
150
151fn cli_schema_plugin(plugin: &str) -> Option<Result<AuthPlugin, RustAuthError>> {
152    if let Err(error) = ensure_plugin_feature_enabled(plugin) {
153        return Some(Err(error));
154    }
155
156    #[cfg(feature = "plugins")]
157    if let Some(plugin) = official_schema_plugin(plugin) {
158        return Some(plugin);
159    }
160
161    match plugin {
162        #[cfg(feature = "oauth-provider")]
163        "oauth-provider" => Some(
164            oauth_provider(OAuthProviderOptions {
165                login_page: "/login".to_owned(),
166                consent_page: "/consent".to_owned(),
167                disable_jwt_plugin: true,
168                ..OAuthProviderOptions::default()
169            })
170            .map_err(|error| RustAuthError::InvalidConfig(error.to_string())),
171        ),
172        #[cfg(feature = "passkey")]
173        "passkey" => Some(Ok(passkey(PasskeyOptions::default()))),
174        #[cfg(feature = "scim")]
175        "scim" => Some(Ok(scim(ScimOptions::default()))),
176        #[cfg(feature = "sso")]
177        "sso" => Some(Ok(sso(SsoOptions::default()))),
178        #[cfg(feature = "stripe")]
179        "stripe" => Some(
180            stripe(
181                StripeOptions::dev()
182                    .subscription(SubscriptionOptions::enabled(Vec::new()))
183                    .organization(OrganizationStripeOptions::enabled()),
184            )
185            .map_err(|error| RustAuthError::InvalidConfig(error.to_string())),
186        ),
187        _ => None,
188    }
189}
190
191fn ensure_plugin_feature_enabled(plugin: &str) -> Result<(), RustAuthError> {
192    let Some(feature) = required_cargo_feature(plugin) else {
193        return Ok(());
194    };
195    if is_cargo_feature_enabled(feature) {
196        Ok(())
197    } else {
198        Err(RustAuthError::FeatureDisabled { feature })
199    }
200}
201
202pub(crate) fn required_cargo_feature(plugin: &str) -> Option<&'static str> {
203    if STATIC_OFFICIAL_SCHEMA_PLUGIN_IDS.contains(&plugin) {
204        return Some("plugins");
205    }
206    match plugin {
207        "oauth-provider" => Some("oauth-provider"),
208        "passkey" => Some("passkey"),
209        "scim" => Some("scim"),
210        "sso" => Some("sso"),
211        "stripe" => Some("stripe"),
212        _ => None,
213    }
214}
215
216#[allow(clippy::match_like_matches_macro)]
217pub(crate) fn is_cargo_feature_enabled(feature: &str) -> bool {
218    match feature {
219        "plugins" => cfg!(feature = "plugins"),
220        "oauth-provider" => cfg!(feature = "oauth-provider"),
221        "passkey" => cfg!(feature = "passkey"),
222        "scim" => cfg!(feature = "scim"),
223        "sso" => cfg!(feature = "sso"),
224        "stripe" => cfg!(feature = "stripe"),
225        _ => false,
226    }
227}
228
229pub fn schema_context_for_config(
230    plugin_ids: &[String],
231) -> Result<rustauth_core::context::AuthContext, RustAuthError> {
232    use std::sync::Arc;
233
234    use rustauth_core::context::create_auth_context_with_adapter;
235    use rustauth_core::db::MemoryAdapter;
236    use rustauth_core::options::RustAuthOptions;
237
238    let mut plugins = Vec::new();
239    for plugin_id in plugin_ids {
240        let Some(plugin) = cli_schema_plugin(plugin_id) else {
241            continue;
242        };
243        plugins.push(plugin?);
244    }
245    create_auth_context_with_adapter(
246        RustAuthOptions::new()
247            .development(true)
248            .secret("rustauth-cli-schema-planning-secret-32ch")
249            .plugins(plugins),
250        Arc::new(MemoryAdapter::new()),
251    )
252}
253
254pub fn plugin_migrations_for_config(
255    plugin_ids: &[String],
256) -> Result<Vec<PluginMigration>, RustAuthError> {
257    Ok(schema_context_for_config(plugin_ids)?.plugin_migrations)
258}
259
260pub fn rust_snippet(plugin: &str) -> Option<&'static str> {
261    match plugin {
262        "two-factor" => Some("rustauth::plugins::two_factor::two_factor(TwoFactorOptions::default())"),
263        "organization" => Some("rustauth::plugins::organization::organization(OrganizationOptions::default())"),
264        "username" => Some("rustauth::plugins::username::username(UsernameOptions::default())"),
265        "admin" => Some("rustauth::plugins::admin::admin(AdminOptions::default())?"),
266        "api-key" => Some("rustauth::plugins::api_key::api_key(ApiKeyOptions::default())?"),
267        "device-authorization" => {
268            Some("rustauth::plugins::device_authorization::device_authorization(DeviceAuthorizationOptions::default())?")
269        }
270        "anonymous" => Some("rustauth::plugins::anonymous::anonymous(AnonymousOptions::default())"),
271        "jwt" => Some("rustauth::plugins::jwt::jwt(JwtOptions::default())?"),
272        _ => None,
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn official_plugins_lists_static_ids_without_plugins_feature() {
282        let plugins = official_plugins();
283        assert!(plugins.iter().any(|plugin| plugin.id == "admin"));
284        assert!(plugins.iter().any(|plugin| plugin.id == "passkey"));
285        assert!(plugins.iter().any(|plugin| plugin.id == "username"));
286    }
287
288    #[cfg(feature = "plugins")]
289    #[test]
290    fn official_schema_registry_covers_extension_plugins() {
291        for id in CLI_SCHEMA_EXTENSION_PLUGIN_IDS {
292            assert!(
293                cli_schema_plugin(id).is_some(),
294                "missing schema plugin for {id}"
295            );
296        }
297    }
298
299    #[cfg(feature = "plugins")]
300    #[test]
301    fn official_schema_plugin_ids_match_registry() {
302        use rustauth_plugins::{is_official_schema_plugin, official_schema_plugin_ids};
303
304        for id in official_schema_plugin_ids() {
305            assert!(is_official_schema_plugin(id));
306        }
307    }
308
309    #[cfg(not(feature = "passkey"))]
310    #[test]
311    fn passkey_schema_requires_passkey_feature() {
312        assert!(matches!(
313            cli_schema_plugin("passkey"),
314            Some(Err(RustAuthError::FeatureDisabled { feature: "passkey" }))
315        ));
316        assert!(!supports_schema_planning("passkey"));
317    }
318}