Skip to main content

systemprompt_cli/commands/admin/users/
mod.rs

1//! User administration command tree.
2//!
3//! [`UsersCommands`] groups the user CRUD, search, export, stats, merge, and
4//! the `bulk`, `role`, `session`, `ban`, and `webauthn` subcommand trees. On a
5//! `--database-url` invocation only the read-only commands are served; write
6//! operations require a full profile context.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod apikey;
12mod ban;
13mod bulk;
14mod count;
15mod create;
16pub(crate) mod delete;
17mod export;
18mod list;
19mod merge;
20mod restore;
21mod role;
22mod search;
23mod service_account;
24mod session;
25mod show;
26mod stats;
27mod types;
28mod update;
29mod webauthn;
30
31use crate::context::CommandContext;
32use crate::descriptor::DataImpact;
33use crate::shared::{CommandOutput, render_result};
34use anyhow::{Result, bail};
35use clap::Subcommand;
36
37pub use apikey::{ApiKeyCommands, IssueArgs as ApiKeyIssueArgs};
38pub use types::*;
39
40#[derive(Debug, Subcommand)]
41pub enum UsersCommands {
42    #[command(about = "List users with pagination and filtering")]
43    List(list::ListArgs),
44
45    #[command(about = "Show detailed user information")]
46    Show(show::ShowArgs),
47
48    #[command(about = "Search users by name, email, or full name")]
49    Search(search::SearchArgs),
50
51    #[command(about = "Create a new user")]
52    Create(create::CreateArgs),
53
54    #[command(about = "Update user fields")]
55    Update(update::UpdateArgs),
56
57    #[command(about = "Archive a user (restorable); --purge deletes an archived user")]
58    Delete(delete::DeleteArgs),
59
60    #[command(about = "Restore an archived user within the retention window")]
61    Restore(restore::RestoreArgs),
62
63    #[command(name = "legal-hold", about = "Place or release a legal hold on a user")]
64    LegalHold(restore::LegalHoldArgs),
65
66    #[command(about = "Get total user count")]
67    Count(count::CountArgs),
68
69    #[command(about = "Export users to JSON")]
70    Export(export::ExportArgs),
71
72    #[command(about = "Show user statistics dashboard")]
73    Stats,
74
75    #[command(about = "Merge source user into target user")]
76    Merge(merge::MergeArgs),
77
78    #[command(subcommand, about = "Bulk operations on users")]
79    Bulk(bulk::BulkCommands),
80
81    #[command(subcommand, about = "Role management commands")]
82    Role(role::RoleCommands),
83
84    #[command(subcommand, about = "Session management commands")]
85    Session(session::SessionCommands),
86
87    #[command(subcommand, about = "IP ban management commands")]
88    Ban(ban::BanCommands),
89
90    #[command(subcommand, about = "WebAuthn credential management commands")]
91    Webauthn(webauthn::WebauthnCommands),
92
93    #[command(
94        subcommand,
95        name = "api-key",
96        about = "Personal access token (sp-live-) management"
97    )]
98    ApiKey(ApiKeyCommands),
99
100    #[command(
101        subcommand,
102        name = "service-account",
103        about = "Service account management"
104    )]
105    ServiceAccount(service_account::ServiceAccountCommands),
106}
107
108pub async fn execute(cmd: UsersCommands, ctx: &CommandContext) -> Result<()> {
109    if ctx.is_database_scoped()
110        && matches!(
111            cmd,
112            UsersCommands::Create(_)
113                | UsersCommands::Update(_)
114                | UsersCommands::Delete(_)
115                | UsersCommands::Restore(_)
116                | UsersCommands::LegalHold(_)
117                | UsersCommands::Merge(_)
118                | UsersCommands::Bulk(_)
119                | UsersCommands::Webauthn(_)
120                | UsersCommands::ApiKey(_)
121                | UsersCommands::ServiceAccount(_)
122        )
123    {
124        bail!("Write operations require full profile context");
125    }
126
127    match cmd {
128        UsersCommands::Bulk(cmd) => Box::pin(bulk::execute(cmd, ctx)).await,
129        UsersCommands::Role(cmd) => Box::pin(role::execute(cmd, ctx)).await,
130        UsersCommands::Session(cmd) => Box::pin(session::execute(cmd, ctx)).await,
131        UsersCommands::Ban(cmd) => Box::pin(ban::execute(cmd, ctx)).await,
132        UsersCommands::Webauthn(cmd) => Box::pin(webauthn::execute(cmd, ctx)).await,
133        other => {
134            let output = Box::pin(render_output(other, ctx)).await?;
135            render_result(&output, &ctx.cli);
136            Ok(())
137        },
138    }
139}
140
141async fn render_output(cmd: UsersCommands, ctx: &CommandContext) -> Result<CommandOutput> {
142    match cmd {
143        UsersCommands::List(args) => list::execute(args, ctx).await,
144        UsersCommands::Show(args) => show::execute(args, ctx).await,
145        UsersCommands::Search(args) => search::execute(args, ctx).await,
146        UsersCommands::Create(args) => create::execute(args, ctx).await,
147        UsersCommands::Update(args) => update::execute(args, ctx).await,
148        UsersCommands::Delete(args) => delete::execute(args, ctx).await,
149        UsersCommands::Restore(args) => restore::execute(args, ctx).await,
150        UsersCommands::LegalHold(args) => restore::execute_legal_hold(args, ctx).await,
151        UsersCommands::Count(args) => count::execute(args, ctx).await,
152        UsersCommands::Export(args) => export::execute(args, ctx).await,
153        UsersCommands::Stats => stats::execute(ctx).await,
154        UsersCommands::Merge(args) => merge::execute(args, ctx).await,
155        UsersCommands::ApiKey(cmd) => apikey::execute(cmd, ctx).await,
156        UsersCommands::ServiceAccount(cmd) => service_account::execute(cmd, ctx).await,
157        UsersCommands::Bulk(_)
158        | UsersCommands::Role(_)
159        | UsersCommands::Session(_)
160        | UsersCommands::Ban(_)
161        | UsersCommands::Webauthn(_) => bail!(
162            "internal: a users subgroup reached the rendering dispatch, which only serves \
163             commands that produce a single output"
164        ),
165    }
166}
167
168impl UsersCommands {
169    pub const fn data_impact(&self) -> DataImpact {
170        match self {
171            Self::Delete(_)
172            | Self::Merge(_)
173            | Self::Bulk(bulk::BulkCommands::Delete(_) | bulk::BulkCommands::Update(_))
174            | Self::Role(
175                role::RoleCommands::Assign(_)
176                | role::RoleCommands::Promote(_)
177                | role::RoleCommands::Demote(_),
178            )
179            | Self::Session(session::SessionCommands::Cleanup(_))
180            | Self::Ban(ban::BanCommands::Cleanup(_)) => DataImpact::Destructive,
181            Self::List(_)
182            | Self::Show(_)
183            | Self::Search(_)
184            | Self::Create(_)
185            | Self::Update(_)
186            | Self::Restore(_)
187            | Self::LegalHold(_)
188            | Self::Count(_)
189            | Self::Export(_)
190            | Self::Stats
191            | Self::Session(session::SessionCommands::List(_) | session::SessionCommands::End(_))
192            | Self::Ban(
193                ban::BanCommands::List(_)
194                | ban::BanCommands::Add(_)
195                | ban::BanCommands::Remove(_)
196                | ban::BanCommands::Check(_),
197            )
198            | Self::Webauthn(webauthn::WebauthnCommands::GenerateSetupToken(_))
199            | Self::ApiKey(
200                ApiKeyCommands::Issue(_) | ApiKeyCommands::List(_) | ApiKeyCommands::Revoke(_),
201            )
202            | Self::ServiceAccount(_) => DataImpact::Preserving,
203        }
204    }
205}