Skip to content
Merged
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
29 changes: 27 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async-trait = "0.1"
bytes = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
clap = { version = "4.5", features = ["std", "string", "derive"] }
cli-engine = { version = "0.10.0", features = ["pkce-auth"] }
cli-engine = { version = "0.10.1", features = ["pkce-auth"] }
Comment thread
jpage-godaddy marked this conversation as resolved.
dirs = "6"
domains-client = { path = "domains-client" }
email-client = { path = "email-client" }
Expand Down
2 changes: 1 addition & 1 deletion rust/src/api/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ mod tests {
/// Same ambiguity, but through human output — covers that rendering
/// path directly, since every other test here only exercises
/// `--output json`. Human error rendering is `Error: {message}` /
/// `Fix: {fix}` (`cli_engine::output::human::render_human_with_view`).
/// `Fix: {fix}`.
#[tokio::test]
async fn operation_get_multiple_fuzzy_matches_human_output_shows_message_and_candidates() {
let output = operation_cli()
Expand Down
8 changes: 4 additions & 4 deletions rust/src/api/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_pagination(PaginationConfig {
default_limit: OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
max_limit: OPERATION_CHILD_LIST_MAX_LIMIT,
}),
.with_pagination(PaginationConfig::new(
OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
OPERATION_CHILD_LIST_MAX_LIMIT,
)),
|_cred, args: ParameterListArgs| async move {
let operation = args.operation.as_str();
let catalog = catalog();
Expand Down
8 changes: 4 additions & 4 deletions rust/src/api/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ pub(super) fn list_command() -> RuntimeCommandSpec {
.with_system("api")
.with_tier(Tier::Read)
.no_auth(true)
.with_pagination(PaginationConfig {
default_limit: OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
max_limit: OPERATION_CHILD_LIST_MAX_LIMIT,
}),
.with_pagination(PaginationConfig::new(
OPERATION_CHILD_LIST_DEFAULT_LIMIT as i64,
OPERATION_CHILD_LIST_MAX_LIMIT,
)),
|_cred, args: ResponseListArgs| async move {
let operation = args.operation.as_str();
let catalog = catalog();
Expand Down
10 changes: 2 additions & 8 deletions rust/src/api/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.no_auth(true)
.with_default_fields("operationId,domain,method,path,summary")
.with_output_schema::<ApiEndpoint>()
.with_pagination(PaginationConfig {
max_limit: 100,
..Default::default()
}),
.with_pagination(PaginationConfig::new(0, 100)),
|_cred, args: SearchArgs| async move {
let hits = search_endpoints(catalog(), &args.query);
if hits.is_empty() {
Expand Down Expand Up @@ -130,10 +127,7 @@ mod tests {
fn command_opts_into_pagination_with_no_default_and_a_max_limit() {
assert_eq!(
command().spec.pagination,
Some(PaginationConfig {
max_limit: 100,
..Default::default()
})
Some(PaginationConfig::new(0, 100))
);
}
}
18 changes: 11 additions & 7 deletions rust/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn build_provider(env: &GddyEnvConfig) -> PkceAuthProvider {
environments::DEFAULT_OAUTH_SCOPES,
)
.with_app_id(environments::APP_ID)
.with_redirect_uri(environments::REDIRECT_URI)
.with_ephemeral_redirect_port()
.with_environments(Arc::clone(environments::instance()))
}

Expand Down Expand Up @@ -124,14 +124,13 @@ fn log_resolved_oauth(env: &GddyEnvConfig) {
auth_url_from_env_var = auth_url_ovr.is_some(),
token_url = %token_url_ovr.as_deref().unwrap_or(&env.token_url),
token_url_from_env_var = token_url_ovr.is_some(),
redirect_uri = environments::REDIRECT_URI,
redirect_uri = "http://127.0.0.1:<ephemeral>/callback",
"resolved OAuth client for login/token exchange"
);
}

/// Rejects any scope in `requested` that isn't registered in [`scopes`] —
/// [`scopes::ALL`] plus the standalone [`scopes::OFFLINE_ACCESS`] directive
/// scope.
/// [`scopes::ALL`] plus the standalone [`scopes::DIRECTIVES`] scopes.
///
/// A scope missing from that list is, by construction, either misspelled or
/// was never registered on the CLI's OAuth client (see the "Adding a scope"
Expand All @@ -143,7 +142,7 @@ fn validate_requested_scopes(requested: &[String]) -> Result<()> {
let unknown: Vec<&str> = requested
.iter()
.map(String::as_str)
.filter(|scope| *scope != scopes::OFFLINE_ACCESS && !scopes::ALL.contains(scope))
.filter(|scope| !scopes::DIRECTIVES.contains(scope) && !scopes::ALL.contains(scope))
.filter(|scope| seen.insert(*scope))
.collect();
if unknown.is_empty() {
Expand Down Expand Up @@ -263,8 +262,13 @@ mod tests {
}

#[test]
fn validate_requested_scopes_accepts_offline_access() {
assert!(validate_requested_scopes(&[scopes::OFFLINE_ACCESS.to_owned()]).is_ok());
fn validate_requested_scopes_accepts_directive_scopes() {
for scope in scopes::DIRECTIVES {
assert!(
validate_requested_scopes(&[(*scope).to_owned()]).is_ok(),
"directive scope {scope:?} was rejected"
);
}
}

#[test]
Expand Down
10 changes: 2 additions & 8 deletions rust/src/dns/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.with_json_schema::<types::DnsRecord>()
.with_view(view_columns())
.with_scopes(&[DOMAINS_READ])
.with_pagination(PaginationConfig {
max_limit: 500,
..Default::default()
}),
.with_pagination(PaginationConfig::new(0, 500)),
|ctx, args: ListArgs| async move {
let domain = args.domain;
let type_opt = args.record_type;
Expand Down Expand Up @@ -126,10 +123,7 @@ mod tests {
fn opts_into_pagination_with_no_default_and_a_max_limit() {
assert_eq!(
command().spec.pagination,
Some(PaginationConfig {
max_limit: 500,
..Default::default()
})
Some(PaginationConfig::new(0, 500))
);
}

Expand Down
10 changes: 2 additions & 8 deletions rust/src/domain/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.with_default_fields("domain,status,expiresAt,autoRenew")
.with_json_schema::<types::Domain>()
.with_scopes(&[DOMAINS_READ])
.with_pagination(PaginationConfig {
max_limit: 500,
..Default::default()
}),
.with_pagination(PaginationConfig::new(0, 500)),
|ctx, args: ListArgs| async move {
let debug = !ctx.middleware.debug.is_empty();
let statuses = parse_statuses(&args.status)?;
Expand Down Expand Up @@ -303,10 +300,7 @@ mod tests {
fn opts_into_pagination_with_no_default_and_a_max_limit() {
assert_eq!(
command().spec.pagination,
Some(PaginationConfig {
max_limit: 500,
..Default::default()
})
Some(PaginationConfig::new(0, 500))
);
}

Expand Down
10 changes: 2 additions & 8 deletions rust/src/email/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.with_system("email")
.with_tier(Tier::Read)
.with_scopes(&[EMAIL_READ])
.with_pagination(PaginationConfig {
default_limit: 25,
max_limit: 500,
}),
.with_pagination(PaginationConfig::new(25, 500)),
|ctx, args: ListArgs| async move {
let client = make_client(&ctx, &[EMAIL_READ]).await?;
let mailboxes = fetch_mailboxes(
Expand Down Expand Up @@ -124,10 +121,7 @@ mod tests {
fn opts_into_pagination_with_a_default_and_a_max_limit() {
assert_eq!(
command().spec.pagination,
Some(PaginationConfig {
default_limit: 25,
max_limit: 500,
})
Some(PaginationConfig::new(25, 500))
);
}

Expand Down
3 changes: 2 additions & 1 deletion rust/src/environments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ pub const DEFAULT_OAUTH_SCOPES: &[&str] = &[
crate::scopes::APP_REGISTRY_READ,
crate::scopes::DOMAINS_READ,
crate::scopes::OFFLINE_ACCESS,
crate::scopes::OPENID,
crate::scopes::PROFILE,
];
pub const REDIRECT_URI: &str = "http://localhost:7443/callback";
pub const APP_ID: &str = "gddy";

pub fn env_prefix(name: &str) -> String {
Expand Down
5 changes: 1 addition & 4 deletions rust/src/platform/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ pub fn group() -> RuntimeGroupSpec {
.no_auth(true)
.with_default_fields("name,description")
.with_output_schema::<ActionSummary>()
.with_pagination(PaginationConfig {
default_limit: 50,
max_limit: 200,
}),
.with_pagination(PaginationConfig::new(50, 200)),
|_cred, _args| async move {
let actions: Vec<_> = ACTIONS
.iter()
Expand Down
10 changes: 2 additions & 8 deletions rust/src/platform/app/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.with_tier(Tier::Read)
.with_default_fields("name,label,status")
.with_output_schema::<ApplicationSummary>()
.with_pagination(PaginationConfig {
max_limit: 200,
..Default::default()
}),
.with_pagination(PaginationConfig::new(0, 200)),
|ctx| async move {
let client = super::make_client(&ctx).await?;
let data = client
Expand Down Expand Up @@ -56,10 +53,7 @@ mod tests {
fn command_opts_into_pagination_with_no_default_and_a_max_limit() {
assert_eq!(
command().spec.pagination,
Some(PaginationConfig {
max_limit: 200,
..Default::default()
})
Some(PaginationConfig::new(0, 200))
);
}

Expand Down
47 changes: 36 additions & 11 deletions rust/src/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
//!
//! `ALL` does NOT cover every scope requiring client registration, though:
//! directive scopes like [`OFFLINE_ACCESS`] aren't `resource:action` grants, so
//! they're declared outside [`declare_scopes!`]/`ALL` but still need the same
//! server-side registration. When syncing the OAuth client's configuration,
//! diff against `ALL` *plus* every such standalone constant, not `ALL` alone.
//! they're declared outside [`declare_scopes!`]/`ALL` (and listed in
//! [`DIRECTIVES`]) but still need the same server-side registration. When
//! syncing the OAuth client's configuration, diff against `ALL` *plus*
//! [`DIRECTIVES`], not `ALL` alone.
//!
//! # Adding a scope (READ THIS)
//!
//! 1. Add a constant to the [`declare_scopes!`] block below (or, for a directive
//! scope that isn't a `resource:action` permission, declare it standalone
//! like [`OFFLINE_ACCESS`]). Constants in `declare_scopes!` are automatically
//! like [`OFFLINE_ACCESS`], and add it to [`DIRECTIVES`]). Constants in `declare_scopes!` are automatically
//! included in [`ALL`] — you cannot add one there without registering it in
//! the list.
//! 2. Reference the new constant from the command via `.with_scopes(&[scopes::…])`.
Expand All @@ -52,10 +53,10 @@ macro_rules! declare_scopes {
/// registration in sync with this list.
///
/// NOT the complete set of scopes requiring OAuth client registration:
/// [`OFFLINE_ACCESS`] is a directive scope (not a `resource:action` permission)
/// and is deliberately excluded, but still must be registered on the client
/// server-side. Diff the client's configuration against `ALL` *plus*
/// [`OFFLINE_ACCESS`], not `ALL` alone.
/// the directive scopes in [`DIRECTIVES`] (e.g. [`OFFLINE_ACCESS`]) aren't
/// `resource:action` permissions and are deliberately excluded, but still must
/// be registered on the client server-side. Diff the client's configuration
/// against `ALL` *plus* [`DIRECTIVES`], not `ALL` alone.
///
/// Not referenced by production code (the individual constants are what
/// commands use); it exists as the authoritative registry to diff against
Expand All @@ -77,6 +78,21 @@ macro_rules! declare_scopes {
/// unregistered scope.
pub const OFFLINE_ACCESS: &str = "offline_access";

/// OIDC directive scope marking the request as an identity (not just
/// authorization) request — some IDPs only populate profile claims when this
/// is present alongside [`PROFILE`], even though `profile` alone is meant to
/// carry the claims.
pub const OPENID: &str = "openid";

/// OIDC directive scope requesting standard profile claims (`name`,
/// `given_name`, `preferred_username`, etc.) on the token.
pub const PROFILE: &str = "profile";
Comment thread
jpage-godaddy marked this conversation as resolved.

/// Every standalone directive scope — the scopes that require OAuth client
/// registration but, not being `resource:action` permissions, live outside
/// [`ALL`]. Diff the client's configuration against `ALL` *plus* this list.
pub const DIRECTIVES: &[&str] = &[OFFLINE_ACCESS, OPENID, PROFILE];

// DON'T FORGET! If you add a scope here, you must also register it on the CLI's OAuth client.
declare_scopes! {
/// Read the caller's registered applications. Requested at login by default
Expand Down Expand Up @@ -308,6 +324,16 @@ pub const SCOPE_REGISTRY: &[ScopeInfo] = &[
description: "Request a refresh token",
default: true,
},
ScopeInfo {
scope: OPENID,
description: "Mark the request as an OIDC identity request",
default: true,
},
ScopeInfo {
scope: PROFILE,
description: "Request standard OIDC profile claims on the token",
default: true,
},
];

/// Every leaf command's space-separated invocation path (e.g. `"domain
Expand Down Expand Up @@ -439,14 +465,13 @@ mod tests {
}

/// [`SCOPE_REGISTRY`] must describe every scope requiring OAuth client
/// registration — `ALL` plus the standalone directive scopes
/// ([`OFFLINE_ACCESS`]) — or `gddy auth scope` would silently omit a
/// registration — `ALL` plus the standalone [`DIRECTIVES`] — or `gddy auth scope` would silently omit a
/// scope an agent needs to plan an eager login around.
#[test]
fn scope_registry_covers_every_declared_scope() {
let registered: std::collections::HashSet<&str> =
SCOPE_REGISTRY.iter().map(|info| info.scope).collect();
for scope in ALL.iter().copied().chain([OFFLINE_ACCESS]) {
for scope in ALL.iter().chain(DIRECTIVES).copied() {
assert!(
registered.contains(scope),
"scope {scope:?} is declared but missing from SCOPE_REGISTRY"
Expand Down
Loading