Skip to content

Commit abc70cb

Browse files
committed
feat(sandbox): add delegated identity for token exchange
Signed-off-by: Gordon Sim <gsim@redhat.com>
1 parent 6546691 commit abc70cb

71 files changed

Lines changed: 16580 additions & 4804 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/openshell-cli/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ serde_json = { workspace = true }
102102
temp-env = "0.3"
103103
tokio-stream = { workspace = true }
104104
url = { workspace = true }
105+
wiremock = "0.6"

‎crates/openshell-cli/src/main.rs‎

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,13 @@ enum Commands {
594594
command: Option<ProfileCommands>,
595595
},
596596

597+
/// Manage delegated identity credential records.
598+
#[command(help_template = SUBCOMMAND_HELP_TEMPLATE)]
599+
DelegatedCredential {
600+
#[command(subcommand)]
601+
command: Option<DelegatedCredentialCommands>,
602+
},
603+
597604
/// Manage workspaces.
598605
#[command(alias = "ws", after_help = WORKSPACE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)]
599606
Workspace {
@@ -1288,6 +1295,62 @@ impl ProfileCommands {
12881295
}
12891296
}
12901297

1298+
#[derive(Subcommand, Debug)]
1299+
enum DelegatedCredentialCommands {
1300+
/// List delegated identity credentials.
1301+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1302+
List {
1303+
/// Maximum number of credentials to return in this page.
1304+
#[arg(long, default_value_t = 100)]
1305+
page_size: i32,
1306+
1307+
/// Opaque continuation token from a previous page.
1308+
#[arg(long, default_value = "")]
1309+
page_token: String,
1310+
1311+
/// Output only credential IDs.
1312+
#[arg(long, conflicts_with = "output")]
1313+
ids: bool,
1314+
1315+
/// Output format.
1316+
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with = "ids")]
1317+
output: OutputFormat,
1318+
},
1319+
1320+
/// Show delegated identity credential status.
1321+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1322+
Status {
1323+
/// Delegated identity credential ID.
1324+
id: String,
1325+
1326+
/// Output format.
1327+
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
1328+
output: OutputFormat,
1329+
},
1330+
1331+
/// Revoke a delegated identity credential.
1332+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1333+
Revoke {
1334+
/// Delegated identity credential ID.
1335+
id: String,
1336+
1337+
/// Expected resource version for compare-and-swap updates.
1338+
#[arg(long = "resource-version", default_value_t = 0)]
1339+
resource_version: u64,
1340+
},
1341+
1342+
/// Delete a delegated identity credential record.
1343+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1344+
Delete {
1345+
/// Delegated identity credential ID.
1346+
id: String,
1347+
1348+
/// Expected resource version for compare-and-swap deletes.
1349+
#[arg(long = "resource-version", default_value_t = 0)]
1350+
resource_version: u64,
1351+
},
1352+
}
1353+
12911354
// -----------------------------------------------------------------------
12921355
// Gateway commands (replaces the old `cluster` / `cluster admin` groups)
12931356
// -----------------------------------------------------------------------
@@ -1528,6 +1591,12 @@ enum SandboxCommands {
15281591
#[arg(long = "provider")]
15291592
providers: Vec<String>,
15301593

1594+
/// Allow the sandbox to use a separately authorized copy of your identity for a bounded duration.
1595+
/// Opens a browser authorization flow when no usable gateway-owned grant exists. Accepts
1596+
/// positive durations with m, h, or d suffixes, for example 30m, 8h, or 7d.
1597+
#[arg(long = "use-my-identity-for", value_name = "DURATION")]
1598+
delegate_identity_for: Option<String>,
1599+
15311600
/// Path to a custom sandbox policy YAML file.
15321601
/// Overrides the built-in default and the `OPENSHELL_SANDBOX_POLICY` env var.
15331602
#[arg(long, value_hint = ValueHint::FilePath)]
@@ -1845,6 +1914,41 @@ enum SandboxCommands {
18451914
/// Manage reusable sandbox workload templates.
18461915
#[command(subcommand)]
18471916
Template(SandboxTemplateCommands),
1917+
1918+
/// Manage sandbox delegated identity.
1919+
#[command(subcommand, name = "delegated-identity")]
1920+
DelegatedIdentity(SandboxDelegatedIdentityCommands),
1921+
}
1922+
1923+
#[derive(Subcommand, Debug)]
1924+
enum SandboxDelegatedIdentityCommands {
1925+
/// Show delegated identity status for a sandbox.
1926+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1927+
Status {
1928+
/// Sandbox name.
1929+
#[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))]
1930+
name: String,
1931+
},
1932+
1933+
/// Withdraw delegated identity from a sandbox.
1934+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1935+
Withdraw {
1936+
/// Sandbox name.
1937+
#[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))]
1938+
name: String,
1939+
},
1940+
1941+
/// Set delegated identity expiry to now plus the requested duration.
1942+
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
1943+
Extend {
1944+
/// Sandbox name.
1945+
#[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))]
1946+
name: String,
1947+
1948+
/// New authorization window, for example 24h.
1949+
#[arg(long = "for", value_name = "DURATION")]
1950+
duration: String,
1951+
},
18481952
}
18491953

18501954
/// Common observation flags; the deadline starts after the mutation is saved.
@@ -3367,6 +3471,7 @@ async fn run_async() -> Result<()> {
33673471
memory,
33683472
driver_config_json,
33693473
providers,
3474+
delegate_identity_for,
33703475
policy,
33713476
forward,
33723477
expose,
@@ -3463,6 +3568,7 @@ async fn run_async() -> Result<()> {
34633568
driver_config_json: driver_config_json.as_deref(),
34643569
editor,
34653570
providers: &providers,
3571+
delegate_identity_for: delegate_identity_for.as_deref(),
34663572
policy: policy.as_deref(),
34673573
forward,
34683574
expose,
@@ -3813,6 +3919,37 @@ async fn run_async() -> Result<()> {
38133919
.await?;
38143920
}
38153921
},
3922+
SandboxCommands::DelegatedIdentity(command) => match command {
3923+
SandboxDelegatedIdentityCommands::Status { name } => {
3924+
run::sandbox_delegated_identity_status(
3925+
endpoint,
3926+
&name,
3927+
&cli.workspace,
3928+
&tls,
3929+
)
3930+
.await?;
3931+
}
3932+
SandboxDelegatedIdentityCommands::Withdraw { name } => {
3933+
run::sandbox_delegated_identity_withdraw(
3934+
endpoint,
3935+
&name,
3936+
&cli.workspace,
3937+
&tls,
3938+
)
3939+
.await?;
3940+
}
3941+
SandboxDelegatedIdentityCommands::Extend { name, duration } => {
3942+
run::sandbox_delegated_identity_extend(
3943+
endpoint,
3944+
&ctx.name,
3945+
&name,
3946+
&duration,
3947+
&cli.workspace,
3948+
&tls,
3949+
)
3950+
.await?;
3951+
}
3952+
},
38163953
}
38173954
}
38183955
}
@@ -4075,6 +4212,61 @@ async fn run_async() -> Result<()> {
40754212
apply_auth(&mut tls, &ctx.name)?;
40764213
command.run(endpoint, &cli.workspace, &tls).await?;
40774214
}
4215+
Some(Commands::DelegatedCredential {
4216+
command: Some(command),
4217+
}) => {
4218+
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
4219+
let endpoint = &ctx.endpoint;
4220+
let mut tls = tls.with_gateway_name(&ctx.name);
4221+
apply_auth(&mut tls, &ctx.name)?;
4222+
4223+
match command {
4224+
DelegatedCredentialCommands::List {
4225+
page_size,
4226+
page_token,
4227+
ids,
4228+
output,
4229+
} => {
4230+
run::delegated_identity_credential_list(
4231+
endpoint,
4232+
page_size,
4233+
&page_token,
4234+
ids,
4235+
output.as_str(),
4236+
&tls,
4237+
)
4238+
.await?;
4239+
}
4240+
DelegatedCredentialCommands::Status { id, output } => {
4241+
run::delegated_identity_credential_status(endpoint, &id, output.as_str(), &tls)
4242+
.await?;
4243+
}
4244+
DelegatedCredentialCommands::Revoke {
4245+
id,
4246+
resource_version,
4247+
} => {
4248+
run::delegated_identity_credential_revoke(
4249+
endpoint,
4250+
&id,
4251+
resource_version,
4252+
&tls,
4253+
)
4254+
.await?;
4255+
}
4256+
DelegatedCredentialCommands::Delete {
4257+
id,
4258+
resource_version,
4259+
} => {
4260+
run::delegated_identity_credential_delete(
4261+
endpoint,
4262+
&id,
4263+
resource_version,
4264+
&tls,
4265+
)
4266+
.await?;
4267+
}
4268+
}
4269+
}
40784270
Some(Commands::Term { theme }) => {
40794271
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
40804272
let mut tls = tls.with_gateway_name(&ctx.name);
@@ -4214,6 +4406,13 @@ async fn run_async() -> Result<()> {
42144406
.print_help()
42154407
.expect("Failed to print help");
42164408
}
4409+
Some(Commands::DelegatedCredential { command: None }) => {
4410+
Cli::command()
4411+
.find_subcommand_mut("delegated-credential")
4412+
.expect("delegated-credential subcommand exists")
4413+
.print_help()
4414+
.expect("Failed to print help");
4415+
}
42174416
Some(Commands::Gateway { command: None }) => {
42184417
Cli::command()
42194418
.find_subcommand_mut("gateway")
@@ -5170,6 +5369,76 @@ mod tests {
51705369
}) if ids == ["custom-api", "custom-alt"]));
51715370
}
51725371

5372+
#[test]
5373+
fn delegated_credential_commands_parse() {
5374+
let list = Cli::try_parse_from([
5375+
"openshell",
5376+
"delegated-credential",
5377+
"list",
5378+
"--page-size",
5379+
"25",
5380+
"--page-token",
5381+
"next-credential-page",
5382+
"--ids",
5383+
])
5384+
.expect("delegated credential list should parse");
5385+
match list.command {
5386+
Some(Commands::DelegatedCredential {
5387+
command:
5388+
Some(DelegatedCredentialCommands::List {
5389+
page_size,
5390+
page_token,
5391+
ids,
5392+
output,
5393+
}),
5394+
}) => {
5395+
assert_eq!(page_size, 25);
5396+
assert_eq!(page_token, "next-credential-page");
5397+
assert!(ids);
5398+
assert_eq!(output, OutputFormat::Table);
5399+
}
5400+
command => panic!("unexpected delegated credential list command: {command:?}"),
5401+
}
5402+
5403+
let status = Cli::try_parse_from([
5404+
"openshell",
5405+
"delegated-credential",
5406+
"status",
5407+
"delegated-identity-123",
5408+
"-o",
5409+
"json",
5410+
])
5411+
.expect("delegated credential status should parse");
5412+
assert!(matches!(
5413+
status.command,
5414+
Some(Commands::DelegatedCredential {
5415+
command: Some(DelegatedCredentialCommands::Status {
5416+
id,
5417+
output: OutputFormat::Json,
5418+
})
5419+
}) if id == "delegated-identity-123"
5420+
));
5421+
5422+
let revoke = Cli::try_parse_from([
5423+
"openshell",
5424+
"delegated-credential",
5425+
"revoke",
5426+
"delegated-identity-123",
5427+
"--resource-version",
5428+
"7",
5429+
])
5430+
.expect("delegated credential revoke should parse");
5431+
assert!(matches!(
5432+
revoke.command,
5433+
Some(Commands::DelegatedCredential {
5434+
command: Some(DelegatedCredentialCommands::Revoke {
5435+
id,
5436+
resource_version: 7,
5437+
})
5438+
}) if id == "delegated-identity-123"
5439+
));
5440+
}
5441+
51735442
#[test]
51745443
fn profile_import_and_lint_require_exactly_one_source() {
51755444
for verb in ["import", "lint"] {
@@ -6363,6 +6632,31 @@ mod tests {
63636632
}
63646633
}
63656634

6635+
#[test]
6636+
fn sandbox_create_use_my_identity_for_parses() {
6637+
let cli = Cli::try_parse_from([
6638+
"openshell",
6639+
"sandbox",
6640+
"create",
6641+
"--use-my-identity-for",
6642+
"8h",
6643+
])
6644+
.expect("sandbox create --use-my-identity-for should parse");
6645+
match cli.command {
6646+
Some(Commands::Sandbox {
6647+
command:
6648+
Some(SandboxCommands::Create {
6649+
delegate_identity_for,
6650+
..
6651+
}),
6652+
..
6653+
}) => {
6654+
assert_eq!(delegate_identity_for.as_deref(), Some("8h"));
6655+
}
6656+
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
6657+
}
6658+
}
6659+
63666660
/// `--approval-mode <bogus>` is rejected by clap's value parser, so the
63676661
/// CLI can't smuggle through a future-mode value that the gateway
63686662
/// doesn't yet know about.

‎crates/openshell-cli/src/oidc_auth.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ pub async fn oidc_browser_auth_flow(
239239
() = tokio::time::sleep(AUTH_TIMEOUT) => {
240240
return Err(miette::miette!(
241241
"OIDC authentication timed out after {} seconds.\n\
242-
Try again with: openshell gateway login",
242+
Please retry the command.",
243243
AUTH_TIMEOUT.as_secs()
244244
));
245245
}

0 commit comments

Comments
 (0)