Skip to content

feat: add access request workflow commands#39

Merged
aaearon merged 9 commits intomainfrom
feat/access-requests
Apr 20, 2026
Merged

feat: add access request workflow commands#39
aaearon merged 9 commits intomainfrom
feat/access-requests

Conversation

@aaearon
Copy link
Copy Markdown
Owner

@aaearon aaearon commented Apr 20, 2026

Summary

  • Adds grant request parent command with subcommands: submit, list, get, cancel, approve, reject
  • New internal/workflows/ package with AccessRequestService (ISP auth, offset/limit pagination)
  • Interactive submit prompts (reason, priority, date, timezone, start/end time) with full flag-based non-interactive support
  • List filter validation (state, result, priority, sort) with injection prevention
  • Timezone validation via time.LoadLocation (accepts UTC, CET, IANA identifiers)
  • Injectable DI (resolveSubmitTargetFn, submitPromptFn) for testability
  • JSON output support on all subcommands

Test plan

  • make test — all tests pass (new + existing)
  • make lint — zero violations
  • make build — binary builds
  • Manual: grant request list --state INVALID returns validation error
  • Manual: grant request list --state running works (case-insensitive)
  • Manual: grant request submit --help shows all flags
  • Manual: grant request submit with --timezone UTC accepted

Implement grant request subcommands: submit, list, get, cancel,
approve, reject. Includes workflows service with ISP auth,
interactive submit prompts, list filter validation, timezone
validation via time.LoadLocation, and injectable DI for testing.
Copilot AI review requested due to automatic review settings April 20, 2026 14:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new “access requests” workflow to the CLI by adding a Workflows API client (internal/workflows) and a new grant request command group for submitting, listing, viewing, canceling, and finalizing access requests (with JSON output support).

Changes:

  • Add internal/workflows service + models to call the Access Requests (Workflows) API, including offset/limit pagination and logging.
  • Add grant request command group with subcommands (submit, list, get, cancel, approve, reject) and JSON output types.
  • Add/extend tests and documentation/changelog entries for the new functionality.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/workflows/service.go Implements Access Requests API client (forms, list, get, submit, cancel, finalize) + pagination + response checking.
internal/workflows/service_config.go Registers service configuration (service name + required isp authenticator).
internal/workflows/logging_client.go Adds request/response logging wrapper for the workflows HTTP client.
internal/workflows/service_test.go Adds unit tests for workflows service behavior, pagination, and error handling.
internal/workflows/models/request.go Introduces Access Request domain model types (AccessRequest, RequestState, RequestResult, etc.).
internal/workflows/models/request_test.go Adds JSON unmarshal + helper method tests for request models.
internal/workflows/models/form.go Adds request form model types and IsRequired helper.
internal/workflows/models/form_test.go Adds tests for form parsing and IsRequired.
internal/workflows/models/submit.go Adds submit request payload model.
internal/workflows/models/cancel.go Adds cancel request payload model.
internal/workflows/models/finalize.go Adds finalize request payload model.
cmd/commands.go Registers NewRequestCommand() on the root command.
cmd/interfaces.go Adds accessRequestService interface for DI in command code/tests.
cmd/test_mocks.go Adds mockAccessRequestService for command tests.
cmd/request.go Adds grant request parent command, bootstrapping, formatting, and output mapping.
cmd/request_submit.go Implements grant request submit (interactive + non-interactive flags, validation, target resolution).
cmd/request_list.go Implements grant request list with validated filters/sort and text/JSON output.
cmd/request_get.go Implements grant request get (text + JSON).
cmd/request_cancel.go Implements grant request cancel (text + JSON).
cmd/request_finalize.go Implements grant request approve/reject (text + JSON).
cmd/output_types.go Adds JSON output structs for access requests.
cmd/request_test.go Adds comprehensive command tests (list/get/submit/cancel/finalize, JSON output, validation).
CLAUDE.md Documents the new Workflows API package, endpoints, and CLI commands.
CHANGELOG.md Adds changelog entries for the new grant request commands and workflows client.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +132 to +136
for range maxPages {
qp := make(map[string]string)
qp["limit"] = strconv.Itoa(limit)
qp["offset"] = strconv.Itoa(offset)
if params.Filter != "" {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListRequests stops after maxPages iterations but then returns success even if more pages remain. If totalCount > maxPages*limit, this will silently return a truncated item list while still reporting the larger totalCount. Consider tracking the page index and returning an explicit error when the loop exhausts maxPages without hitting the break condition (similar to internal/sca paginate).

Copilot uses AI. Check for mistakes.
Comment thread cmd/request.go Outdated
Comment on lines +168 to +173
// formatTimestamp truncates a timestamp to just the date+time portion (no microseconds).
func formatTimestamp(ts string) string {
if len(ts) > 19 {
return ts[:19]
}
return ts
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatTimestamp truncates the input to 19 characters, which will also strip timezone/offset information if the API ever returns RFC3339 timestamps with a trailing "Z" or "+/-HH:MM". If the goal is only to drop fractional seconds, consider parsing RFC3339/RFC3339Nano and re-formatting without subseconds while preserving the timezone, or only trimming the fractional part when present.

Copilot uses AI. Check for mistakes.
- Resolve real IANA timezone instead of "Local" default
- Validate provider flag via parseProvider() before API calls
- Add context.WithTimeout to prevent indefinite hangs
- Add confirmation prompt with --yes flag to skip
- Add inline survey validators for date/time/timezone formats
- Handle partial --target/--role by filtering selector
- Prompt only for missing fields (pass existing to promptFn)
- Add IsInteractive() guard in defaultSubmitPrompt()
- Reorder prompts: timezone before date for correct defaults
- Enforce presence of date/timezone/from/to in validation
@aaearon
Copy link
Copy Markdown
Owner Author

aaearon commented Apr 20, 2026

@copilot review the latest commit

aaearon and others added 2 commits April 20, 2026 17:17
…imestamp

- ListRequests: return errPaginationLimit sentinel error when maxPages
  exhausted instead of silently returning truncated data (matches SCA
  paginate() pattern)
- formatTimestamp: parse RFC3339Nano and reformat without fractional
  seconds while preserving timezone offset; fall back to trimming at '.'
  for non-RFC3339 timestamps
- Extract pagination limit error to package-level var errPaginationLimit
- Test: use errors.Is(err, errPaginationLimit) for precise assertion
- Test: add RFC3339 timezone cases to TestFormatTimestamp

Agent-Logs-Url: https://github.com/aaearon/grant-cli/sessions/a09d2658-41d5-414b-9336-32ddff12742f

Co-authored-by: aaearon <812640+aaearon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 20, 2026

@copilot review the latest commit

Addressed both reviewer comments in commit 8296314:

  1. ListRequests silent truncation — The for loop now returns success early when pagination completes, and returns a errPaginationLimit sentinel error after the loop body if maxPages is exhausted without finishing. A new TestListRequests_MaxPagesExceeded test verifies this using errors.Is.

  2. formatTimestamp timezone stripping — The function now parses RFC3339Nano (handles Z and ±HH:MM offsets) and reformats as RFC3339 without fractional seconds, preserving the original timezone offset. Non-RFC3339 timestamps (e.g. no TZ) fall back to trimming at the . decimal point. TestFormatTimestamp now covers UTC (Z) and offset (+05:30) cases.

aaearon added 5 commits April 20, 2026 17:33
The interactive selector now shows deduplicated workspaces (not
role/workspace combos) since on-demand requests are for roles the
user doesn't already have a JIT policy for. Users select a workspace
they have access to, then provide the role ID they want to request.

- Replace role-based target selector with workspace selector
- Add --role-id flag (required) for the role being requested
- --role flag becomes optional display name (defaults to role ID)
- Deduplicate eligibility targets to unique workspaces by ID
- Add workspace type labels in selector (Account, Subscription, etc.)
API returns mixed-case workspace types (e.g. "directory",
"management_group") while Go constants are uppercase. Use
case-insensitive comparison for display labels.
Replaces the manual Role ID prompt with a fuzzy-filterable list fetched
from the SCA on-demand role discovery endpoints. Supported workspace
types: DIRECTORY (azure_ad), ACCOUNT (aws), MANAGEMENT_GROUP
(azure_resource). Other Azure-resource scopes reject with a clear error
pointing to --role-id.

Roles are cached in ~/.grant/cache/ondemand_roles_<platform>_<sha256>.json
(shared 4h Store). Interactive prompt order is now: workspace → role →
reason → priority → timezone → date → start time → end time.
When <requestId> is omitted in a terminal, each command now shows a
fuzzy-filterable picker scoped to actionable requests:

- cancel: STARTING/RUNNING/PENDING requests you created (role=CREATOR)
- approve/reject: PENDING requests assigned to you (role=APPROVER)
- get: all requests (no filter)

Positional arg path is unchanged. Non-TTY invocation (pipe, CI) returns
ErrNotInteractive with a hint to pass the ID or run `grant request list`.

Adds internal/ui/request_selector.go (Format/Build/Select trio) and
cmd/request_picker.go (pickerScope + resolveRequestIDFn injectable var).
Guard against auth errors in non-interactive contexts by checking
ErrNotInteractive before bootstrapping the workflows service. Fix
timezone-offset timestamp sort by parsing to time.Time instead of
relying on lexicographic string comparison.
@aaearon aaearon merged commit 0dd4a93 into main Apr 20, 2026
1 check passed
@aaearon aaearon deleted the feat/access-requests branch April 20, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants