Skip to content
Closed
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
58 changes: 56 additions & 2 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2545,7 +2545,7 @@ enum WorkspaceMemberCommands {
workspace: String,

/// OIDC subject claim identifying the principal.
#[arg(long)]
#[arg(long, allow_hyphen_values = true)]
subject: String,

/// Role to assign (user or admin).
Expand All @@ -2561,7 +2561,7 @@ enum WorkspaceMemberCommands {
workspace: String,

/// OIDC subject claim identifying the principal.
#[arg(long)]
#[arg(long, allow_hyphen_values = true)]
subject: String,
},

Expand Down Expand Up @@ -5448,6 +5448,60 @@ mod tests {
));
}

#[test]
fn workspace_member_parses_hyphen_leading_subject() {
// OIDC subjects are opaque strings and can start with '-'. The remediation
// hint printed by the server quotes them (`--subject '-…'`), so that form
// must parse verbatim instead of failing with "unexpected argument".
let cli = Cli::try_parse_from([
"openshell",
"workspace",
"member",
"add",
"--workspace",
"default",
"--subject",
"-oAbC123",
"--role",
"user",
])
.expect("hyphen-leading subject should parse on add");
let Some(Commands::Workspace {
command:
Some(WorkspaceCommands::Member(WorkspaceMemberCommands::Add {
workspace,
subject,
role,
})),
}) = cli.command
else {
panic!("expected workspace member add");
};
assert_eq!(workspace, "default");
assert_eq!(subject, "-oAbC123");
assert_eq!(role, "user");

let cli = Cli::try_parse_from([
"openshell",
"workspace",
"member",
"remove",
"--workspace",
"default",
"--subject",
"-oAbC123",
])
.expect("hyphen-leading subject should parse on remove");
let Some(Commands::Workspace {
command:
Some(WorkspaceCommands::Member(WorkspaceMemberCommands::Remove { subject, .. })),
}) = cli.command
else {
panic!("expected workspace member remove");
};
assert_eq!(subject, "-oAbC123");
}

#[test]
fn remaining_list_commands_accept_structured_output() {
for (format, expected) in [("json", OutputFormat::Json), ("yaml", OutputFormat::Yaml)] {
Expand Down
23 changes: 23 additions & 0 deletions crates/openshell-server/src/auth/workspace_authz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,29 @@ mod tests {
);
}

#[tokio::test]
async fn non_member_remediation_handles_hyphen_leading_subject() {
// OIDC subjects are opaque strings and can start with '-'. The hint is
// meant to be copied verbatim, so the quoted value must stay intact and
// parse as one --subject value on the CLI side.
let store = test_store().await;
let principal = user_principal("-oAbC123", &["openshell-user"]);
let result = authorize_workspace(
&store,
"openshell-admin",
&principal,
"default",
MinWorkspaceRole::User,
)
.await;

let err = result.unwrap_err();
assert_eq!(err.code(), tonic::Code::PermissionDenied);
assert!(err.message().contains(
"openshell workspace member add --workspace 'default' --subject '-oAbC123' --role user"
));
}

#[tokio::test]
async fn anonymous_principal_rejected() {
let store = test_store().await;
Expand Down
Loading