Skip to content

Self Serve Ticket Requests#1818

Open
SteezyCougar wants to merge 21 commits intowarp-tech:mainfrom
SteezyCougar:little/ticket-self-service-improvements
Open

Self Serve Ticket Requests#1818
SteezyCougar wants to merge 21 commits intowarp-tech:mainfrom
SteezyCougar:little/ticket-self-service-improvements

Conversation

@SteezyCougar
Copy link
Copy Markdown
Contributor

@SteezyCougar SteezyCougar commented Mar 23, 2026

This PR Adds a self-service ticket request system where users can request temporary access tickets from
their profile. The existing ticket system was only usable by admins, and involved an admin seeing the user's credentials (Which isn't always ideal). This PR is meant to be a pretty substantial expansion to the existing ticket system that makes it more useful, and handles a few different use cases.

Changes:

GLOBAL SETTINGS:

  1. Adds a new setting called "Allow users to request tickets". When enabled, it also adds the following settings:
    A. Auto-approve when user already has role-based access (I figured this is a easy way to solve Postgres app approval: better way to handle multiple connections #1774) as then they can just have temporary creds that can be expired whenever. Think similar to how AWS SSO flow works.
    B. Require description on ticket requests
    C. Show all targets in ticket request form (For some resources we can't allow users to have unlimited access. Instead it has to be justified, and approved. Allowing them to see all targets means they can open the requests to these resources, have an admin approve, and then get temporary access. This is probably the most useful part of this PR)
    D. Default max ticket duration (Only applies to requests, but makes it so someone can't request more than your policies allow)
    E. Max uses per ticket (Similar to above)
image

TARGET SETTINGS
The following settings will now show up in targets (But ONLY if the global setting to allow users to request tickets is enabled)

  1. Disable ticket requests for this target (Allows per-resource customization.)
  2. Always require admin approval (Can be set for more sensitive resources)
  3. Max self-service ticket duration (To set it different than the default)
  4. Max users per ticket (To set it different than the default)
image

USER PROFILE
The User profile will now show a "Ticket Requests" section underneath their username:
image

From that ticket requests menu, they can create a request, they can revoke their own active tickets (Not shown but it's below the requests)
image

TICKETS (Admin section)
This has mostly just been expanded to allow approving or denying requests (Deny will ask for a reason), but otherwise should be very similar to what it was before. All existing functionality SHOULD be preserved.

@SteezyCougar SteezyCougar force-pushed the little/ticket-self-service-improvements branch from 4686e9e to e06a6bc Compare March 23, 2026 21:59
Add ticket request workflow where users can request time-limited access
tickets through the UI. Requests can be auto-approved when users already
have role-based access, or queued for admin approval.

- Per-target and global max duration settings with client-side validation
- Admin approve/deny workflow with deny reasons
- Connection instructions shown for auto-approved tickets
- User can list own requests/tickets and revoke tickets
- Fix admin ticket delete button
The permission aggregation loop was missing these two fields, causing
the Create/Delete buttons on the admin tickets page to always be disabled.
- Hide form after successful ticket creation, show "Request another" button
- Add personal-use-only warning on auto-approved tickets
- Human-friendly duration inputs (e.g. "8h", "1d") for global and per-target settings
- Conditionally hide per-target ticket settings when self-service is disabled
- Add InfoBox tooltips explaining self-service and show-all-targets toggles
- Add "show all targets" parameter to optionally list all targets in request form
- Shorten self-service and show-all-targets InfoBox text
- Replace nested alerts with card + inline icon warnings for cleaner dark theme appearance
Add per-target controls: disable ticket requests, require admin approval,
and max uses per ticket. Admin-created tickets bypass per-target restrictions.
Targets with requests disabled are filtered from the ticket request form.
- Refresh serverInfo after saving global parameters so per-target
  ticket settings show/hide without hard refresh
- Disable request button when description is required and empty
- Use human-readable duration input (8h, 30m, 1d) instead of minutes
- Default to max limits when duration/uses omitted in ticket requests
- Reject requests for non-accessible targets when show_all_targets is off
- Re-validate duration/uses against current limits on approve
- Use entity references in migration instead of Alias::new()
- Unconditional Set for boolean target fields on update
- Use TicketRequestStatus enum for API query parameter
- Use formatDuration in admin ticket requests list
- Add Loadable wrapper and lowercase h1 in admin UI
…ion issues

- Fix UTF-8 panic on deny reason truncation (use chars not bytes)
- Fix description validation to count characters not bytes
- Move ticket_requests_disabled check after auth to prevent target existence leak
- Add OpenAPI security scheme to all gateway ticket endpoints
- Gate ticket_max_duration/uses behind auth check in target listing
- Require TicketRequestsManage permission for admin ticket list endpoint
- Approve flow verifies user by ID instead of username
- Add ticket_max_uses to gateway Info API
- Add client-side uses validation with max enforcement
- Fix unparseable duration input silently submitting
- Fix AsyncButton showing success on API failures (approve/create)
- Show deny errors inside modal instead of behind it
- Reset descriptionTouched on successful submit and new request
Fixes security issue where admin could see ticket secrets meant for the
requesting user. Now admin approval only changes status — the actual
ticket and secret are created when the user activates, ensuring the
secret is only ever shown to its owner.

- Admin approve no longer creates a ticket or returns a secret
- New gateway endpoint POST /ticket-requests/:id/activate
- User sees Activate button on approved requests
- Duration clock starts at activation, not approval
- Duration/uses re-validated against current limits at activation time
Combines the separate /config/tickets and /config/ticket-requests pages
into one unified view with clear sections: pending requests at top,
active tickets with self-service badge, and request history with filter.
- Fix test_self_service_pending_approval for two-phase approval flow
- Add activation tests: double-activate (409), pending (404), target-gone (410)
- Delete admin TicketRequests.svelte (merged into Tickets.svelte)
- Match admin Tickets.svelte flex layout pattern (me-auto, flex-shrink-0)
- Add scoped style block for list-group-item flex display
- Limit visible requests to 25 with "show all" toggle
@SteezyCougar SteezyCougar force-pushed the little/ticket-self-service-improvements branch from e06a6bc to 102eb42 Compare March 23, 2026 22:15
Comment thread warpgate-protocol-http/src/api/ticket_requests.rs Fixed
@SteezyCougar
Copy link
Copy Markdown
Contributor Author

@Eugeny apologies if it's a large PR, but I think it's to a point where it should be ready for you to review. As always feel free to change or modify according to whatever makes the most sense for the app

Copy link
Copy Markdown
Member

@Eugeny Eugeny left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I've noted a few issues, but generally it looks ok

pub minimize_password_login: Option<bool>,
pub ticket_self_service_enabled: Option<bool>,
pub ticket_auto_approve_existing_access: Option<bool>,
pub ticket_max_duration_seconds: Option<Option<i64>>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this actually translate into appropriate handling of missing fields when poem_openapi::Object gets deserialized? I suspect it just gets flattened into Option meaning any request from an older client (e.g. terraform) will wipe these fields

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is copying the same pattern the upstream (ssh_client_auth_publickey). That said I can do it differently if desired

// This ensures revoking a user's role immediately invalidates their
// self-service tickets, unlike admin-created tickets which intentionally
// bypass role checks.
if ticket.self_service {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does this interact with ticket_auto_approve_existing_access? If the user has no access via roles and the ticket is approved, this check still prevents access as far as I see

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'll push a fix for this!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ok, authorize_ticket no longer does a self service role re-check since create_ticket_request has everything we need

}

// Validate description length
if params.description.chars().count() > 2000 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hallucinated?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It seemed like it needed some sort of cap so some user doesn't paste like a ridiculous amount of logs in or something as their justification, but 2,000 is absolutely an arbitrary number. I'm not set on it specifically, but it might be worth having SOME max length so a user doesn't do something dumb

"Duration must be a positive number".into(),
));
}
if duration < 60 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These checks should happen before invalid data can get into the system (at the API boundary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's put here since it's the shared entry point for both the api and the auto-approve. I could split it up though or add additional validation on the API layer, whatever you think makes the most sense I'm down to do

Comment thread warpgate-core/src/ticket_requests.rs Outdated
};

// Validate and enforce uses limits
let max_uses = target.ticket_max_uses.or(policy.ticket_max_uses);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there actually a real world use case for the user self-limiting themselves with the number of uses?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Honestly, I think it'd be rare but it could be possible. Maybe when pairing with someone and sharing or recording your screen you might want to limit for the sake of confidence?

That said I can only think of edge-case type reasons here like that, If you think the simplification presents better I'm more than happy to update that

};

// Verify user still exists (use user_id, not username, to avoid confusion if username was reused)
let user_exists = warpgate_db_entities::User::Entity::find_by_id(request.user_id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs rebase - the Ticket model is now normalized to use IDs for both user and target

return Ok(None);
};

let reason = reason.map(|r| {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same

Comment thread warpgate-db-entities/src/Ticket.rs Outdated
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[oai(skip)]
#[serde(skip_serializing)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually the entire derive(Serialize) can be removed from this struct

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sweet, I removed Serialize and poem_openapi::Object from Ticket entirely

#[sea_orm(string_value = "denied")]
Denied,
#[sea_orm(string_value = "expired")]
Expired,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

never used

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removed!

&self,
ctx: Data<&AuthenticatedRequestContext>,
search: Query<Option<String>>,
for_ticket_request: Query<Option<bool>>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be a separate API endpoint with a minimal view into the targets (id + name) - instead of gating every field manually

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah, good call on that, I'll make that change as well

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done!

…ean up API

- Normalize Ticket entity: replace username/target strings with user_id/target_id UUID FKs
- Add data migration to convert existing tickets from string to UUID references
- Admin tickets API returns DTO with resolved username/target_name via joins
- Revert targets_list.rs to match upstream (move ticket-request filtering to dedicated endpoint)
- Add /ticket-request-targets endpoint for self-service target selection
- Rename migration m00033 -> m00041 to avoid collision with upstream migrations
- Fix authorize_ticket to match upstream 2-param signature returning 3-tuple
- Pass admin user UUID (not username) for approve/deny operations
- Remove redundant comments, add WHY comments for non-obvious behavior
- Regenerate admin and gateway OpenAPI schemas from Rust code
- Update Tickets.svelte: resolve user/target names from UUID via lookups
- Update TicketRequests.svelte: use new /ticket-request-targets endpoint,
  MyTicketModel type, remove uses field from form
- Remove references to deleted fields (requestedUses, resolvedByUsername,
  forTicketRequest, ticketMaxDurationSeconds on TargetSnapshot)
@SteezyCougar
Copy link
Copy Markdown
Contributor Author

@Eugeny just checking in to see if there is anything else

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.

4 participants