update docs
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-04-17 14:00:48 +10:00
parent ae3e2be89a
commit 7848557002
32 changed files with 1226 additions and 90 deletions
+2 -2
View File
@@ -35,14 +35,14 @@ steps:
commands: commands:
- export PATH=/drone/src/pkg.tools:$PATH - export PATH=/drone/src/pkg.tools:$PATH
- go install github.com/a-h/templ/cmd/templ@v0.3.1001 - go install github.com/a-h/templ/cmd/templ@v0.3.1001
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.29.0 - go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.30.0
- go install github.com/swaggo/swag/cmd/swag@v1.16.6 - go install github.com/swaggo/swag/cmd/swag@v1.16.6
# - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest # - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
- sqlc generate - sqlc generate
- templ generate -path ./components - templ generate -path ./components
- swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs - swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs
- chmod +x ./scripts/*.sh - chmod +x ./scripts/*.sh
- ./scripts/update-swagger-ui.sh - ./scripts/update-swagger-ui.sh v5.32.4
- ./scripts/drone.sh - ./scripts/drone.sh
- cp ./build/vctp-linux-amd64 /shared/ - cp ./build/vctp-linux-amd64 /shared/
+5
View File
@@ -220,6 +220,11 @@ Login flow:
```http ```http
Authorization: Bearer <access_token> Authorization: Bearer <access_token>
``` ```
3. Optional whoami/debug check: call `GET /api/auth/me` with the bearer token to view current JWT identity/role claims.
Auth audit logging:
- vCTP emits structured `auth_audit` log events for login decisions, token validation denials, and role authorization denials.
- Logs include request metadata and decision reason, but do not log credentials or raw bearer tokens.
Auth modes: Auth modes:
- `settings.auth_mode: disabled`: middleware bypassed. - `settings.auth_mode: disabled`: middleware bypassed.
+10
View File
@@ -1,3 +1,13 @@
// Package main vCTP API entrypoint.
//
// @title vCTP API
// @version 1.0
// @description vCTP API endpoints for inventory snapshots, reporting, and administration.
// @BasePath /
// @schemes http https
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
package main package main
import ( import (
+33
View File
@@ -0,0 +1,33 @@
package audit
import (
"log/slog"
"net/http"
)
const authAuditMessage = "auth_audit"
// LogAuthEvent emits a structured auth audit log record.
// It is intentionally generic and should never receive raw credentials or tokens.
func LogAuthEvent(logger *slog.Logger, r *http.Request, event string, outcome string, attrs ...any) {
if logger == nil {
logger = slog.Default()
}
logAttrs := make([]any, 0, 14+len(attrs))
logAttrs = append(logAttrs, "category", "auth", "event", event, "outcome", outcome)
if r != nil {
requestPath := r.URL.RequestURI()
if requestPath == "" {
requestPath = r.URL.Path
}
logAttrs = append(logAttrs,
"method", r.Method,
"path", requestPath,
"remote", r.RemoteAddr,
)
}
logAttrs = append(logAttrs, attrs...)
logger.Info(authAuditMessage, logAttrs...)
}
+52 -5
View File
@@ -7,6 +7,8 @@ import (
"strings" "strings"
"time" "time"
"vctp/internal/auth" "vctp/internal/auth"
"vctp/server/audit"
"vctp/server/middleware"
"vctp/server/models" "vctp/server/models"
) )
@@ -50,12 +52,14 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
if h == nil || h.Settings == nil || h.Settings.Values == nil { if h == nil || h.Settings == nil || h.Settings.Values == nil {
audit.LogAuthEvent(nil, r, "login", "error", "reason", "settings_not_configured")
writeJSONError(w, http.StatusInternalServerError, "authentication is not configured") writeJSONError(w, http.StatusInternalServerError, "authentication is not configured")
return return
} }
cfg := h.Settings.Values.Settings cfg := h.Settings.Values.Settings
if !cfg.AuthEnabled { if !cfg.AuthEnabled {
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "auth_disabled")
writeJSONError(w, http.StatusServiceUnavailable, "authentication is disabled") writeJSONError(w, http.StatusServiceUnavailable, "authentication is disabled")
return return
} }
@@ -63,12 +67,14 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
var req models.AuthLoginRequest var req models.AuthLoginRequest
if err := decodeJSONBody(w, r, &req); err != nil { if err := decodeJSONBody(w, r, &req); err != nil {
h.Logger.Error("unable to decode auth login request", "error", err) h.Logger.Error("unable to decode auth login request", "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "invalid_request_json", "error", err)
writeJSONError(w, http.StatusBadRequest, "invalid JSON body") writeJSONError(w, http.StatusBadRequest, "invalid JSON body")
return return
} }
username := strings.TrimSpace(req.Username) username := strings.TrimSpace(req.Username)
password := req.Password password := req.Password
if username == "" || strings.TrimSpace(password) == "" { if username == "" || strings.TrimSpace(password) == "" {
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "missing_username_or_password", "username", username)
writeJSONError(w, http.StatusBadRequest, "username and password are required") writeJSONError(w, http.StatusBadRequest, "username and password are required")
return return
} }
@@ -83,6 +89,7 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
}) })
if err != nil { if err != nil {
h.Logger.Error("failed to initialize ldap authenticator", "error", err) h.Logger.Error("failed to initialize ldap authenticator", "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "error", "reason", "ldap_authenticator_init_failed", "username", username, "error", err)
writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable") writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable")
return return
} }
@@ -92,23 +99,23 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
identity, err := ldapAuth.AuthenticateAndFetchGroups(ctx, username, password) identity, err := ldapAuth.AuthenticateAndFetchGroups(ctx, username, password)
if err != nil { if err != nil {
if errors.Is(err, auth.ErrLDAPInvalidCredentials) { if errors.Is(err, auth.ErrLDAPInvalidCredentials) {
h.Logger.Warn("auth login rejected", "username", username, "reason", "invalid_credentials") audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "invalid_credentials", "username", username)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage) writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return return
} }
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
h.Logger.Warn("auth login ldap timeout", "username", username, "error", err) audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "ldap_timeout", "username", username, "error", err)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage) writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return return
} }
h.Logger.Warn("auth login ldap failure", "username", username, "error", err) audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "ldap_authentication_failed", "username", username, "error", err)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage) writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return return
} }
roles := auth.ResolveRoles(identity.Groups, cfg.AuthGroupRoleMappings) roles := auth.ResolveRoles(identity.Groups, cfg.AuthGroupRoleMappings)
if !auth.HasAnyGroup(identity.Groups, cfg.LDAPGroups) || len(roles) == 0 { if !auth.HasAnyGroup(identity.Groups, cfg.LDAPGroups) || len(roles) == 0 {
h.Logger.Warn("auth login rejected", "username", username, "reason", "group_or_role_denied") audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "group_or_role_denied", "username", username, "group_count", len(identity.Groups), "resolved_roles", roles)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage) writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return return
} }
@@ -122,6 +129,7 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
}) })
if err != nil { if err != nil {
h.Logger.Error("failed to initialize jwt service", "error", err) h.Logger.Error("failed to initialize jwt service", "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "error", "reason", "jwt_service_init_failed", "username", username, "error", err)
writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable") writeJSONError(w, http.StatusInternalServerError, "authentication service unavailable")
return return
} }
@@ -133,14 +141,53 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
token, claims, err := jwtSvc.IssueToken(subject, roles, identity.Groups) token, claims, err := jwtSvc.IssueToken(subject, roles, identity.Groups)
if err != nil { if err != nil {
h.Logger.Error("failed to issue auth token", "username", username, "error", err) h.Logger.Error("failed to issue auth token", "username", username, "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "error", "reason", "token_issue_failed", "username", username, "error", err)
writeJSONError(w, http.StatusInternalServerError, "failed to issue access token") writeJSONError(w, http.StatusInternalServerError, "failed to issue access token")
return return
} }
h.Logger.Info("auth login successful", "username", subject, "roles", roles) audit.LogAuthEvent(h.Logger, r, "login", "allow", "username", subject, "resolved_roles", roles, "expires_at", claims.ExpiresAt)
writeJSON(w, http.StatusOK, models.AuthLoginResponse{ writeJSON(w, http.StatusOK, models.AuthLoginResponse{
AccessToken: token, AccessToken: token,
ExpiresAt: claims.ExpiresAt, ExpiresAt: claims.ExpiresAt,
TokenType: "Bearer", TokenType: "Bearer",
}) })
} }
// AuthMe returns the currently authenticated identity from validated JWT claims.
// @Summary Who am I
// @Description Returns JWT claims for the currently authenticated bearer token.
// @Description Requires Bearer authentication.
// @Tags auth
// @Produce json
// @Success 200 {object} models.AuthMeResponse "Authenticated identity"
// @Failure 401 {object} models.ErrorResponse "Missing or invalid authentication context"
// @Router /api/auth/me [get]
// @Security BearerAuth
func (h *Handler) AuthMe(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
claims, ok := middleware.ClaimsFromContext(r.Context())
if !ok {
audit.LogAuthEvent(h.Logger, r, "whoami", "deny", "reason", "missing_auth_context")
writeJSONError(w, http.StatusUnauthorized, "missing authentication context")
return
}
audit.LogAuthEvent(h.Logger, r, "whoami", "allow", "subject", claims.Subject, "roles", claims.Roles)
writeJSON(w, http.StatusOK, models.AuthMeResponse{
Status: "OK",
Subject: claims.Subject,
Roles: claims.Roles,
Groups: claims.Groups,
Issuer: claims.Issuer,
Audience: claims.Audience,
IssuedAt: claims.IssuedAt,
ExpiresAt: claims.ExpiresAt,
NotBefore: claims.NotBefore,
TokenID: claims.ID,
})
}
+75
View File
@@ -12,6 +12,7 @@ import (
"time" "time"
"vctp/internal/auth" "vctp/internal/auth"
"vctp/internal/settings" "vctp/internal/settings"
"vctp/server/middleware"
"vctp/server/models" "vctp/server/models"
) )
@@ -187,6 +188,80 @@ func TestAuthLoginJWTFactoryFailure(t *testing.T) {
} }
} }
func TestAuthMeSuccess(t *testing.T) {
h := &Handler{
Logger: newTestLogger(),
Settings: testAuthEnabledSettings(),
}
protected := middleware.RequireAuth(newTestLogger(), h.Settings)(http.HandlerFunc(h.AuthMe))
tokenSvc, err := auth.NewJWTService(auth.JWTConfig{
SigningKeyBase64: h.Settings.Values.Settings.AuthJWTSigningKey,
Issuer: h.Settings.Values.Settings.AuthJWTIssuer,
Audience: h.Settings.Values.Settings.AuthJWTAudience,
TokenLifespan: time.Duration(h.Settings.Values.Settings.AuthTokenLifespanMinutes) * time.Minute,
ClockSkew: time.Duration(h.Settings.Values.Settings.AuthClockSkewSeconds) * time.Second,
})
if err != nil {
t.Fatalf("failed to create jwt service: %v", err)
}
token, claims, err := tokenSvc.IssueToken("alice", []string{"viewer"}, []string{"cn=vctp-viewers,ou=groups,dc=example,dc=com"})
if err != nil {
t.Fatalf("failed to issue token: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
rr := httptest.NewRecorder()
protected.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
}
var payload models.AuthMeResponse
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if payload.Status != "OK" {
t.Fatalf("unexpected status: %q", payload.Status)
}
if payload.Subject != claims.Subject {
t.Fatalf("unexpected subject: %q", payload.Subject)
}
if payload.Issuer != claims.Issuer || payload.Audience != claims.Audience {
t.Fatalf("unexpected issuer/audience: %q/%q", payload.Issuer, payload.Audience)
}
if payload.TokenID != claims.ID {
t.Fatalf("unexpected token id: %q", payload.TokenID)
}
}
func TestAuthMeMissingAuthContext(t *testing.T) {
h := &Handler{
Logger: newTestLogger(),
}
req := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
rr := httptest.NewRecorder()
h.AuthMe(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rr.Code)
}
}
func TestAuthMeMethodNotAllowed(t *testing.T) {
h := &Handler{
Logger: newTestLogger(),
}
req := httptest.NewRequest(http.MethodPost, "/api/auth/me", nil)
rr := httptest.NewRecorder()
h.AuthMe(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rr.Code)
}
}
func testAuthEnabledSettings() *settings.Settings { func testAuthEnabledSettings() *settings.Settings {
cfg := &settings.Settings{Values: &settings.SettingsYML{}} cfg := &settings.Settings{Values: &settings.SettingsYML{}}
cfg.Values.Settings.AuthEnabled = true cfg.Values.Settings.AuthEnabled = true
@@ -13,6 +13,7 @@ import (
// DailyCreationDiagnostics returns missing CreationTime diagnostics for a daily summary table. // DailyCreationDiagnostics returns missing CreationTime diagnostics for a daily summary table.
// @Summary Daily summary CreationTime diagnostics // @Summary Daily summary CreationTime diagnostics
// @Description Returns counts of daily summary rows missing CreationTime and sample rows for the given date. // @Description Returns counts of daily summary rows missing CreationTime and sample rows for the given date.
// @Description Requires Bearer authentication with the viewer role (admin also allowed).
// @Tags diagnostics // @Tags diagnostics
// @Produce json // @Produce json
// @Param date query string true "Daily date (YYYY-MM-DD)" // @Param date query string true "Daily date (YYYY-MM-DD)"
@@ -20,6 +21,7 @@ import (
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 404 {object} models.ErrorResponse "Summary not found" // @Failure 404 {object} models.ErrorResponse "Summary not found"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/diagnostics/daily-creation [get] // @Router /api/diagnostics/daily-creation [get]
func (h *Handler) DailyCreationDiagnostics(w http.ResponseWriter, r *http.Request) { func (h *Handler) DailyCreationDiagnostics(w http.ResponseWriter, r *http.Request) {
dateValue := strings.TrimSpace(r.URL.Query().Get("date")) dateValue := strings.TrimSpace(r.URL.Query().Get("date"))
+2
View File
@@ -17,6 +17,7 @@ type encryptRequest struct {
// EncryptData encrypts a plaintext value and returns the ciphertext. // EncryptData encrypts a plaintext value and returns the ciphertext.
// @Summary Encrypt data // @Summary Encrypt data
// @Description Encrypts a plaintext value and returns the ciphertext. // @Description Encrypts a plaintext value and returns the ciphertext.
// @Description Requires Bearer authentication with the admin role.
// @Tags crypto // @Tags crypto
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -24,6 +25,7 @@ type encryptRequest struct {
// @Success 200 {object} models.StatusMessageResponse "Ciphertext response" // @Success 200 {object} models.StatusMessageResponse "Ciphertext response"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/encrypt [post] // @Router /api/encrypt [post]
func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) { func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+4
View File
@@ -9,10 +9,12 @@ import (
// InventoryReportDownload returns the inventory report as an XLSX download. // InventoryReportDownload returns the inventory report as an XLSX download.
// @Summary Download inventory report // @Summary Download inventory report
// @Description Generates an inventory XLSX report and returns it as a file download. // @Description Generates an inventory XLSX report and returns it as a file download.
// @Description Requires Bearer authentication with the viewer role (admin also allowed).
// @Tags reports // @Tags reports
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
// @Success 200 {file} file "Inventory XLSX report" // @Success 200 {file} file "Inventory XLSX report"
// @Failure 500 {object} models.ErrorResponse "Report generation failed" // @Failure 500 {object} models.ErrorResponse "Report generation failed"
// @Security BearerAuth
// @Router /api/report/inventory [get] // @Router /api/report/inventory [get]
func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := withRequestTimeout(r, reportRequestTimeout) ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
@@ -38,10 +40,12 @@ func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request
// UpdateReportDownload returns the updates report as an XLSX download. // UpdateReportDownload returns the updates report as an XLSX download.
// @Summary Download updates report // @Summary Download updates report
// @Description Generates an updates XLSX report and returns it as a file download. // @Description Generates an updates XLSX report and returns it as a file download.
// @Description Requires Bearer authentication with the viewer role (admin also allowed).
// @Tags reports // @Tags reports
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
// @Success 200 {file} file "Updates XLSX report" // @Success 200 {file} file "Updates XLSX report"
// @Failure 500 {object} models.ErrorResponse "Report generation failed" // @Failure 500 {object} models.ErrorResponse "Report generation failed"
// @Security BearerAuth
// @Router /api/report/updates [get] // @Router /api/report/updates [get]
func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := withRequestTimeout(r, reportRequestTimeout) ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
+2
View File
@@ -11,6 +11,7 @@ import (
// SnapshotAggregateForce forces regeneration of a daily or monthly summary table. // SnapshotAggregateForce forces regeneration of a daily or monthly summary table.
// @Summary Force snapshot aggregation // @Summary Force snapshot aggregation
// @Description Forces regeneration of a daily or monthly summary table for a specified date or month. // @Description Forces regeneration of a daily or monthly summary table for a specified date or month.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Produce json // @Produce json
// @Param type query string true "Aggregation type: daily or monthly" // @Param type query string true "Aggregation type: daily or monthly"
@@ -19,6 +20,7 @@ import (
// @Success 200 {object} models.StatusResponse "Aggregation complete" // @Success 200 {object} models.StatusResponse "Aggregation complete"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/snapshots/aggregate [post] // @Router /api/snapshots/aggregate [post]
func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -10,6 +10,7 @@ import (
// SnapshotForceHourly triggers an on-demand hourly snapshot run. // SnapshotForceHourly triggers an on-demand hourly snapshot run.
// @Summary Trigger hourly snapshot (manual) // @Summary Trigger hourly snapshot (manual)
// @Description Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution. // @Description Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -17,6 +18,7 @@ import (
// @Success 200 {object} models.StatusResponse "Snapshot started" // @Success 200 {object} models.StatusResponse "Snapshot started"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/snapshots/hourly/force [post] // @Router /api/snapshots/hourly/force [post]
func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -9,10 +9,12 @@ import (
// SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names. // SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names.
// @Summary Migrate snapshot registry // @Summary Migrate snapshot registry
// @Description Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names. // @Description Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Produce json // @Produce json
// @Success 200 {object} models.SnapshotMigrationResponse "Migration results" // @Success 200 {object} models.SnapshotMigrationResponse "Migration results"
// @Failure 500 {object} models.SnapshotMigrationResponse "Server error" // @Failure 500 {object} models.SnapshotMigrationResponse "Server error"
// @Security BearerAuth
// @Router /api/snapshots/migrate [post] // @Router /api/snapshots/migrate [post]
func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@@ -13,10 +13,12 @@ import (
// SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk. // SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk.
// @Summary Regenerate hourly snapshot reports // @Summary Regenerate hourly snapshot reports
// @Description Regenerates XLSX reports for hourly snapshots when the report files are missing or empty. // @Description Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Produce json // @Produce json
// @Success 200 {object} models.SnapshotRegenerateReportsResponse "Regeneration summary" // @Success 200 {object} models.SnapshotRegenerateReportsResponse "Regeneration summary"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/snapshots/regenerate-hourly-reports [post] // @Router /api/snapshots/regenerate-hourly-reports [post]
func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+4
View File
@@ -15,9 +15,11 @@ import (
// SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields. // SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields.
// @Summary Repair daily summaries // @Summary Repair daily summaries
// @Description Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data. // @Description Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Produce json // @Produce json
// @Success 200 {object} models.SnapshotRepairResponse // @Success 200 {object} models.SnapshotRepairResponse
// @Security BearerAuth
// @Router /api/snapshots/repair [post] // @Router /api/snapshots/repair [post]
func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@@ -98,9 +100,11 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
// It rebuilds the snapshot registry, syncs vcenter totals, repairs daily summaries, and refines monthly lifecycle data. // It rebuilds the snapshot registry, syncs vcenter totals, repairs daily summaries, and refines monthly lifecycle data.
// @Summary Run full snapshot repair suite // @Summary Run full snapshot repair suite
// @Description Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle. // @Description Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.
// @Description Requires Bearer authentication with the admin role.
// @Tags snapshots // @Tags snapshots
// @Produce json // @Produce json
// @Success 200 {object} models.SnapshotRepairSuiteResponse // @Success 200 {object} models.SnapshotRepairSuiteResponse
// @Security BearerAuth
// @Router /api/snapshots/repair/all [post] // @Router /api/snapshots/repair/all [post]
func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -49,12 +49,14 @@ func (h *Handler) SnapshotMonthlyList(w http.ResponseWriter, r *http.Request) {
// SnapshotReportDownload streams a snapshot table as XLSX. // SnapshotReportDownload streams a snapshot table as XLSX.
// @Summary Download snapshot report // @Summary Download snapshot report
// @Description Downloads a snapshot table as an XLSX file. // @Description Downloads a snapshot table as an XLSX file.
// @Description Requires Bearer authentication with the viewer role (admin also allowed).
// @Tags snapshots // @Tags snapshots
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
// @Param table query string true "Snapshot table name" // @Param table query string true "Snapshot table name"
// @Success 200 {file} file "Snapshot XLSX report" // @Success 200 {file} file "Snapshot XLSX report"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/report/snapshot [get] // @Router /api/report/snapshot [get]
func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := withRequestTimeout(r, reportRequestTimeout) ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
+2
View File
@@ -8,11 +8,13 @@ import (
// UpdateCleanup removes orphaned update records. // UpdateCleanup removes orphaned update records.
// @Summary Cleanup updates (deprecated) // @Summary Cleanup updates (deprecated)
// @Description Deprecated: Removes update records that are no longer associated with a VM. // @Description Deprecated: Removes update records that are no longer associated with a VM.
// @Description Requires Bearer authentication with the admin role.
// @Tags maintenance // @Tags maintenance
// @Deprecated // @Deprecated
// @Produce text/plain // @Produce text/plain
// @Success 200 {string} string "Cleanup completed" // @Success 200 {string} string "Cleanup completed"
// @Failure 500 {string} string "Server error" // @Failure 500 {string} string "Server error"
// @Security BearerAuth
// @Router /api/cleanup/updates [delete] // @Router /api/cleanup/updates [delete]
func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) { func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
+2
View File
@@ -15,6 +15,7 @@ import (
// VcenterCacheRebuild force-regenerates cached vCenter reference data in the database. // VcenterCacheRebuild force-regenerates cached vCenter reference data in the database.
// @Summary Rebuild vCenter object cache // @Summary Rebuild vCenter object cache
// @Description Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables. // @Description Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.
// @Description Requires Bearer authentication with the admin role.
// @Tags vcenters // @Tags vcenters
// @Produce json // @Produce json
// @Param vcenter query string false "Optional single vCenter URL to rebuild; defaults to all configured vCenters" // @Param vcenter query string false "Optional single vCenter URL to rebuild; defaults to all configured vCenters"
@@ -22,6 +23,7 @@ import (
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 405 {object} models.ErrorResponse "Method not allowed" // @Failure 405 {object} models.ErrorResponse "Method not allowed"
// @Failure 500 {object} models.VcenterCacheRebuildResponse "All rebuild attempts failed" // @Failure 500 {object} models.VcenterCacheRebuildResponse "All rebuild attempts failed"
// @Security BearerAuth
// @Router /api/vcenters/cache/rebuild [post] // @Router /api/vcenters/cache/rebuild [post]
func (h *Handler) VcenterCacheRebuild(w http.ResponseWriter, r *http.Request) { func (h *Handler) VcenterCacheRebuild(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -11,6 +11,7 @@ import (
// VmCleanup removes a VM from inventory by ID and datacenter. // VmCleanup removes a VM from inventory by ID and datacenter.
// @Summary Cleanup VM inventory entry (deprecated) // @Summary Cleanup VM inventory entry (deprecated)
// @Description Deprecated: Removes a VM inventory entry by VM ID and datacenter name. // @Description Deprecated: Removes a VM inventory entry by VM ID and datacenter name.
// @Description Requires Bearer authentication with the admin role.
// @Tags inventory // @Tags inventory
// @Deprecated // @Deprecated
// @Produce json // @Produce json
@@ -18,6 +19,7 @@ import (
// @Param datacenter_name query string true "Datacenter name" // @Param datacenter_name query string true "Datacenter name"
// @Success 200 {object} models.StatusMessageResponse "Cleanup completed" // @Success 200 {object} models.StatusMessageResponse "Cleanup completed"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Security BearerAuth
// @Router /api/inventory/vm/delete [delete] // @Router /api/inventory/vm/delete [delete]
func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
+2
View File
@@ -15,6 +15,7 @@ import (
// VmCreateEvent records a VM creation CloudEvent. // VmCreateEvent records a VM creation CloudEvent.
// @Summary Record VM create event (deprecated) // @Summary Record VM create event (deprecated)
// @Description Deprecated: Parses a VM create CloudEvent and stores the event data. // @Description Deprecated: Parses a VM create CloudEvent and stores the event data.
// @Description Requires Bearer authentication with the admin role.
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
@@ -23,6 +24,7 @@ import (
// @Success 200 {object} models.StatusMessageResponse "Create event processed" // @Success 200 {object} models.StatusMessageResponse "Create event processed"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/event/vm/create [post] // @Router /api/event/vm/create [post]
func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -12,6 +12,7 @@ import (
// VmDeleteEvent records a VM deletion CloudEvent in the inventory. // VmDeleteEvent records a VM deletion CloudEvent in the inventory.
// @Summary Record VM delete event (deprecated) // @Summary Record VM delete event (deprecated)
// @Description Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory. // @Description Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
// @Description Requires Bearer authentication with the admin role.
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
@@ -20,6 +21,7 @@ import (
// @Success 200 {object} models.StatusMessageResponse "Delete event processed" // @Success 200 {object} models.StatusMessageResponse "Delete event processed"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/event/vm/delete [post] // @Router /api/event/vm/delete [post]
func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -14,6 +14,7 @@ import (
// VmImport ingests a bulk VM import payload. // VmImport ingests a bulk VM import payload.
// @Summary Import VMs (deprecated) // @Summary Import VMs (deprecated)
// @Description Deprecated: Imports existing VM inventory data in bulk. // @Description Deprecated: Imports existing VM inventory data in bulk.
// @Description Requires Bearer authentication with the admin role.
// @Tags inventory // @Tags inventory
// @Deprecated // @Deprecated
// @Accept json // @Accept json
@@ -21,6 +22,7 @@ import (
// @Param import body models.ImportReceived true "Bulk import payload" // @Param import body models.ImportReceived true "Bulk import payload"
// @Success 200 {object} models.StatusMessageResponse "Import processed" // @Success 200 {object} models.StatusMessageResponse "Import processed"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/import/vm [post] // @Router /api/import/vm [post]
func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -21,6 +21,7 @@ import (
// VmModifyEvent records a VM modification CloudEvent. // VmModifyEvent records a VM modification CloudEvent.
// @Summary Record VM modify event (deprecated) // @Summary Record VM modify event (deprecated)
// @Description Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected. // @Description Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.
// @Description Requires Bearer authentication with the admin role.
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
@@ -29,6 +30,7 @@ import (
// @Success 200 {object} models.StatusMessageResponse "Modify event processed" // @Success 200 {object} models.StatusMessageResponse "Modify event processed"
// @Success 202 {object} models.StatusMessageResponse "No relevant changes" // @Success 202 {object} models.StatusMessageResponse "No relevant changes"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/event/vm/modify [post] // @Router /api/event/vm/modify [post]
func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -14,6 +14,7 @@ import (
// VmMoveEvent records a VM move CloudEvent as an update. // VmMoveEvent records a VM move CloudEvent as an update.
// @Summary Record VM move event (deprecated) // @Summary Record VM move event (deprecated)
// @Description Deprecated: Parses a VM move CloudEvent and creates an update record. // @Description Deprecated: Parses a VM move CloudEvent and creates an update record.
// @Description Requires Bearer authentication with the admin role.
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
@@ -22,6 +23,7 @@ import (
// @Success 200 {object} models.StatusMessageResponse "Move event processed" // @Success 200 {object} models.StatusMessageResponse "Move event processed"
// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/event/vm/move [post] // @Router /api/event/vm/move [post]
func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+2
View File
@@ -12,11 +12,13 @@ import (
// VmUpdateDetails refreshes inventory metadata from vCenter. // VmUpdateDetails refreshes inventory metadata from vCenter.
// @Summary Refresh VM details (deprecated) // @Summary Refresh VM details (deprecated)
// @Description Deprecated: Queries vCenter and updates inventory records with missing details. // @Description Deprecated: Queries vCenter and updates inventory records with missing details.
// @Description Requires Bearer authentication with the admin role.
// @Tags inventory // @Tags inventory
// @Deprecated // @Deprecated
// @Produce json // @Produce json
// @Success 200 {object} models.StatusMessageResponse "Update completed" // @Success 200 {object} models.StatusMessageResponse "Update completed"
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Security BearerAuth
// @Router /api/inventory/vm/update [post] // @Router /api/inventory/vm/update [post]
func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
+7 -1
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"vctp/internal/auth" "vctp/internal/auth"
"vctp/internal/settings" "vctp/internal/settings"
"vctp/server/audit"
) )
const ( const (
@@ -61,6 +62,7 @@ func RequireAuth(logger *slog.Logger, cfg *settings.Settings) Handler {
}) })
if err != nil { if err != nil {
logger.Error("auth middleware init failed", "error", err) logger.Error("auth middleware init failed", "error", err)
audit.LogAuthEvent(logger, nil, "auth_middleware_init", "error", "reason", "jwt_service_init_failed", "error", err)
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeJSONAuthError(w, http.StatusServiceUnavailable, "authentication service unavailable") writeJSONAuthError(w, http.StatusServiceUnavailable, "authentication service unavailable")
@@ -73,6 +75,7 @@ func RequireAuth(logger *slog.Logger, cfg *settings.Settings) Handler {
token, hasHeader, parseOK := extractBearerToken(r.Header.Get("Authorization")) token, hasHeader, parseOK := extractBearerToken(r.Header.Get("Authorization"))
if !hasHeader { if !hasHeader {
if mode == authModeRequired { if mode == authModeRequired {
audit.LogAuthEvent(logger, r, "token_validation", "deny", "reason", "missing_bearer_token", "auth_mode", mode)
writeJSONAuthError(w, http.StatusUnauthorized, "missing bearer token") writeJSONAuthError(w, http.StatusUnauthorized, "missing bearer token")
return return
} }
@@ -80,13 +83,14 @@ func RequireAuth(logger *slog.Logger, cfg *settings.Settings) Handler {
return return
} }
if !parseOK { if !parseOK {
audit.LogAuthEvent(logger, r, "token_validation", "deny", "reason", "invalid_bearer_header", "auth_mode", mode)
writeJSONAuthError(w, http.StatusUnauthorized, "invalid bearer token") writeJSONAuthError(w, http.StatusUnauthorized, "invalid bearer token")
return return
} }
claims, err := jwtSvc.VerifyToken(token) claims, err := jwtSvc.VerifyToken(token)
if err != nil { if err != nil {
logger.Warn("auth middleware token validation failed", "path", r.URL.Path, "error", err) audit.LogAuthEvent(logger, r, "token_validation", "deny", "reason", "invalid_token", "error", err)
writeJSONAuthError(w, http.StatusUnauthorized, "invalid bearer token") writeJSONAuthError(w, http.StatusUnauthorized, "invalid bearer token")
return return
} }
@@ -111,10 +115,12 @@ func RequireRole(requiredRoles ...string) Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := ClaimsFromContext(r.Context()) claims, ok := ClaimsFromContext(r.Context())
if !ok { if !ok {
audit.LogAuthEvent(nil, r, "role_authorization", "deny", "reason", "missing_auth_context", "required_roles", normalizedRequired)
writeJSONAuthError(w, http.StatusUnauthorized, "missing authentication context") writeJSONAuthError(w, http.StatusUnauthorized, "missing authentication context")
return return
} }
if !hasAnyRequiredRole(claims.Roles, normalizedRequired) { if !hasAnyRequiredRole(claims.Roles, normalizedRequired) {
audit.LogAuthEvent(nil, r, "role_authorization", "deny", "reason", "insufficient_role", "required_roles", normalizedRequired, "user_roles", normalizeRoles(claims.Roles), "subject", claims.Subject)
writeJSONAuthError(w, http.StatusForbidden, "insufficient role") writeJSONAuthError(w, http.StatusForbidden, "insufficient role")
return return
} }
+14
View File
@@ -30,6 +30,20 @@ type AuthLoginResponse struct {
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
} }
// AuthMeResponse represents the authenticated identity extracted from JWT claims.
type AuthMeResponse struct {
Status string `json:"status"`
Subject string `json:"subject"`
Roles []string `json:"roles,omitempty"`
Groups []string `json:"groups,omitempty"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
IssuedAt int64 `json:"issued_at"`
ExpiresAt int64 `json:"expires_at"`
NotBefore int64 `json:"not_before"`
TokenID string `json:"token_id"`
}
// SnapshotMigrationStats mirrors the snapshot registry migration stats payload. // SnapshotMigrationStats mirrors the snapshot registry migration stats payload.
type SnapshotMigrationStats struct { type SnapshotMigrationStats struct {
HourlyRenamed int `json:"HourlyRenamed"` HourlyRenamed int `json:"HourlyRenamed"`
+190
View File
@@ -0,0 +1,190 @@
package router
import (
"encoding/base64"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"vctp/internal/auth"
"vctp/internal/secrets"
"vctp/internal/settings"
)
func TestProtectedRoutesRequireAuthentication(t *testing.T) {
cfg := testRouterSettings(t, false)
app := testRouter(t, cfg)
tests := []struct {
name string
method string
path string
body string
}{
{
name: "auth me",
method: http.MethodGet,
path: "/api/auth/me",
},
{
name: "viewer route",
method: http.MethodGet,
path: "/api/report/snapshot",
},
{
name: "admin route",
method: http.MethodPost,
path: "/api/encrypt",
body: `{"plaintext":"hello"}`,
},
{
name: "legacy route",
method: http.MethodPost,
path: "/api/import/vm",
body: `{`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
if tc.body != "" {
req.Header.Set("Content-Type", "application/json")
}
rr := httptest.NewRecorder()
app.ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d for %s %s", http.StatusUnauthorized, rr.Code, tc.method, tc.path)
}
})
}
}
func TestViewerCanReadButCannotMutate(t *testing.T) {
cfg := testRouterSettings(t, false)
app := testRouter(t, cfg)
viewerToken := mustIssueToken(t, cfg, "viewer-user", []string{"viewer"})
readReq := httptest.NewRequest(http.MethodGet, "/api/report/snapshot", nil)
readReq.Header.Set("Authorization", "Bearer "+viewerToken)
readRR := httptest.NewRecorder()
app.ServeHTTP(readRR, readReq)
if readRR.Code != http.StatusBadRequest {
t.Fatalf("expected viewer read to reach handler and return %d, got %d", http.StatusBadRequest, readRR.Code)
}
mutateReq := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"hello"}`))
mutateReq.Header.Set("Authorization", "Bearer "+viewerToken)
mutateReq.Header.Set("Content-Type", "application/json")
mutateRR := httptest.NewRecorder()
app.ServeHTTP(mutateRR, mutateReq)
if mutateRR.Code != http.StatusForbidden {
t.Fatalf("expected viewer mutate to return %d, got %d", http.StatusForbidden, mutateRR.Code)
}
}
func TestAdminCanMutate(t *testing.T) {
cfg := testRouterSettings(t, false)
app := testRouter(t, cfg)
adminToken := mustIssueToken(t, cfg, "admin-user", []string{"admin"})
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"hello"}`))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
app.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected admin mutate to return %d, got %d body=%s", http.StatusOK, rr.Code, rr.Body.String())
}
var payload map[string]string
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if payload["status"] != "OK" {
t.Fatalf("expected status OK, got %q", payload["status"])
}
if strings.TrimSpace(payload["ciphertext"]) == "" {
t.Fatal("expected ciphertext in response")
}
}
func TestLegacyEndpointGatingStillAppliesWithAuthEnabled(t *testing.T) {
disabledCfg := testRouterSettings(t, false)
disabledApp := testRouter(t, disabledCfg)
adminToken := mustIssueToken(t, disabledCfg, "admin-user", []string{"admin"})
disabledReq := httptest.NewRequest(http.MethodPost, "/api/import/vm", strings.NewReader(`{`))
disabledReq.Header.Set("Authorization", "Bearer "+adminToken)
disabledReq.Header.Set("Content-Type", "application/json")
disabledRR := httptest.NewRecorder()
disabledApp.ServeHTTP(disabledRR, disabledReq)
if disabledRR.Code != http.StatusGone {
t.Fatalf("expected legacy-disabled route to return %d, got %d", http.StatusGone, disabledRR.Code)
}
enabledCfg := testRouterSettings(t, true)
enabledApp := testRouter(t, enabledCfg)
enabledToken := mustIssueToken(t, enabledCfg, "admin-user", []string{"admin"})
enabledReq := httptest.NewRequest(http.MethodPost, "/api/import/vm", strings.NewReader(`{`))
enabledReq.Header.Set("Authorization", "Bearer "+enabledToken)
enabledReq.Header.Set("Content-Type", "application/json")
enabledRR := httptest.NewRecorder()
enabledApp.ServeHTTP(enabledRR, enabledReq)
if enabledRR.Code != http.StatusBadRequest {
t.Fatalf("expected legacy-enabled route to reach handler and return %d, got %d", http.StatusBadRequest, enabledRR.Code)
}
}
func testRouter(t *testing.T, cfg *settings.Settings) http.Handler {
t.Helper()
logger := testRouterLogger()
secretKey := []byte("0123456789abcdef0123456789abcdef")
secretSvc := secrets.New(logger, secretKey)
return New(logger, nil, "test-build", "test-sha", "go-test", nil, secretSvc, cfg)
}
func testRouterSettings(t *testing.T, enableLegacyAPI bool) *settings.Settings {
t.Helper()
cfg := &settings.Settings{Values: &settings.SettingsYML{}}
cfg.Values.Settings.AuthEnabled = true
cfg.Values.Settings.AuthMode = "required"
cfg.Values.Settings.AuthJWTSigningKey = base64.StdEncoding.EncodeToString([]byte("router-auth-test-signing-key"))
cfg.Values.Settings.AuthTokenLifespanMinutes = 120
cfg.Values.Settings.AuthJWTIssuer = "vctp"
cfg.Values.Settings.AuthJWTAudience = "vctp-api"
cfg.Values.Settings.AuthClockSkewSeconds = 60
cfg.Values.Settings.EnableLegacyAPI = enableLegacyAPI
cfg.Values.Settings.ReportsDir = t.TempDir()
return cfg
}
func mustIssueToken(t *testing.T, cfg *settings.Settings, subject string, roles []string) string {
t.Helper()
svc, err := auth.NewJWTService(auth.JWTConfig{
SigningKeyBase64: cfg.Values.Settings.AuthJWTSigningKey,
Issuer: cfg.Values.Settings.AuthJWTIssuer,
Audience: cfg.Values.Settings.AuthJWTAudience,
TokenLifespan: time.Duration(cfg.Values.Settings.AuthTokenLifespanMinutes) * time.Minute,
ClockSkew: time.Duration(cfg.Values.Settings.AuthClockSkewSeconds) * time.Second,
})
if err != nil {
t.Fatalf("failed to create jwt service: %v", err)
}
token, _, err := svc.IssueToken(subject, roles, nil)
if err != nil {
t.Fatalf("failed to issue token: %v", err)
}
return token
}
func testRouterLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
+287 -25
View File
@@ -41,9 +41,103 @@ const docTemplate = `{
} }
} }
}, },
"/api/auth/login": {
"post": {
"description": "Authenticates a username/password against LDAP and returns a signed access token.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Login",
"parameters": [
{
"description": "Login credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.AuthLoginRequest"
}
}
],
"responses": {
"200": {
"description": "Login success",
"schema": {
"$ref": "#/definitions/models.AuthLoginResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "Invalid credentials",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Server error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"503": {
"description": "Authentication disabled",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/auth/me": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Returns JWT claims for the currently authenticated bearer token.\nRequires Bearer authentication.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Who am I",
"responses": {
"200": {
"description": "Authenticated identity",
"schema": {
"$ref": "#/definitions/models.AuthMeResponse"
}
},
"401": {
"description": "Missing or invalid authentication context",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cleanup/updates": { "/api/cleanup/updates": {
"delete": { "delete": {
"description": "Deprecated: Removes update records that are no longer associated with a VM.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Removes update records that are no longer associated with a VM.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"text/plain" "text/plain"
], ],
@@ -106,7 +200,12 @@ const docTemplate = `{
}, },
"/api/diagnostics/daily-creation": { "/api/diagnostics/daily-creation": {
"get": { "get": {
"description": "Returns counts of daily summary rows missing CreationTime and sample rows for the given date.", "security": [
{
"BearerAuth": []
}
],
"description": "Returns counts of daily summary rows missing CreationTime and sample rows for the given date.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -153,7 +252,12 @@ const docTemplate = `{
}, },
"/api/encrypt": { "/api/encrypt": {
"post": { "post": {
"description": "Encrypts a plaintext value and returns the ciphertext.", "security": [
{
"BearerAuth": []
}
],
"description": "Encrypts a plaintext value and returns the ciphertext.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -202,7 +306,12 @@ const docTemplate = `{
}, },
"/api/event/vm/create": { "/api/event/vm/create": {
"post": { "post": {
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -249,7 +358,12 @@ const docTemplate = `{
}, },
"/api/event/vm/delete": { "/api/event/vm/delete": {
"post": { "post": {
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -296,7 +410,12 @@ const docTemplate = `{
}, },
"/api/event/vm/modify": { "/api/event/vm/modify": {
"post": { "post": {
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -343,7 +462,12 @@ const docTemplate = `{
}, },
"/api/event/vm/move": { "/api/event/vm/move": {
"post": { "post": {
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -390,7 +514,12 @@ const docTemplate = `{
}, },
"/api/import/vm": { "/api/import/vm": {
"post": { "post": {
"description": "Deprecated: Imports existing VM inventory data in bulk.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Imports existing VM inventory data in bulk.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -431,7 +560,12 @@ const docTemplate = `{
}, },
"/api/inventory/vm/delete": { "/api/inventory/vm/delete": {
"delete": { "delete": {
"description": "Deprecated: Removes a VM inventory entry by VM ID and datacenter name.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Removes a VM inventory entry by VM ID and datacenter name.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -474,7 +608,12 @@ const docTemplate = `{
}, },
"/api/inventory/vm/update": { "/api/inventory/vm/update": {
"post": { "post": {
"description": "Deprecated: Queries vCenter and updates inventory records with missing details.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Queries vCenter and updates inventory records with missing details.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -501,7 +640,12 @@ const docTemplate = `{
}, },
"/api/report/inventory": { "/api/report/inventory": {
"get": { "get": {
"description": "Generates an inventory XLSX report and returns it as a file download.", "security": [
{
"BearerAuth": []
}
],
"description": "Generates an inventory XLSX report and returns it as a file download.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -527,7 +671,12 @@ const docTemplate = `{
}, },
"/api/report/snapshot": { "/api/report/snapshot": {
"get": { "get": {
"description": "Downloads a snapshot table as an XLSX file.", "security": [
{
"BearerAuth": []
}
],
"description": "Downloads a snapshot table as an XLSX file.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -568,7 +717,12 @@ const docTemplate = `{
}, },
"/api/report/updates": { "/api/report/updates": {
"get": { "get": {
"description": "Generates an updates XLSX report and returns it as a file download.", "security": [
{
"BearerAuth": []
}
],
"description": "Generates an updates XLSX report and returns it as a file download.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -594,7 +748,12 @@ const docTemplate = `{
}, },
"/api/snapshots/aggregate": { "/api/snapshots/aggregate": {
"post": { "post": {
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.", "security": [
{
"BearerAuth": []
}
],
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -648,7 +807,12 @@ const docTemplate = `{
}, },
"/api/snapshots/hourly/force": { "/api/snapshots/hourly/force": {
"post": { "post": {
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.", "security": [
{
"BearerAuth": []
}
],
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -692,7 +856,12 @@ const docTemplate = `{
}, },
"/api/snapshots/migrate": { "/api/snapshots/migrate": {
"post": { "post": {
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -718,7 +887,12 @@ const docTemplate = `{
}, },
"/api/snapshots/regenerate-hourly-reports": { "/api/snapshots/regenerate-hourly-reports": {
"post": { "post": {
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.", "security": [
{
"BearerAuth": []
}
],
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -744,7 +918,12 @@ const docTemplate = `{
}, },
"/api/snapshots/repair": { "/api/snapshots/repair": {
"post": { "post": {
"description": "Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.", "security": [
{
"BearerAuth": []
}
],
"description": "Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -764,7 +943,12 @@ const docTemplate = `{
}, },
"/api/snapshots/repair/all": { "/api/snapshots/repair/all": {
"post": { "post": {
"description": "Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -784,7 +968,12 @@ const docTemplate = `{
}, },
"/api/vcenters/cache/rebuild": { "/api/vcenters/cache/rebuild": {
"post": { "post": {
"description": "Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -1120,6 +1309,72 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"models.AuthLoginRequest": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"models.AuthLoginResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"token_type": {
"type": "string"
}
}
},
"models.AuthMeResponse": {
"type": "object",
"properties": {
"audience": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
},
"issued_at": {
"type": "integer"
},
"issuer": {
"type": "string"
},
"not_before": {
"type": "integer"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string"
},
"subject": {
"type": "string"
},
"token_id": {
"type": "string"
}
}
},
"models.CloudEventReceived": { "models.CloudEventReceived": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1613,17 +1868,24 @@ const docTemplate = `{
} }
} }
} }
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
} }
}` }`
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = &swag.Spec{
Version: "", Version: "1.0",
Host: "", Host: "",
BasePath: "", BasePath: "/",
Schemes: []string{}, Schemes: []string{"http", "https"},
Title: "", Title: "vCTP API",
Description: "", Description: "vCTP API endpoints for inventory snapshots, reporting, and administration.",
InfoInstanceName: "swagger", InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate, SwaggerTemplate: docTemplate,
LeftDelim: "{{", LeftDelim: "{{",
+291 -21
View File
@@ -1,8 +1,16 @@
{ {
"schemes": [
"http",
"https"
],
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"contact": {} "description": "vCTP API endpoints for inventory snapshots, reporting, and administration.",
"title": "vCTP API",
"contact": {},
"version": "1.0"
}, },
"basePath": "/",
"paths": { "paths": {
"/": { "/": {
"get": { "get": {
@@ -30,9 +38,103 @@
} }
} }
}, },
"/api/auth/login": {
"post": {
"description": "Authenticates a username/password against LDAP and returns a signed access token.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Login",
"parameters": [
{
"description": "Login credentials",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.AuthLoginRequest"
}
}
],
"responses": {
"200": {
"description": "Login success",
"schema": {
"$ref": "#/definitions/models.AuthLoginResponse"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "Invalid credentials",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Server error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"503": {
"description": "Authentication disabled",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/auth/me": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Returns JWT claims for the currently authenticated bearer token.\nRequires Bearer authentication.",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Who am I",
"responses": {
"200": {
"description": "Authenticated identity",
"schema": {
"$ref": "#/definitions/models.AuthMeResponse"
}
},
"401": {
"description": "Missing or invalid authentication context",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/cleanup/updates": { "/api/cleanup/updates": {
"delete": { "delete": {
"description": "Deprecated: Removes update records that are no longer associated with a VM.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Removes update records that are no longer associated with a VM.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"text/plain" "text/plain"
], ],
@@ -95,7 +197,12 @@
}, },
"/api/diagnostics/daily-creation": { "/api/diagnostics/daily-creation": {
"get": { "get": {
"description": "Returns counts of daily summary rows missing CreationTime and sample rows for the given date.", "security": [
{
"BearerAuth": []
}
],
"description": "Returns counts of daily summary rows missing CreationTime and sample rows for the given date.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -142,7 +249,12 @@
}, },
"/api/encrypt": { "/api/encrypt": {
"post": { "post": {
"description": "Encrypts a plaintext value and returns the ciphertext.", "security": [
{
"BearerAuth": []
}
],
"description": "Encrypts a plaintext value and returns the ciphertext.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -191,7 +303,12 @@
}, },
"/api/event/vm/create": { "/api/event/vm/create": {
"post": { "post": {
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -238,7 +355,12 @@
}, },
"/api/event/vm/delete": { "/api/event/vm/delete": {
"post": { "post": {
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -285,7 +407,12 @@
}, },
"/api/event/vm/modify": { "/api/event/vm/modify": {
"post": { "post": {
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -332,7 +459,12 @@
}, },
"/api/event/vm/move": { "/api/event/vm/move": {
"post": { "post": {
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -379,7 +511,12 @@
}, },
"/api/import/vm": { "/api/import/vm": {
"post": { "post": {
"description": "Deprecated: Imports existing VM inventory data in bulk.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Imports existing VM inventory data in bulk.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -420,7 +557,12 @@
}, },
"/api/inventory/vm/delete": { "/api/inventory/vm/delete": {
"delete": { "delete": {
"description": "Deprecated: Removes a VM inventory entry by VM ID and datacenter name.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Removes a VM inventory entry by VM ID and datacenter name.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -463,7 +605,12 @@
}, },
"/api/inventory/vm/update": { "/api/inventory/vm/update": {
"post": { "post": {
"description": "Deprecated: Queries vCenter and updates inventory records with missing details.", "security": [
{
"BearerAuth": []
}
],
"description": "Deprecated: Queries vCenter and updates inventory records with missing details.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -490,7 +637,12 @@
}, },
"/api/report/inventory": { "/api/report/inventory": {
"get": { "get": {
"description": "Generates an inventory XLSX report and returns it as a file download.", "security": [
{
"BearerAuth": []
}
],
"description": "Generates an inventory XLSX report and returns it as a file download.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -516,7 +668,12 @@
}, },
"/api/report/snapshot": { "/api/report/snapshot": {
"get": { "get": {
"description": "Downloads a snapshot table as an XLSX file.", "security": [
{
"BearerAuth": []
}
],
"description": "Downloads a snapshot table as an XLSX file.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -557,7 +714,12 @@
}, },
"/api/report/updates": { "/api/report/updates": {
"get": { "get": {
"description": "Generates an updates XLSX report and returns it as a file download.", "security": [
{
"BearerAuth": []
}
],
"description": "Generates an updates XLSX report and returns it as a file download.\nRequires Bearer authentication with the viewer role (admin also allowed).",
"produces": [ "produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
], ],
@@ -583,7 +745,12 @@
}, },
"/api/snapshots/aggregate": { "/api/snapshots/aggregate": {
"post": { "post": {
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.", "security": [
{
"BearerAuth": []
}
],
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -637,7 +804,12 @@
}, },
"/api/snapshots/hourly/force": { "/api/snapshots/hourly/force": {
"post": { "post": {
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.", "security": [
{
"BearerAuth": []
}
],
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.\nRequires Bearer authentication with the admin role.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -681,7 +853,12 @@
}, },
"/api/snapshots/migrate": { "/api/snapshots/migrate": {
"post": { "post": {
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -707,7 +884,12 @@
}, },
"/api/snapshots/regenerate-hourly-reports": { "/api/snapshots/regenerate-hourly-reports": {
"post": { "post": {
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.", "security": [
{
"BearerAuth": []
}
],
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -733,7 +915,12 @@
}, },
"/api/snapshots/repair": { "/api/snapshots/repair": {
"post": { "post": {
"description": "Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.", "security": [
{
"BearerAuth": []
}
],
"description": "Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -753,7 +940,12 @@
}, },
"/api/snapshots/repair/all": { "/api/snapshots/repair/all": {
"post": { "post": {
"description": "Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -773,7 +965,12 @@
}, },
"/api/vcenters/cache/rebuild": { "/api/vcenters/cache/rebuild": {
"post": { "post": {
"description": "Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.", "security": [
{
"BearerAuth": []
}
],
"description": "Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.\nRequires Bearer authentication with the admin role.",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -1109,6 +1306,72 @@
} }
}, },
"definitions": { "definitions": {
"models.AuthLoginRequest": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"models.AuthLoginResponse": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"token_type": {
"type": "string"
}
}
},
"models.AuthMeResponse": {
"type": "object",
"properties": {
"audience": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"groups": {
"type": "array",
"items": {
"type": "string"
}
},
"issued_at": {
"type": "integer"
},
"issuer": {
"type": "string"
},
"not_before": {
"type": "integer"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string"
},
"subject": {
"type": "string"
},
"token_id": {
"type": "string"
}
}
},
"models.CloudEventReceived": { "models.CloudEventReceived": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1602,5 +1865,12 @@
} }
} }
} }
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
} }
} }
+215 -35
View File
@@ -1,4 +1,48 @@
basePath: /
definitions: definitions:
models.AuthLoginRequest:
properties:
password:
type: string
username:
type: string
type: object
models.AuthLoginResponse:
properties:
access_token:
type: string
expires_at:
type: integer
token_type:
type: string
type: object
models.AuthMeResponse:
properties:
audience:
type: string
expires_at:
type: integer
groups:
items:
type: string
type: array
issued_at:
type: integer
issuer:
type: string
not_before:
type: integer
roles:
items:
type: string
type: array
status:
type: string
subject:
type: string
token_id:
type: string
type: object
models.CloudEventReceived: models.CloudEventReceived:
properties: properties:
cloudEvent: cloudEvent:
@@ -320,6 +364,9 @@ definitions:
type: object type: object
info: info:
contact: {} contact: {}
description: vCTP API endpoints for inventory snapshots, reporting, and administration.
title: vCTP API
version: "1.0"
paths: paths:
/: /:
get: get:
@@ -338,11 +385,72 @@ paths:
summary: Home page summary: Home page
tags: tags:
- ui - ui
/api/auth/login:
post:
consumes:
- application/json
description: Authenticates a username/password against LDAP and returns a signed
access token.
parameters:
- description: Login credentials
in: body
name: payload
required: true
schema:
$ref: '#/definitions/models.AuthLoginRequest'
produces:
- application/json
responses:
"200":
description: Login success
schema:
$ref: '#/definitions/models.AuthLoginResponse'
"400":
description: Invalid request
schema:
$ref: '#/definitions/models.ErrorResponse'
"401":
description: Invalid credentials
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Server error
schema:
$ref: '#/definitions/models.ErrorResponse'
"503":
description: Authentication disabled
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Login
tags:
- auth
/api/auth/me:
get:
description: |-
Returns JWT claims for the currently authenticated bearer token.
Requires Bearer authentication.
produces:
- application/json
responses:
"200":
description: Authenticated identity
schema:
$ref: '#/definitions/models.AuthMeResponse'
"401":
description: Missing or invalid authentication context
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Who am I
tags:
- auth
/api/cleanup/updates: /api/cleanup/updates:
delete: delete:
deprecated: true deprecated: true
description: 'Deprecated: Removes update records that are no longer associated description: |-
with a VM.' Deprecated: Removes update records that are no longer associated with a VM.
Requires Bearer authentication with the admin role.
produces: produces:
- text/plain - text/plain
responses: responses:
@@ -354,6 +462,8 @@ paths:
description: Server error description: Server error
schema: schema:
type: string type: string
security:
- BearerAuth: []
summary: Cleanup updates (deprecated) summary: Cleanup updates (deprecated)
tags: tags:
- maintenance - maintenance
@@ -384,8 +494,9 @@ paths:
- maintenance - maintenance
/api/diagnostics/daily-creation: /api/diagnostics/daily-creation:
get: get:
description: Returns counts of daily summary rows missing CreationTime and sample description: |-
rows for the given date. Returns counts of daily summary rows missing CreationTime and sample rows for the given date.
Requires Bearer authentication with the viewer role (admin also allowed).
parameters: parameters:
- description: Daily date (YYYY-MM-DD) - description: Daily date (YYYY-MM-DD)
in: query in: query
@@ -411,6 +522,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Daily summary CreationTime diagnostics summary: Daily summary CreationTime diagnostics
tags: tags:
- diagnostics - diagnostics
@@ -418,7 +531,9 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Encrypts a plaintext value and returns the ciphertext. description: |-
Encrypts a plaintext value and returns the ciphertext.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: Plaintext payload - description: Plaintext payload
in: body in: body
@@ -443,6 +558,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Encrypt data summary: Encrypt data
tags: tags:
- crypto - crypto
@@ -451,8 +568,9 @@ paths:
consumes: consumes:
- application/json - application/json
deprecated: true deprecated: true
description: 'Deprecated: Parses a VM create CloudEvent and stores the event description: |-
data.' Deprecated: Parses a VM create CloudEvent and stores the event data.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: CloudEvent payload - description: CloudEvent payload
in: body in: body
@@ -475,6 +593,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Record VM create event (deprecated) summary: Record VM create event (deprecated)
tags: tags:
- events - events
@@ -483,8 +603,9 @@ paths:
consumes: consumes:
- application/json - application/json
deprecated: true deprecated: true
description: 'Deprecated: Parses a VM delete CloudEvent and marks the VM as description: |-
deleted in inventory.' Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: CloudEvent payload - description: CloudEvent payload
in: body in: body
@@ -507,6 +628,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Record VM delete event (deprecated) summary: Record VM delete event (deprecated)
tags: tags:
- events - events
@@ -515,8 +638,9 @@ paths:
consumes: consumes:
- application/json - application/json
deprecated: true deprecated: true
description: 'Deprecated: Parses a VM modify CloudEvent and creates an update description: |-
record when relevant changes are detected.' Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: CloudEvent payload - description: CloudEvent payload
in: body in: body
@@ -539,6 +663,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Record VM modify event (deprecated) summary: Record VM modify event (deprecated)
tags: tags:
- events - events
@@ -547,8 +673,9 @@ paths:
consumes: consumes:
- application/json - application/json
deprecated: true deprecated: true
description: 'Deprecated: Parses a VM move CloudEvent and creates an update description: |-
record.' Deprecated: Parses a VM move CloudEvent and creates an update record.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: CloudEvent payload - description: CloudEvent payload
in: body in: body
@@ -571,6 +698,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Record VM move event (deprecated) summary: Record VM move event (deprecated)
tags: tags:
- events - events
@@ -579,7 +708,9 @@ paths:
consumes: consumes:
- application/json - application/json
deprecated: true deprecated: true
description: 'Deprecated: Imports existing VM inventory data in bulk.' description: |-
Deprecated: Imports existing VM inventory data in bulk.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: Bulk import payload - description: Bulk import payload
in: body in: body
@@ -598,14 +729,17 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Import VMs (deprecated) summary: Import VMs (deprecated)
tags: tags:
- inventory - inventory
/api/inventory/vm/delete: /api/inventory/vm/delete:
delete: delete:
deprecated: true deprecated: true
description: 'Deprecated: Removes a VM inventory entry by VM ID and datacenter description: |-
name.' Deprecated: Removes a VM inventory entry by VM ID and datacenter name.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: VM ID - description: VM ID
in: query in: query
@@ -628,14 +762,17 @@ paths:
description: Invalid request description: Invalid request
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Cleanup VM inventory entry (deprecated) summary: Cleanup VM inventory entry (deprecated)
tags: tags:
- inventory - inventory
/api/inventory/vm/update: /api/inventory/vm/update:
post: post:
deprecated: true deprecated: true
description: 'Deprecated: Queries vCenter and updates inventory records with description: |-
missing details.' Deprecated: Queries vCenter and updates inventory records with missing details.
Requires Bearer authentication with the admin role.
produces: produces:
- application/json - application/json
responses: responses:
@@ -647,12 +784,16 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Refresh VM details (deprecated) summary: Refresh VM details (deprecated)
tags: tags:
- inventory - inventory
/api/report/inventory: /api/report/inventory:
get: get:
description: Generates an inventory XLSX report and returns it as a file download. description: |-
Generates an inventory XLSX report and returns it as a file download.
Requires Bearer authentication with the viewer role (admin also allowed).
produces: produces:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
responses: responses:
@@ -664,12 +805,16 @@ paths:
description: Report generation failed description: Report generation failed
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Download inventory report summary: Download inventory report
tags: tags:
- reports - reports
/api/report/snapshot: /api/report/snapshot:
get: get:
description: Downloads a snapshot table as an XLSX file. description: |-
Downloads a snapshot table as an XLSX file.
Requires Bearer authentication with the viewer role (admin also allowed).
parameters: parameters:
- description: Snapshot table name - description: Snapshot table name
in: query in: query
@@ -691,12 +836,16 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Download snapshot report summary: Download snapshot report
tags: tags:
- snapshots - snapshots
/api/report/updates: /api/report/updates:
get: get:
description: Generates an updates XLSX report and returns it as a file download. description: |-
Generates an updates XLSX report and returns it as a file download.
Requires Bearer authentication with the viewer role (admin also allowed).
produces: produces:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
responses: responses:
@@ -708,13 +857,16 @@ paths:
description: Report generation failed description: Report generation failed
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Download updates report summary: Download updates report
tags: tags:
- reports - reports
/api/snapshots/aggregate: /api/snapshots/aggregate:
post: post:
description: Forces regeneration of a daily or monthly summary table for a specified description: |-
date or month. Forces regeneration of a daily or monthly summary table for a specified date or month.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: 'Aggregation type: daily or monthly' - description: 'Aggregation type: daily or monthly'
in: query in: query
@@ -745,6 +897,8 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Force snapshot aggregation summary: Force snapshot aggregation
tags: tags:
- snapshots - snapshots
@@ -752,8 +906,9 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Manually trigger an hourly snapshot for all configured vCenters. description: |-
Requires confirmation text to avoid accidental execution. Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: Confirmation text; must be 'FORCE' - description: Confirmation text; must be 'FORCE'
in: query in: query
@@ -775,13 +930,16 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Trigger hourly snapshot (manual) summary: Trigger hourly snapshot (manual)
tags: tags:
- snapshots - snapshots
/api/snapshots/migrate: /api/snapshots/migrate:
post: post:
description: Rebuilds the snapshot registry from existing tables and renames description: |-
hourly tables to epoch-based names. Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.
Requires Bearer authentication with the admin role.
produces: produces:
- application/json - application/json
responses: responses:
@@ -793,13 +951,16 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.SnapshotMigrationResponse' $ref: '#/definitions/models.SnapshotMigrationResponse'
security:
- BearerAuth: []
summary: Migrate snapshot registry summary: Migrate snapshot registry
tags: tags:
- snapshots - snapshots
/api/snapshots/regenerate-hourly-reports: /api/snapshots/regenerate-hourly-reports:
post: post:
description: Regenerates XLSX reports for hourly snapshots when the report files description: |-
are missing or empty. Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.
Requires Bearer authentication with the admin role.
produces: produces:
- application/json - application/json
responses: responses:
@@ -811,13 +972,16 @@ paths:
description: Server error description: Server error
schema: schema:
$ref: '#/definitions/models.ErrorResponse' $ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: Regenerate hourly snapshot reports summary: Regenerate hourly snapshot reports
tags: tags:
- snapshots - snapshots
/api/snapshots/repair: /api/snapshots/repair:
post: post:
description: Backfills SnapshotTime and lifecycle info for existing daily summary description: |-
tables and reruns monthly lifecycle refinement using hourly data. Backfills SnapshotTime and lifecycle info for existing daily summary tables and reruns monthly lifecycle refinement using hourly data.
Requires Bearer authentication with the admin role.
produces: produces:
- application/json - application/json
responses: responses:
@@ -825,13 +989,16 @@ paths:
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SnapshotRepairResponse' $ref: '#/definitions/models.SnapshotRepairResponse'
security:
- BearerAuth: []
summary: Repair daily summaries summary: Repair daily summaries
tags: tags:
- snapshots - snapshots
/api/snapshots/repair/all: /api/snapshots/repair/all:
post: post:
description: Rebuilds snapshot registry, backfills per-vCenter totals, repairs description: |-
daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle. Rebuilds snapshot registry, backfills per-vCenter totals, repairs daily summaries (SnapshotTime/lifecycle), and refines monthly lifecycle.
Requires Bearer authentication with the admin role.
produces: produces:
- application/json - application/json
responses: responses:
@@ -839,13 +1006,16 @@ paths:
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SnapshotRepairSuiteResponse' $ref: '#/definitions/models.SnapshotRepairSuiteResponse'
security:
- BearerAuth: []
summary: Run full snapshot repair suite summary: Run full snapshot repair suite
tags: tags:
- snapshots - snapshots
/api/vcenters/cache/rebuild: /api/vcenters/cache/rebuild:
post: post:
description: Rebuilds cached folder/resource-pool/host(cluster+datacenter) references description: |-
from vCenter and rewrites the database cache tables. Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.
Requires Bearer authentication with the admin role.
parameters: parameters:
- description: Optional single vCenter URL to rebuild; defaults to all configured - description: Optional single vCenter URL to rebuild; defaults to all configured
vCenters vCenters
@@ -871,6 +1041,8 @@ paths:
description: All rebuild attempts failed description: All rebuild attempts failed
schema: schema:
$ref: '#/definitions/models.VcenterCacheRebuildResponse' $ref: '#/definitions/models.VcenterCacheRebuildResponse'
security:
- BearerAuth: []
summary: Rebuild vCenter object cache summary: Rebuild vCenter object cache
tags: tags:
- vcenters - vcenters
@@ -1069,4 +1241,12 @@ paths:
summary: Trace VM history summary: Trace VM history
tags: tags:
- vm - vm
schemes:
- http
- https
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0" swagger: "2.0"
+4
View File
@@ -30,6 +30,9 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux := http.NewServeMux() mux := http.NewServeMux()
requireAuth := middleware.RequireAuth(logger, settings) requireAuth := middleware.RequireAuth(logger, settings)
withAuth := func(next http.HandlerFunc) http.Handler {
return requireAuth(http.HandlerFunc(next))
}
withAuthRole := func(next http.HandlerFunc, roles ...string) http.Handler { withAuthRole := func(next http.HandlerFunc, roles ...string) http.Handler {
wrapped := http.Handler(http.HandlerFunc(next)) wrapped := http.Handler(http.HandlerFunc(next))
if len(roles) > 0 { if len(roles) > 0 {
@@ -78,6 +81,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux.Handle("/api/snapshots/regenerate-hourly-reports", withAuthRole(h.SnapshotRegenerateHourlyReports, middleware.RoleAdmin)) mux.Handle("/api/snapshots/regenerate-hourly-reports", withAuthRole(h.SnapshotRegenerateHourlyReports, middleware.RoleAdmin))
mux.Handle("/api/diagnostics/daily-creation", withAuthRole(h.DailyCreationDiagnostics, middleware.RoleViewer)) mux.Handle("/api/diagnostics/daily-creation", withAuthRole(h.DailyCreationDiagnostics, middleware.RoleViewer))
mux.HandleFunc("/api/auth/login", h.AuthLogin) mux.HandleFunc("/api/auth/login", h.AuthLogin)
mux.Handle("/api/auth/me", withAuth(h.AuthMe))
mux.HandleFunc("/vm/trace", h.VmTrace) mux.HandleFunc("/vm/trace", h.VmTrace)
mux.HandleFunc("/vcenters", h.VcenterList) mux.HandleFunc("/vcenters", h.VcenterList)
mux.HandleFunc("/vcenters/totals", h.VcenterTotals) mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
+1 -1
View File
@@ -1,7 +1,7 @@
name: "vctp" name: "vctp"
arch: "amd64" arch: "amd64"
platform: "linux" platform: "linux"
version: "v26.1.3" version: "v26.4.1"
version_schema: semver version_schema: semver
description: vCTP monitors VMware VM inventory and event data to build chargeback reports description: vCTP monitors VMware VM inventory and event data to build chargeback reports
maintainer: "@coadn" maintainer: "@coadn"