Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ Learn how to [create API keys](https://clickhouse.com/docs/cloud/manage/openapi?

```bash
clickhousectl cloud auth status # Show current auth state (including read-only/read-write labels)
clickhousectl cloud auth whoami # (Beta) Ask the Cloud API who the active credentials belong to
clickhousectl cloud auth logout # Clear all saved credentials (credentials.json & tokens.json)
clickhousectl cloud auth logout --oauth # Clear only OAuth tokens, keep API keys
clickhousectl cloud auth logout --api-keys # Clear only API keys, keep OAuth tokens
Expand All @@ -693,6 +694,8 @@ Credential resolution order:

Supplying only `--api-key` or only `--api-secret` blocks fallback to other sources.
`cloud auth status` still succeeds with no active authentication; `--debug` identifies the missing flag.
`cloud auth whoami` works with OAuth or API keys and needs no organization: a user shows its ID, email,
name and organizations; an API key shows its key ID, name and owning organization ID.

When environment credentials are configured but a credentials file or explicit
CLI flags take precedence, clickhousectl prints a one-line note to stderr.
Expand Down Expand Up @@ -857,6 +860,14 @@ clickhousectl cloud service create --name restored-service \
--ip-allow <trusted-public-ip>/32 \
--backup-id <backup-uuid>

# Restore a TDE service's backup from your own bucket (private preview): pass the
# backup's encryption_config.json unchanged, or - to read it from stdin
clickhousectl cloud service create --name restored-service \
--provider aws \
--region us-east-1 \
--ip-allow <trusted-public-ip>/32 \
--backup-id <backup-uuid> --backup-encryption-config ./encryption_config.json

# Create with release channel
clickhousectl cloud service create --name my-service \
--provider aws \
Expand Down Expand Up @@ -1361,7 +1372,11 @@ clickhousectl cloud postgres certs get <pg-id> --output ca.pem
PGSSLMODE=verify-full PGSSLROOTCERT=ca.pem psql --host <host-from-get> --port 5432 \
--username <username-from-get> --dbname postgres

# Read replica and PITR restore
# Retained base backups, newest first; follow `Next cursor:` (JSON: nextCursor) with --cursor
clickhousectl cloud postgres backup list <pg-id> --limit 20
clickhousectl cloud postgres backup list <pg-id> --cursor <next-cursor>

# Read replica and PITR restore (restore takes a point in time, not a backup key)
clickhousectl cloud postgres read-replica create --source-name primary --name replica-1
clickhousectl cloud postgres read-replica create <pg-id> --name replica-2 \
--tag env=prod --pg-config-file ./pg.json
Expand Down Expand Up @@ -2113,6 +2128,17 @@ clickhousectl cloud clickpipe create kafka <service-id> \
--database default --table events \
--column "event_id:Int64"

# Avro via the AWS Glue Schema Registry on MSK; --glue-role-arn defaults to
# --iam-role and is required when the source does not use --iam-role
clickhousectl cloud clickpipe create kafka <service-id> \
--name my-glue-pipe \
--brokers 'broker:9098' --topics events --kafka-type msk \
--format Avro --iam-role arn:aws:iam::123456789012:role/ClickPipes \
--schema-registry-type glue \
--glue-region us-east-1 --glue-registry-name my-registry \
--database default --table events \
--column "event_id:Int64"

# Protobuf schema from a file, with exactly-once delivery
clickhousectl cloud clickpipe create kafka <service-id> \
--name my-protobuf-pipe \
Expand Down
115 changes: 114 additions & 1 deletion crates/clickhousectl/src/cloud/auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::permissions::Declaration as Permission;
use clickhouse_cloud_api::meta::operations as op;

// Declare every API call made by these workflows, including optional lookups.
pub(super) const PERMISSIONS: &[Permission] = &[
Expand All @@ -15,15 +16,18 @@ pub(super) const PERMISSIONS: &[Permission] = &[
"Reads local authentication state; no Cloud API call.",
),
Permission::non_api("auth signup", "Opens account signup; no Cloud API call."),
Permission::api("auth whoami", &[&op::WHOAMI_GET]).unscoped(),
];

use crate::cloud::client::{CloudClient, Result as CloudResult};
use crate::cloud::credentials;
use crate::cloud::output::eprint_line;
use crate::cloud::output::{eprint_line, print_human};
use crate::cloud::{
AuthSource, dotenv_env_provenance, env_cred_presence, resolve_active_auth_source,
};
use crate::error::Error;
use clap::Subcommand;
use clickhouse_cloud_api::models::Whoami;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

Expand Down Expand Up @@ -68,6 +72,12 @@ CONTEXT FOR AGENTS:
},
/// Show current authentication status
Status,
/// Show the identity behind the active credentials (Beta)
#[command(after_help = "\
CONTEXT FOR AGENTS:
Works with OAuth or API key credentials and needs no --org-id.
`cloud auth status` shows which credential source is active.")]
Whoami,
/// Create a ClickHouse Cloud account
#[command(after_help = "\
CONTEXT FOR AGENTS:
Expand Down Expand Up @@ -108,10 +118,60 @@ impl AuthCommands {
AuthCommands::Logout { .. } => false,
AuthCommands::Status => false,
AuthCommands::Signup => false,
AuthCommands::Whoami => false,
}
}

/// Whether this command calls the Cloud API, and so needs a `CloudClient`
/// built from the resolved credentials. The others manage local state.
pub fn needs_client(&self) -> bool {
match self {
AuthCommands::Login { .. }
| AuthCommands::Logout { .. }
| AuthCommands::Status
| AuthCommands::Signup => false,
AuthCommands::Whoami => true,
}
}
}

/// Run an auth command that calls the Cloud API (see [`AuthCommands::needs_client`]).
pub async fn run_with_client(
client: &CloudClient,
command: AuthCommands,
json: bool,
) -> CloudResult<()> {
match command {
AuthCommands::Whoami => {
let identity = client.get_whoami().await?;
if json {
println!("{}", serde_json::to_string_pretty(&identity)?);
} else {
print_human(&identity)?;
}
Ok(())
}
AuthCommands::Login { .. }
| AuthCommands::Logout { .. }
| AuthCommands::Status
| AuthCommands::Signup => {
unreachable!("local auth commands are handled before a client is built")
}
}
}

impl CloudClient {
/// Resolve the caller behind the active credentials; not organization-scoped.
async fn get_whoami(&self) -> CloudResult<Whoami> {
let response = self
.api()
.whoami_get()
.await
.map_err(|error| self.convert_error(error))?;
Self::unwrap_response(response)
}
}

pub async fn run(
command: AuthCommands,
api_key: Option<&str>,
Expand Down Expand Up @@ -158,6 +218,7 @@ pub async fn run(
Ok(())
}
}
AuthCommands::Whoami => unreachable!("whoami runs through run_with_client"),
AuthCommands::Signup => {
let api_url = api_url.unwrap_or("https://api.clickhouse.cloud");
let parsed = url::Url::parse(api_url)
Expand Down Expand Up @@ -838,8 +899,60 @@ mod tests {
AuthCli::try_parse_from(["clickhousectl", "signup"])
.unwrap()
.command,
AuthCli::try_parse_from(["clickhousectl", "whoami"])
.unwrap()
.command,
];
assert!(commands.iter().all(|command| !command.is_write()));
// Only whoami calls the Cloud API; the rest manage local state.
let remote: Vec<_> = commands
.iter()
.map(|command| matches!(command, AuthCommands::Whoami))
.collect();
assert_eq!(
commands
.iter()
.map(AuthCommands::needs_client)
.collect::<Vec<_>>(),
remote
);
assert_eq!(remote.iter().filter(|remote| **remote).count(), 1);
}

#[test]
fn auth_whoami_takes_shared_credentials_and_rejects_arguments() {
let cli = Cli::try_parse_from([
"clickhousectl",
"cloud",
"auth",
"whoami",
"--api-key",
"key",
"--api-secret",
"secret",
"--json",
])
.unwrap();
let Commands::Cloud(args) = cli.command else {
panic!("expected cloud command");
};
assert_eq!(args.api_key.as_deref(), Some("key"));
assert_eq!(args.api_secret.as_deref(), Some("secret"));
assert!(args.json);
assert!(!args.command.is_write_command());
assert!(matches!(
args.command,
crate::cloud::cli::CloudCommands::Auth {
command: AuthCommands::Whoami
}
));
assert_eq!(
Cli::try_parse_from(["clickhousectl", "cloud", "auth", "whoami", "extra"])
.err()
.unwrap()
.kind(),
clap::error::ErrorKind::UnknownArgument
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/clickhousectl/src/cloud/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ pub(crate) mod tests {
&["activity", "get", "activity-1"],
&["backup", "get", "svc-1", "backup-1"],
&["postgres", "config", "get", "pg-1"],
&["postgres", "backup", "list", "pg-1"],
&["clickstack", "dashboard", "get", "svc-1", "dashboard-1"],
&[
"clickpipe",
Expand Down
Loading
Loading