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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ tools/bruno/collection.bru
!CHANGELOG.md
!SECURITY.md
!frontend/**/*.md

# Generate swagger files
cmd/api/docs/swagger.json
cmd/api/docs/swagger.yaml
21 changes: 21 additions & 0 deletions cmd/api/handlers/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ import (
//
// Returns the SPA-canonical paginated envelope. The handler audit-logs the
// visit on success.
// @Summary List audit logs
// @Description Returns paginated API audit log entries.
// @Tags audit
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Param q query string false "Search query"
// @Param service query string false "Service filter"
// @Param username query string false "Username filter"
// @Param env query string false "Environment filter"
// @Success 200 {object} types.AuditLogsPagedResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/audit-logs [get]
func (h *HandlersApi) AuditLogsHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down
15 changes: 15 additions & 0 deletions cmd/api/handlers/auth_logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ type LogoutResponse struct {
// "someone forged a logout request" — the only consequence is
// invalidating the legitimate user's token, which is exactly what
// logout is supposed to do. No privilege gain.
// @Summary Log out
// @Description Clears API session cookies and revokes the active token when present.
// @Tags auth
// @Accept json
// @Produce json
// @Success 200 {object} LogoutResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/logout [post]
func (h *HandlersApi) LogoutHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down
14 changes: 14 additions & 0 deletions cmd/api/handlers/auth_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ type AuthMethodsResponse struct {
// Rate-limited at the route layer (same preAuthRateLimit as the
// env/sample endpoints) to keep this from being a free metadata
// scrape vector.
// @Summary List authentication methods
// @Description Returns the authentication methods enabled for the API login UI.
// @Tags auth
// @Produce json
// @Success 200 {object} AuthMethodsResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/methods [get]
func (h *HandlersApi) AuthMethodsHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down
32 changes: 32 additions & 0 deletions cmd/api/handlers/auth_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ func InitOIDC(ctx context.Context, cfg config.YAMLConfigurationOIDC) error {
// EnvUUID on the State is a fixed sentinel ("api") because osctrl-api
// has no per-env IdP concept — see auth-providers spec § "OIDC is
// global." The legacy admin uses "admin" for the same reason.
// @Summary Start OIDC login
// @Description Redirects the browser to the configured OIDC identity provider.
// @Tags auth
// @Produce json
// @Success 200 {string} string
// @Failure 302 {string} string
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/oidc/login [get]
func (h *HandlersApi) OIDCLoginHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down Expand Up @@ -163,6 +178,23 @@ func (h *HandlersApi) OIDCLoginHandler(w http.ResponseWriter, r *http.Request) {
// server-side log records WHY; the client gets a generic outcome.
// This is the timing-oracle defense (threat T31): every failure mode
// produces an indistinguishable client-visible response.
// @Summary Complete OIDC login
// @Description Handles the OIDC authorization callback and creates an API session.
// @Tags auth
// @Produce json
// @Param code query string false "OIDC authorization code"
// @Param state query string false "OIDC state"
// @Success 200 {string} string
// @Failure 302 {string} string
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/oidc/callback [get]
func (h *HandlersApi) OIDCCallbackHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down
47 changes: 47 additions & 0 deletions cmd/api/handlers/auth_saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ func InitSAML(ctx context.Context, cfg config.YAMLConfigurationSAML, entityID, a
// EnvUUID on the State is a fixed sentinel ("api") because osctrl-api
// is single-tenant for federated login. Matches the OIDC handler's
// posture.
// @Summary Start SAML login
// @Description Redirects the browser to the configured SAML identity provider.
// @Tags auth
// @Produce json
// @Success 200 {string} string
// @Failure 302 {string} string
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/saml/login [get]
func (h *HandlersApi) SAMLLoginHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down Expand Up @@ -147,6 +162,24 @@ func (h *HandlersApi) SAMLLoginHandler(w http.ResponseWriter, r *http.Request) {
// Failure paths redirect to "/" too (no error param leak). Server-side
// log records WHY; client sees a generic outcome. Timing-oracle defense
// matches the OIDC handler.
// @Summary Complete SAML login
// @Description Handles the SAML assertion consumer service callback and creates an API session.
// @Tags auth
// @Accept json
// @Produce json
// @Param SAMLResponse formData string false "SAML response assertion"
// @Param RelayState formData string false "SAML relay state"
// @Success 200 {string} string
// @Failure 302 {string} string
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/saml/acs [post]
func (h *HandlersApi) SAMLACSHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down Expand Up @@ -208,6 +241,20 @@ func (h *HandlersApi) SAMLACSHandler(w http.ResponseWriter, r *http.Request) {
// during the first login attempt.
//
// Rate-limited at the route layer like the other unauth endpoints.
// @Summary Get SAML metadata
// @Description Returns service provider metadata for SAML identity provider registration.
// @Tags auth
// @Produce application/xml
// @Success 200 {string} string
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Router /api/v1/auth/saml/metadata [get]
func (h *HandlersApi) SAMLMetadataHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, false)
Expand Down
108 changes: 108 additions & 0 deletions cmd/api/handlers/carves.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ func carveFileView(c carves.CarvedFile) types.CarveFileView {
// Returns the carve query metadata plus the array of per-node CarvedFile rows
// produced by the carve. Returns 404 when the carve query name does not exist
// in the environment.
// @Summary Get file carve
// @Description Returns a file carve and the files produced by it.
// @Tags carves
// @Produce json
// @Param env path string true "Environment name or UUID"
// @Param name path string true "Carve query name"
// @Success 200 {object} types.CarveDetailResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env}/{name} [get]
func (h *HandlersApi) CarveShowHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down Expand Up @@ -108,6 +125,23 @@ func (h *HandlersApi) CarveShowHandler(w http.ResponseWriter, r *http.Request) {
//
// Returns carve queries by target. Retained from the legacy contract; the
// canonical list endpoint is now CarveListHandler at /api/v1/carves/{env}.
// @Summary List carve queries
// @Description Returns file carve queries by target and environment.
// @Tags carves
// @Produce json
// @Param env path string true "Environment name or UUID"
// @Param target path string true "Carve target filter"
// @Success 200 {array} queries.DistributedQuery
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env}/queries/{target} [get]
func (h *HandlersApi) CarveQueriesHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down Expand Up @@ -151,6 +185,26 @@ func (h *HandlersApi) CarveQueriesHandler(w http.ResponseWriter, r *http.Request
// Paginated, sorted, searchable list of carve queries (DistributedQuery rows
// with type=carve). Query params: page, page_size, q, sort, dir, target.
// Empty result → HTTP 200 with items: [].
// @Summary List file carves
// @Description Returns paginated file carves for an environment.
// @Tags carves
// @Produce json
// @Param env path string true "Environment name or UUID"
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Param q query string false "Search query"
// @Success 200 {object} types.CarvesPagedResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env} [get]
// @Router /api/v1/carves/{env}/list [get]
func (h *HandlersApi) CarveListHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down Expand Up @@ -222,6 +276,24 @@ func (h *HandlersApi) CarveListHandler(w http.ResponseWriter, r *http.Request) {
}

// CarvesRunHandler - POST /api/v1/carves/{env}
// @Summary Run file carve
// @Description Starts a new file carve.
// @Tags carves
// @Accept json
// @Produce json
// @Param env path string true "Environment name or UUID"
// @Param request body types.ApiDistributedQueryRequest true "Request body"
// @Success 200 {object} types.ApiQueriesResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env} [post]
func (h *HandlersApi) CarvesRunHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down Expand Up @@ -314,6 +386,25 @@ func (h *HandlersApi) CarvesRunHandler(w http.ResponseWriter, r *http.Request) {
}

// CarvesActionHandler - POST /api/v1/carves/{env}/{action}/{name}
// @Summary Execute carve action
// @Description Deletes, expires, or otherwise acts on a file carve.
// @Tags carves
// @Accept json
// @Produce json
// @Param env path string true "Environment name or UUID"
// @Param action path string true "Carve action"
// @Param name path string true "Carve query name"
// @Success 200 {object} types.ApiGenericResponse
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env}/{action}/{name} [post]
func (h *HandlersApi) CarvesActionHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down Expand Up @@ -394,6 +485,23 @@ func (h *HandlersApi) CarvesActionHandler(w http.ResponseWriter, r *http.Request
// download URL is returned via 302 redirect).
//
// Content-Disposition is set to attachment with the carve archive filename.
// @Summary Download carve archive
// @Description Downloads the archive for a completed file carve.
// @Tags carves
// @Produce application/octet-stream
// @Param env path string true "Environment name or UUID"
// @Param name path string true "Carve query name"
// @Success 200 {file} file
// @Failure 400 {object} types.ApiErrorResponse "Bad request"
// @Failure 401 {object} types.ApiErrorResponse "Unauthorized"
// @Failure 403 {object} types.ApiErrorResponse "Forbidden"
// @Failure 404 {object} types.ApiErrorResponse "Not found"
// @Failure 409 {object} types.ApiErrorResponse "Conflict"
// @Failure 429 {object} types.ApiErrorResponse "Too many requests"
// @Failure 500 {object} types.ApiErrorResponse "Internal server error"
// @Failure 503 {object} types.ApiErrorResponse "Service unavailable"
// @Security ApiKeyAuth
// @Router /api/v1/carves/{env}/archive/{name} [get]
func (h *HandlersApi) CarveArchiveHandler(w http.ResponseWriter, r *http.Request) {
if h.DebugHTTPConfig.EnableHTTP {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
Expand Down
Loading
Loading