Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI - Just-in-time login flow #2158

Merged
merged 26 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33e7ac5
[bfops/anonymous-cli]: (WIP) just-in-time login flow
bfops Jan 22, 2025
09518b4
[bfops/anonymous-cli]: builds
bfops Jan 22, 2025
0a78157
[bfops/anonymous-cli]: (WIP) respect --yes
bfops Jan 22, 2025
5422463
[bfops/anonymous-cli]: finish including --yes
bfops Jan 22, 2025
4179620
[bfops/anonymous-cli]: fix subscribe?
bfops Jan 22, 2025
72873d1
[bfops/anonymous-cli]: Merge remote-tracking branch 'origin/master' i…
bfops Jan 22, 2025
99cf667
[bfops/anonymous-cli]: remove type annotation
bfops Jan 22, 2025
8c98982
[bfops/anonymous-cli]: remove stale comment
bfops Jan 22, 2025
72c06b2
Merge branch 'master' into bfops/anonymous-cli
bfops Jan 22, 2025
575ef8e
Merge branch 'master' into bfops/anonymous-cli
bfops Jan 23, 2025
c5de7a9
[bfops/anonymous-cli]: review
bfops Jan 23, 2025
b557ff0
[bfops/anonymous-cli]: review
bfops Jan 23, 2025
10561e0
[bfops/anonymous-cli]: review
bfops Jan 23, 2025
74f013f
[bfops/anonymous-cli]: fix `login show`
bfops Jan 23, 2025
80fda32
[bfops/anonymous-cli]: warnings
bfops Jan 23, 2025
6eb7ed8
[bfops/anonymous-cli]: error for anon_identity publishing
bfops Jan 23, 2025
05bde2a
[bfops/anonymous-cli]: lint
bfops Jan 23, 2025
281aada
[bfops/anonymous-cli]: update help text
bfops Jan 24, 2025
9a3bb27
[bfops/anonymous-cli]: review
bfops Jan 24, 2025
4d63e39
[bfops/anonymous-cli]: comment
bfops Jan 24, 2025
7e0b131
[bfops/anonymous-cli]: warning message on direct login
bfops Jan 30, 2025
b333e52
[bfops/anonymous-cli]: Merge remote-tracking branch 'origin/master' i…
bfops Jan 30, 2025
ee9843f
[bfops/anonymous-cli]: Update PermissionDenied case/message
bfops Jan 30, 2025
c37a46f
[bfops/anonymous-cli]: clarity tweaks, comments
bfops Jan 31, 2025
35e063d
[bfops/anonymous-cli]: lints
bfops Jan 31, 2025
a688831
Merge branch 'master' into bfops/anonymous-cli
bfops Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cli/src/common_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ pub fn yes() -> Arg {
.long("yes")
.short('y')
.action(SetTrue)
.help("Assume \"yes\" as answer to all prompts and run non-interactively")
.help("Run non-interactively wherever possible. This will answer \"yes\" to almost all prompts.")
cloutiertyler marked this conversation as resolved.
Show resolved Hide resolved
}
8 changes: 0 additions & 8 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,14 +820,6 @@ Update the server's fingerprint with:
pub fn spacetimedb_token(&self) -> Option<&String> {
self.home.spacetimedb_token.as_ref()
}

pub fn spacetimedb_token_or_error(&self) -> anyhow::Result<&String> {
if let Some(token) = self.spacetimedb_token() {
Ok(token)
} else {
Err(anyhow::anyhow!("No login token found. Please run `spacetime login`."))
}
}
}

#[cfg(test)]
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/subcommands/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub fn cli() -> clap::Command {
.arg(Arg::new("arguments").help("arguments formatted as JSON").num_args(1..))
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::anonymous())
.arg(common_args::yes())
.after_help("Run `spacetime help call` for more detailed information.\n")
}

Expand All @@ -38,6 +39,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
let reducer_name = args.get_one::<String>("reducer_name").unwrap();
let arguments = args.get_many::<String>("arguments");
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");

let anon_identity = args.get_flag("anon_identity");

Expand All @@ -49,14 +51,15 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), Error> {
database_identity.clone(),
reducer_name
));
let auth_header = get_auth_header(&config, anon_identity)?;
let auth_header = get_auth_header(&mut config, anon_identity, server, !force).await?;
let builder = add_auth_header_opt(builder, &auth_header);
let describe_reducer = util::describe_reducer(
&mut config,
database_identity,
server.map(|x| x.to_string()),
reducer_name.clone(),
anon_identity,
!force,
)
.await?;

Expand Down
6 changes: 4 additions & 2 deletions crates/cli/src/subcommands/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ pub fn cli() -> clap::Command {
.help("The name or identity of the database to delete"),
)
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::yes())
.after_help("Run `spacetime help delete` for more detailed information.\n")
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let database = args.get_one::<String>("database").unwrap();
let force = args.get_flag("force");

let identity = database_identity(&config, database, server).await?;

let builder = reqwest::Client::new().post(format!("{}/database/delete/{}", config.get_host_url(server)?, identity));
let auth_header = get_auth_header(&config, false)?;
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
let builder = add_auth_header_opt(builder, &auth_header);
builder.send().await?.error_for_status()?;

Expand Down
6 changes: 4 additions & 2 deletions crates/cli/src/subcommands/describe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ pub fn cli() -> clap::Command {
)
.arg(common_args::anonymous())
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::yes())
.after_help("Run `spacetime help describe` for more detailed information.\n")
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let database = args.get_one::<String>("database").unwrap();
let entity_name = args.get_one::<String>("entity_name");
let entity_type = args.get_one::<String>("entity_type");
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
bfops marked this conversation as resolved.
Show resolved Hide resolved

let anon_identity = args.get_flag("anon_identity");

Expand All @@ -46,7 +48,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
entity_name
),
});
let auth_header = get_auth_header(&config, anon_identity)?;
let auth_header = get_auth_header(&mut config, anon_identity, server, !force).await?;
let builder = add_auth_header_opt(builder, &auth_header);

let descr = builder.send().await?.error_for_status()?.text().await?;
Expand Down
15 changes: 10 additions & 5 deletions crates/cli/src/subcommands/dns.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::common_args;
use crate::config::Config;
use crate::util::{add_auth_header_opt, decode_identity, get_auth_header, spacetime_register_tld};
use crate::util::{
add_auth_header_opt, decode_identity, get_auth_header, get_login_token_or_log_in, spacetime_register_tld,
};
use clap::ArgMatches;
use clap::{Arg, Command};
use reqwest::Url;
Expand All @@ -22,17 +24,20 @@ pub fn cli() -> Command {
.help("The database identity to rename"),
)
.arg(common_args::server().help("The nickname, host name or URL of the server on which to set the name"))
.arg(common_args::yes())
.after_help("Run `spacetime rename --help` for more detailed information.\n")
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let domain = args.get_one::<String>("new-name").unwrap();
let database_identity = args.get_one::<String>("database-identity").unwrap();
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let identity = decode_identity(&config)?;
let auth_header = get_auth_header(&config, false)?;
let force = args.get_flag("force");
let token = get_login_token_or_log_in(&mut config, server, !force).await?;
let identity = decode_identity(&token)?;
let auth_header = get_auth_header(&mut config, false, server, !force).await?;

match spacetime_register_tld(&config, domain, server).await? {
match spacetime_register_tld(&mut config, domain, server, !force).await? {
RegisterTldResult::Success { domain } => {
println!("Registered domain: {}", domain);
}
Expand Down
11 changes: 7 additions & 4 deletions crates/cli/src/subcommands/energy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::common_args;
use clap::ArgMatches;

use crate::config::Config;
use crate::util;
use crate::util::{self, get_login_token_or_log_in};

pub fn cli() -> clap::Command {
clap::Command::new("energy")
Expand All @@ -26,7 +26,8 @@ fn get_energy_subcommands() -> Vec<clap::Command> {
.arg(
common_args::server()
.help("The nickname, host name or URL of the server from which to request balance information"),
)]
)
.arg(common_args::yes())]
}

async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result<(), anyhow::Error> {
Expand All @@ -41,15 +42,17 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
exec_subcommand(config, cmd, subcommand_args).await
}

async fn exec_status(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
async fn exec_status(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
// let project_name = args.value_of("project name").unwrap();
let identity = args.get_one::<String>("identity");
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
// TODO: We should remove the ability to call this for arbitrary users. At *least* remove it from the CLI.
let identity = if let Some(identity) = identity {
identity.clone()
} else {
util::decode_identity(&config)?
let token = get_login_token_or_log_in(&mut config, server, !force).await?;
util::decode_identity(&token)?
};

let status = reqwest::Client::new()
Expand Down
10 changes: 7 additions & 3 deletions crates/cli/src/subcommands/list.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::common_args;
use crate::util;
use crate::util::get_login_token_or_log_in;
use crate::Config;
use clap::{ArgMatches, Command};
use reqwest::StatusCode;
Expand All @@ -14,6 +15,7 @@ pub fn cli() -> Command {
Command::new("list")
.about("Lists the databases attached to an identity")
.arg(common_args::server().help("The nickname, host name or URL of the server from which to list databases"))
.arg(common_args::yes())
}

#[derive(Deserialize)]
Expand All @@ -27,9 +29,11 @@ struct IdentityRow {
pub db_identity: Identity,
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let identity = util::decode_identity(&config)?;
let force = args.get_flag("force");
let token = get_login_token_or_log_in(&mut config, server, !force).await?;
let identity = util::decode_identity(&token)?;

let client = reqwest::Client::new();
let res = client
Expand All @@ -38,7 +42,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
config.get_host_url(server)?,
identity
))
.basic_auth("token", Some(config.spacetimedb_token_or_error()?))
.basic_auth("token", Some(token))
.send()
.await?;

Expand Down
39 changes: 25 additions & 14 deletions crates/cli/src/subcommands/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use reqwest::Url;
use serde::Deserialize;
use webbrowser;

pub const DEFAULT_AUTH_HOST: &str = "https://spacetimedb.com";

pub fn cli() -> Command {
Command::new("login")
.args_conflicts_with_subcommands(true)
Expand All @@ -13,7 +15,7 @@ pub fn cli() -> Command {
.arg(
Arg::new("auth-host")
.long("auth-host")
.default_value("https://spacetimedb.com")
.default_value(DEFAULT_AUTH_HOST)
.group("login-method")
.help("Fetch login token from a different host"),
)
Expand Down Expand Up @@ -79,13 +81,17 @@ async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result
async fn exec_show(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let include_token = args.get_flag("token");

let identity = decode_identity(&config)?;
let token = if let Some(token) = config.spacetimedb_token() {
token
} else {
println!("You are not logged in. Run `spacetime login` to log in.");
return Ok(());
};
cloutiertyler marked this conversation as resolved.
Show resolved Hide resolved

let identity = decode_identity(&token)?;
println!("You are logged in as {}", identity);

if include_token {
// We can `unwrap` because `decode_identity` fetches this too.
// TODO: maybe decode_identity should take token as a param.
let token = config.spacetimedb_token().unwrap();
cloutiertyler marked this conversation as resolved.
Show resolved Hide resolved
println!("Your auth token (don't share this!) is {}", token);
}

Expand All @@ -100,18 +106,23 @@ async fn spacetimedb_token_cached(config: &mut Config, host: &Url, direct_login:
println!("If you want to log out, use spacetime logout.");
Ok(token.clone())
} else {
let token = if direct_login {
spacetimedb_direct_login(host).await?
} else {
let session_token = web_login_cached(config, host).await?;
spacetimedb_login(host, &session_token).await?
};
config.set_spacetimedb_token(token.clone());
config.save();
Ok(token)
spacetimedb_login_force(config, host, direct_login).await
cloutiertyler marked this conversation as resolved.
Show resolved Hide resolved
}
}

pub async fn spacetimedb_login_force(config: &mut Config, host: &Url, direct_login: bool) -> anyhow::Result<String> {
let token = if direct_login {
spacetimedb_direct_login(host).await?
} else {
let session_token = web_login_cached(config, host).await?;
spacetimedb_login(host, &session_token).await?
};
config.set_spacetimedb_token(token.clone());
config.save();

Ok(token)
}

async fn web_login_cached(config: &mut Config, host: &Url) -> anyhow::Result<String> {
if let Some(session_token) = config.web_session_token() {
// Currently, these session tokens do not expire. At some point in the future, we may also need to check this session token for validity.
Expand Down
6 changes: 4 additions & 2 deletions crates/cli/src/subcommands/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub fn cli() -> clap::Command {
.value_parser(clap::value_parser!(Format))
.help("Output format for the logs")
)
.arg(common_args::yes())
.after_help("Run `spacetime help logs` for more detailed information.\n")
}

Expand Down Expand Up @@ -109,14 +110,15 @@ impl clap::ValueEnum for Format {
}
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
let mut num_lines = args.get_one::<u32>("num_lines").copied();
let database = args.get_one::<String>("database").unwrap();
let follow = args.get_flag("follow");
let format = *args.get_one::<Format>("format").unwrap();

let auth_header = get_auth_header(&config, false)?;
let auth_header = get_auth_header(&mut config, false, server, !force).await?;

let database_identity = database_identity(&config, database, server).await?;

Expand Down
16 changes: 11 additions & 5 deletions crates/cli/src/subcommands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::fs;
use std::path::PathBuf;

use crate::config::Config;
use crate::util::{add_auth_header_opt, get_auth_header};
use crate::util::{add_auth_header_opt, get_auth_header, get_login_token_or_log_in};
use crate::util::{decode_identity, unauth_error_context, y_or_n};
use crate::{build, common_args};

Expand Down Expand Up @@ -65,7 +65,7 @@ pub fn cli() -> clap::Command {
.after_help("Run `spacetime help publish` for more detailed information.")
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let server = args.get_one::<String>("server").map(|s| s.as_str());
let name_or_identity = args.get_one::<String>("name|identity");
let path_to_project = args.get_one::<PathBuf>("project_path").unwrap();
Expand All @@ -80,7 +80,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
// we want to use the default identity
// TODO(jdetter): We should maybe have some sort of user prompt here for them to be able to
// easily create a new identity with an email
let auth_header = get_auth_header(&config, anon_identity)?;
let auth_header = get_auth_header(&mut config, anon_identity, server, !force).await?;

let mut query_params = Vec::<(&str, &str)>::new();
query_params.push(("host_type", "wasm"));
Expand Down Expand Up @@ -159,7 +159,8 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error

let res = builder.body(program_bytes).send().await?;
if res.status() == StatusCode::UNAUTHORIZED && !anon_identity {
let identity = decode_identity(&config)?;
let token = get_login_token_or_log_in(&mut config, server, !force).await?;
let identity = decode_identity(&token)?;
let err = res.text().await?;
return unauth_error_context(
Err(anyhow::anyhow!(err)),
Expand Down Expand Up @@ -198,7 +199,12 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
));
}
PublishResult::PermissionDenied { domain } => {
let identity = decode_identity(&config)?;
if anon_identity {
anyhow::bail!("You need to be logged in to publish to {}", domain.tld());
}
bfops marked this conversation as resolved.
Show resolved Hide resolved

let token = get_login_token_or_log_in(&mut config, server, !force).await?;
bfops marked this conversation as resolved.
Show resolved Hide resolved
let identity = decode_identity(&token)?;
//TODO(jdetter): Have a nice name generator here, instead of using some abstract characters
// we should perhaps generate fun names like 'green-fire-dragon' instead
let suggested_tld: String = identity.chars().take(12).collect();
Expand Down
6 changes: 4 additions & 2 deletions crates/cli/src/subcommands/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ pub fn cli() -> clap::Command {
)
.arg(common_args::anonymous())
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::yes())
}

pub(crate) async fn parse_req(config: Config, args: &ArgMatches) -> Result<Connection, anyhow::Error> {
pub(crate) async fn parse_req(mut config: Config, args: &ArgMatches) -> Result<Connection, anyhow::Error> {
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
let database_name_or_identity = args.get_one::<String>("database").unwrap();
let anon_identity = args.get_flag("anon_identity");

Ok(Connection {
host: config.get_host_url(server)?,
auth_header: get_auth_header(&config, anon_identity)?,
auth_header: get_auth_header(&mut config, anon_identity, server, !force).await?,
database_identity: database_identity(&config, database_name_or_identity, server).await?,
database: database_name_or_identity.to_string(),
})
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/subcommands/subscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub fn cli() -> clap::Command {
.help("Print the initial update for the queries."),
)
.arg(common_args::anonymous())
.arg(common_args::yes())
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
}

Expand Down
Loading
Loading