diff --git a/.drone.yml b/.drone.yml index fa6cdcb..5551f1f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -35,14 +35,14 @@ steps: commands: - export PATH=/drone/src/pkg.tools:$PATH - 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/goreleaser/nfpm/v2/cmd/nfpm@latest - sqlc generate - templ generate -path ./components - swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs - chmod +x ./scripts/*.sh - - ./scripts/update-swagger-ui.sh + - ./scripts/update-swagger-ui.sh v5.32.4 - ./scripts/drone.sh - cp ./build/vctp-linux-amd64 /shared/ diff --git a/README.md b/README.md index 098f0e8..58da63e 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,11 @@ Login flow: ```http Authorization: Bearer ``` +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: - `settings.auth_mode: disabled`: middleware bypassed. diff --git a/main.go b/main.go index 3a59068..674e201 100644 --- a/main.go +++ b/main.go @@ -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 import ( diff --git a/server/audit/auth.go b/server/audit/auth.go new file mode 100644 index 0000000..8a1f2ae --- /dev/null +++ b/server/audit/auth.go @@ -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...) +} diff --git a/server/handler/auth.go b/server/handler/auth.go index 3da4328..9545b4f 100644 --- a/server/handler/auth.go +++ b/server/handler/auth.go @@ -7,6 +7,8 @@ import ( "strings" "time" "vctp/internal/auth" + "vctp/server/audit" + "vctp/server/middleware" "vctp/server/models" ) @@ -50,12 +52,14 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { return } 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") return } cfg := h.Settings.Values.Settings if !cfg.AuthEnabled { + audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "auth_disabled") writeJSONError(w, http.StatusServiceUnavailable, "authentication is disabled") return } @@ -63,12 +67,14 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { var req models.AuthLoginRequest if err := decodeJSONBody(w, r, &req); err != nil { 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") return } username := strings.TrimSpace(req.Username) password := req.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") return } @@ -83,6 +89,7 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { }) if err != nil { 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") return } @@ -92,23 +99,23 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { identity, err := ldapAuth.AuthenticateAndFetchGroups(ctx, username, password) if err != nil { 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) return } 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) 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) return } roles := auth.ResolveRoles(identity.Groups, cfg.AuthGroupRoleMappings) 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) return } @@ -122,6 +129,7 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { }) if err != nil { 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") return } @@ -133,14 +141,53 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) { token, claims, err := jwtSvc.IssueToken(subject, roles, identity.Groups) if err != nil { 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") 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{ AccessToken: token, ExpiresAt: claims.ExpiresAt, 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, + }) +} diff --git a/server/handler/auth_test.go b/server/handler/auth_test.go index eb6c903..981fa19 100644 --- a/server/handler/auth_test.go +++ b/server/handler/auth_test.go @@ -12,6 +12,7 @@ import ( "time" "vctp/internal/auth" "vctp/internal/settings" + "vctp/server/middleware" "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 { cfg := &settings.Settings{Values: &settings.SettingsYML{}} cfg.Values.Settings.AuthEnabled = true diff --git a/server/handler/dailyCreationDiagnostics.go b/server/handler/dailyCreationDiagnostics.go index 5047f84..e4955d3 100644 --- a/server/handler/dailyCreationDiagnostics.go +++ b/server/handler/dailyCreationDiagnostics.go @@ -13,6 +13,7 @@ import ( // DailyCreationDiagnostics returns missing CreationTime diagnostics for a daily summary table. // @Summary Daily summary CreationTime diagnostics // @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 // @Produce json // @Param date query string true "Daily date (YYYY-MM-DD)" @@ -20,6 +21,7 @@ import ( // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 404 {object} models.ErrorResponse "Summary not found" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/diagnostics/daily-creation [get] func (h *Handler) DailyCreationDiagnostics(w http.ResponseWriter, r *http.Request) { dateValue := strings.TrimSpace(r.URL.Query().Get("date")) diff --git a/server/handler/encryptData.go b/server/handler/encryptData.go index 3368f74..2bc4761 100644 --- a/server/handler/encryptData.go +++ b/server/handler/encryptData.go @@ -17,6 +17,7 @@ type encryptRequest struct { // EncryptData encrypts a plaintext value and returns the ciphertext. // @Summary Encrypt data // @Description Encrypts a plaintext value and returns the ciphertext. +// @Description Requires Bearer authentication with the admin role. // @Tags crypto // @Accept json // @Produce json @@ -24,6 +25,7 @@ type encryptRequest struct { // @Success 200 {object} models.StatusMessageResponse "Ciphertext response" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/encrypt [post] func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/reportDownload.go b/server/handler/reportDownload.go index 1044c91..07be254 100644 --- a/server/handler/reportDownload.go +++ b/server/handler/reportDownload.go @@ -9,10 +9,12 @@ import ( // InventoryReportDownload returns the inventory report as an XLSX download. // @Summary Download inventory report // @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 // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Success 200 {file} file "Inventory XLSX report" // @Failure 500 {object} models.ErrorResponse "Report generation failed" +// @Security BearerAuth // @Router /api/report/inventory [get] func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) { 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. // @Summary Download updates report // @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 // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Success 200 {file} file "Updates XLSX report" // @Failure 500 {object} models.ErrorResponse "Report generation failed" +// @Security BearerAuth // @Router /api/report/updates [get] func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) { ctx, cancel := withRequestTimeout(r, reportRequestTimeout) diff --git a/server/handler/snapshotAggregate.go b/server/handler/snapshotAggregate.go index ce19515..6c24877 100644 --- a/server/handler/snapshotAggregate.go +++ b/server/handler/snapshotAggregate.go @@ -11,6 +11,7 @@ import ( // SnapshotAggregateForce forces regeneration of a daily or monthly summary table. // @Summary Force snapshot aggregation // @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 // @Produce json // @Param type query string true "Aggregation type: daily or monthly" @@ -19,6 +20,7 @@ import ( // @Success 200 {object} models.StatusResponse "Aggregation complete" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/snapshots/aggregate [post] func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/snapshotForceHourly.go b/server/handler/snapshotForceHourly.go index 64045a5..fdcf1f9 100644 --- a/server/handler/snapshotForceHourly.go +++ b/server/handler/snapshotForceHourly.go @@ -10,6 +10,7 @@ import ( // SnapshotForceHourly triggers an on-demand hourly snapshot run. // @Summary Trigger hourly snapshot (manual) // @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 // @Accept json // @Produce json @@ -17,6 +18,7 @@ import ( // @Success 200 {object} models.StatusResponse "Snapshot started" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/snapshots/hourly/force [post] func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/snapshotMigrate.go b/server/handler/snapshotMigrate.go index df840f5..6f7ace8 100644 --- a/server/handler/snapshotMigrate.go +++ b/server/handler/snapshotMigrate.go @@ -9,10 +9,12 @@ import ( // SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names. // @Summary Migrate snapshot registry // @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 // @Produce json // @Success 200 {object} models.SnapshotMigrationResponse "Migration results" // @Failure 500 {object} models.SnapshotMigrationResponse "Server error" +// @Security BearerAuth // @Router /api/snapshots/migrate [post] func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/snapshotRegenerateHourly.go b/server/handler/snapshotRegenerateHourly.go index 2b7886b..bae7c0d 100644 --- a/server/handler/snapshotRegenerateHourly.go +++ b/server/handler/snapshotRegenerateHourly.go @@ -13,10 +13,12 @@ import ( // SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk. // @Summary Regenerate hourly snapshot reports // @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 // @Produce json // @Success 200 {object} models.SnapshotRegenerateReportsResponse "Regeneration summary" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/snapshots/regenerate-hourly-reports [post] func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/snapshotRepair.go b/server/handler/snapshotRepair.go index 3a863eb..b0c76e8 100644 --- a/server/handler/snapshotRepair.go +++ b/server/handler/snapshotRepair.go @@ -15,9 +15,11 @@ import ( // SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields. // @Summary Repair daily summaries // @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 // @Produce json // @Success 200 {object} models.SnapshotRepairResponse +// @Security BearerAuth // @Router /api/snapshots/repair [post] func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) { 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. // @Summary Run full snapshot repair suite // @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 // @Produce json // @Success 200 {object} models.SnapshotRepairSuiteResponse +// @Security BearerAuth // @Router /api/snapshots/repair/all [post] func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/snapshots.go b/server/handler/snapshots.go index 1afb78f..0271eaa 100644 --- a/server/handler/snapshots.go +++ b/server/handler/snapshots.go @@ -49,12 +49,14 @@ func (h *Handler) SnapshotMonthlyList(w http.ResponseWriter, r *http.Request) { // SnapshotReportDownload streams a snapshot table as XLSX. // @Summary Download snapshot report // @Description Downloads a snapshot table as an XLSX file. +// @Description Requires Bearer authentication with the viewer role (admin also allowed). // @Tags snapshots // @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // @Param table query string true "Snapshot table name" // @Success 200 {file} file "Snapshot XLSX report" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/report/snapshot [get] func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request) { ctx, cancel := withRequestTimeout(r, reportRequestTimeout) diff --git a/server/handler/updateCleanup.go b/server/handler/updateCleanup.go index 8a9bf2d..45739e4 100644 --- a/server/handler/updateCleanup.go +++ b/server/handler/updateCleanup.go @@ -8,11 +8,13 @@ import ( // UpdateCleanup removes orphaned update records. // @Summary Cleanup updates (deprecated) // @Description Deprecated: Removes update records that are no longer associated with a VM. +// @Description Requires Bearer authentication with the admin role. // @Tags maintenance // @Deprecated // @Produce text/plain // @Success 200 {string} string "Cleanup completed" // @Failure 500 {string} string "Server error" +// @Security BearerAuth // @Router /api/cleanup/updates [delete] func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { diff --git a/server/handler/vcenterCacheRebuild.go b/server/handler/vcenterCacheRebuild.go index 8f66914..2e777e2 100644 --- a/server/handler/vcenterCacheRebuild.go +++ b/server/handler/vcenterCacheRebuild.go @@ -15,6 +15,7 @@ import ( // VcenterCacheRebuild force-regenerates cached vCenter reference data in the database. // @Summary Rebuild vCenter object cache // @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 // @Produce json // @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 405 {object} models.ErrorResponse "Method not allowed" // @Failure 500 {object} models.VcenterCacheRebuildResponse "All rebuild attempts failed" +// @Security BearerAuth // @Router /api/vcenters/cache/rebuild [post] func (h *Handler) VcenterCacheRebuild(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmCleanup.go b/server/handler/vmCleanup.go index f1fe887..ce2fa1b 100644 --- a/server/handler/vmCleanup.go +++ b/server/handler/vmCleanup.go @@ -11,6 +11,7 @@ import ( // VmCleanup removes a VM from inventory by ID and datacenter. // @Summary Cleanup VM inventory entry (deprecated) // @Description Deprecated: Removes a VM inventory entry by VM ID and datacenter name. +// @Description Requires Bearer authentication with the admin role. // @Tags inventory // @Deprecated // @Produce json @@ -18,6 +19,7 @@ import ( // @Param datacenter_name query string true "Datacenter name" // @Success 200 {object} models.StatusMessageResponse "Cleanup completed" // @Failure 400 {object} models.ErrorResponse "Invalid request" +// @Security BearerAuth // @Router /api/inventory/vm/delete [delete] func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { diff --git a/server/handler/vmCreateEvent.go b/server/handler/vmCreateEvent.go index 7652d8c..d674d7f 100644 --- a/server/handler/vmCreateEvent.go +++ b/server/handler/vmCreateEvent.go @@ -15,6 +15,7 @@ import ( // VmCreateEvent records a VM creation CloudEvent. // @Summary Record VM create event (deprecated) // @Description Deprecated: Parses a VM create CloudEvent and stores the event data. +// @Description Requires Bearer authentication with the admin role. // @Tags events // @Deprecated // @Accept json @@ -23,6 +24,7 @@ import ( // @Success 200 {object} models.StatusMessageResponse "Create event processed" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/event/vm/create [post] func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmDeleteEvent.go b/server/handler/vmDeleteEvent.go index 6e77cde..a40aaa8 100644 --- a/server/handler/vmDeleteEvent.go +++ b/server/handler/vmDeleteEvent.go @@ -12,6 +12,7 @@ import ( // VmDeleteEvent records a VM deletion CloudEvent in the inventory. // @Summary Record VM delete event (deprecated) // @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 // @Deprecated // @Accept json @@ -20,6 +21,7 @@ import ( // @Success 200 {object} models.StatusMessageResponse "Delete event processed" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/event/vm/delete [post] func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmImport.go b/server/handler/vmImport.go index 3e00d80..4fbce3d 100644 --- a/server/handler/vmImport.go +++ b/server/handler/vmImport.go @@ -14,6 +14,7 @@ import ( // VmImport ingests a bulk VM import payload. // @Summary Import VMs (deprecated) // @Description Deprecated: Imports existing VM inventory data in bulk. +// @Description Requires Bearer authentication with the admin role. // @Tags inventory // @Deprecated // @Accept json @@ -21,6 +22,7 @@ import ( // @Param import body models.ImportReceived true "Bulk import payload" // @Success 200 {object} models.StatusMessageResponse "Import processed" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/import/vm [post] func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmModifyEvent.go b/server/handler/vmModifyEvent.go index c1992c3..45fb3af 100644 --- a/server/handler/vmModifyEvent.go +++ b/server/handler/vmModifyEvent.go @@ -21,6 +21,7 @@ import ( // VmModifyEvent records a VM modification CloudEvent. // @Summary Record VM modify event (deprecated) // @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 // @Deprecated // @Accept json @@ -29,6 +30,7 @@ import ( // @Success 200 {object} models.StatusMessageResponse "Modify event processed" // @Success 202 {object} models.StatusMessageResponse "No relevant changes" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/event/vm/modify [post] func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmMoveEvent.go b/server/handler/vmMoveEvent.go index c68c95f..b088277 100644 --- a/server/handler/vmMoveEvent.go +++ b/server/handler/vmMoveEvent.go @@ -14,6 +14,7 @@ import ( // VmMoveEvent records a VM move CloudEvent as an update. // @Summary Record VM move event (deprecated) // @Description Deprecated: Parses a VM move CloudEvent and creates an update record. +// @Description Requires Bearer authentication with the admin role. // @Tags events // @Deprecated // @Accept json @@ -22,6 +23,7 @@ import ( // @Success 200 {object} models.StatusMessageResponse "Move event processed" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/event/vm/move [post] func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/handler/vmUpdateDetails.go b/server/handler/vmUpdateDetails.go index 852983d..afcbe32 100644 --- a/server/handler/vmUpdateDetails.go +++ b/server/handler/vmUpdateDetails.go @@ -12,11 +12,13 @@ import ( // VmUpdateDetails refreshes inventory metadata from vCenter. // @Summary Refresh VM details (deprecated) // @Description Deprecated: Queries vCenter and updates inventory records with missing details. +// @Description Requires Bearer authentication with the admin role. // @Tags inventory // @Deprecated // @Produce json // @Success 200 {object} models.StatusMessageResponse "Update completed" // @Failure 500 {object} models.ErrorResponse "Server error" +// @Security BearerAuth // @Router /api/inventory/vm/update [post] func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/server/middleware/auth.go b/server/middleware/auth.go index c760ad9..52d06f3 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -9,6 +9,7 @@ import ( "time" "vctp/internal/auth" "vctp/internal/settings" + "vctp/server/audit" ) const ( @@ -61,6 +62,7 @@ func RequireAuth(logger *slog.Logger, cfg *settings.Settings) Handler { }) if err != nil { 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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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")) if !hasHeader { if mode == authModeRequired { + audit.LogAuthEvent(logger, r, "token_validation", "deny", "reason", "missing_bearer_token", "auth_mode", mode) writeJSONAuthError(w, http.StatusUnauthorized, "missing bearer token") return } @@ -80,13 +83,14 @@ func RequireAuth(logger *slog.Logger, cfg *settings.Settings) Handler { return } if !parseOK { + audit.LogAuthEvent(logger, r, "token_validation", "deny", "reason", "invalid_bearer_header", "auth_mode", mode) writeJSONAuthError(w, http.StatusUnauthorized, "invalid bearer token") return } claims, err := jwtSvc.VerifyToken(token) 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") return } @@ -111,10 +115,12 @@ func RequireRole(requiredRoles ...string) Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims, ok := ClaimsFromContext(r.Context()) if !ok { + audit.LogAuthEvent(nil, r, "role_authorization", "deny", "reason", "missing_auth_context", "required_roles", normalizedRequired) writeJSONAuthError(w, http.StatusUnauthorized, "missing authentication context") return } 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") return } diff --git a/server/models/api_responses.go b/server/models/api_responses.go index a783f09..f77c533 100644 --- a/server/models/api_responses.go +++ b/server/models/api_responses.go @@ -30,6 +30,20 @@ type AuthLoginResponse struct { 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. type SnapshotMigrationStats struct { HourlyRenamed int `json:"HourlyRenamed"` diff --git a/server/router/auth_integration_test.go b/server/router/auth_integration_test.go new file mode 100644 index 0000000..eaab539 --- /dev/null +++ b/server/router/auth_integration_test.go @@ -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)) +} diff --git a/server/router/docs/docs.go b/server/router/docs/docs.go index b2c87be..abad718 100644 --- a/server/router/docs/docs.go +++ b/server/router/docs/docs.go @@ -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": { "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": [ "text/plain" ], @@ -106,7 +200,12 @@ const docTemplate = `{ }, "/api/diagnostics/daily-creation": { "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": [ "application/json" ], @@ -153,7 +252,12 @@ const docTemplate = `{ }, "/api/encrypt": { "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": [ "application/json" ], @@ -202,7 +306,12 @@ const docTemplate = `{ }, "/api/event/vm/create": { "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": [ "application/json" ], @@ -249,7 +358,12 @@ const docTemplate = `{ }, "/api/event/vm/delete": { "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": [ "application/json" ], @@ -296,7 +410,12 @@ const docTemplate = `{ }, "/api/event/vm/modify": { "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": [ "application/json" ], @@ -343,7 +462,12 @@ const docTemplate = `{ }, "/api/event/vm/move": { "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": [ "application/json" ], @@ -390,7 +514,12 @@ const docTemplate = `{ }, "/api/import/vm": { "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": [ "application/json" ], @@ -431,7 +560,12 @@ const docTemplate = `{ }, "/api/inventory/vm/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": [ "application/json" ], @@ -474,7 +608,12 @@ const docTemplate = `{ }, "/api/inventory/vm/update": { "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": [ "application/json" ], @@ -501,7 +640,12 @@ const docTemplate = `{ }, "/api/report/inventory": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -527,7 +671,12 @@ const docTemplate = `{ }, "/api/report/snapshot": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -568,7 +717,12 @@ const docTemplate = `{ }, "/api/report/updates": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -594,7 +748,12 @@ const docTemplate = `{ }, "/api/snapshots/aggregate": { "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": [ "application/json" ], @@ -648,7 +807,12 @@ const docTemplate = `{ }, "/api/snapshots/hourly/force": { "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": [ "application/json" ], @@ -692,7 +856,12 @@ const docTemplate = `{ }, "/api/snapshots/migrate": { "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": [ "application/json" ], @@ -718,7 +887,12 @@ const docTemplate = `{ }, "/api/snapshots/regenerate-hourly-reports": { "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": [ "application/json" ], @@ -744,7 +918,12 @@ const docTemplate = `{ }, "/api/snapshots/repair": { "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": [ "application/json" ], @@ -764,7 +943,12 @@ const docTemplate = `{ }, "/api/snapshots/repair/all": { "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": [ "application/json" ], @@ -784,7 +968,12 @@ const docTemplate = `{ }, "/api/vcenters/cache/rebuild": { "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": [ "application/json" ], @@ -1120,6 +1309,72 @@ const docTemplate = `{ } }, "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": { "type": "object", "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 var SwaggerInfo = &swag.Spec{ - Version: "", + Version: "1.0", Host: "", - BasePath: "", - Schemes: []string{}, - Title: "", - Description: "", + BasePath: "/", + Schemes: []string{"http", "https"}, + Title: "vCTP API", + Description: "vCTP API endpoints for inventory snapshots, reporting, and administration.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/server/router/docs/swagger.json b/server/router/docs/swagger.json index 9929b73..fa111dc 100644 --- a/server/router/docs/swagger.json +++ b/server/router/docs/swagger.json @@ -1,8 +1,16 @@ { + "schemes": [ + "http", + "https" + ], "swagger": "2.0", "info": { - "contact": {} + "description": "vCTP API endpoints for inventory snapshots, reporting, and administration.", + "title": "vCTP API", + "contact": {}, + "version": "1.0" }, + "basePath": "/", "paths": { "/": { "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": { "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": [ "text/plain" ], @@ -95,7 +197,12 @@ }, "/api/diagnostics/daily-creation": { "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": [ "application/json" ], @@ -142,7 +249,12 @@ }, "/api/encrypt": { "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": [ "application/json" ], @@ -191,7 +303,12 @@ }, "/api/event/vm/create": { "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": [ "application/json" ], @@ -238,7 +355,12 @@ }, "/api/event/vm/delete": { "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": [ "application/json" ], @@ -285,7 +407,12 @@ }, "/api/event/vm/modify": { "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": [ "application/json" ], @@ -332,7 +459,12 @@ }, "/api/event/vm/move": { "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": [ "application/json" ], @@ -379,7 +511,12 @@ }, "/api/import/vm": { "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": [ "application/json" ], @@ -420,7 +557,12 @@ }, "/api/inventory/vm/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": [ "application/json" ], @@ -463,7 +605,12 @@ }, "/api/inventory/vm/update": { "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": [ "application/json" ], @@ -490,7 +637,12 @@ }, "/api/report/inventory": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -516,7 +668,12 @@ }, "/api/report/snapshot": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -557,7 +714,12 @@ }, "/api/report/updates": { "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": [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], @@ -583,7 +745,12 @@ }, "/api/snapshots/aggregate": { "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": [ "application/json" ], @@ -637,7 +804,12 @@ }, "/api/snapshots/hourly/force": { "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": [ "application/json" ], @@ -681,7 +853,12 @@ }, "/api/snapshots/migrate": { "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": [ "application/json" ], @@ -707,7 +884,12 @@ }, "/api/snapshots/regenerate-hourly-reports": { "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": [ "application/json" ], @@ -733,7 +915,12 @@ }, "/api/snapshots/repair": { "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": [ "application/json" ], @@ -753,7 +940,12 @@ }, "/api/snapshots/repair/all": { "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": [ "application/json" ], @@ -773,7 +965,12 @@ }, "/api/vcenters/cache/rebuild": { "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": [ "application/json" ], @@ -1109,6 +1306,72 @@ } }, "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": { "type": "object", "properties": { @@ -1602,5 +1865,12 @@ } } } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } } \ No newline at end of file diff --git a/server/router/docs/swagger.yaml b/server/router/docs/swagger.yaml index 139aeef..9ada283 100644 --- a/server/router/docs/swagger.yaml +++ b/server/router/docs/swagger.yaml @@ -1,4 +1,48 @@ +basePath: / 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: properties: cloudEvent: @@ -320,6 +364,9 @@ definitions: type: object info: contact: {} + description: vCTP API endpoints for inventory snapshots, reporting, and administration. + title: vCTP API + version: "1.0" paths: /: get: @@ -338,11 +385,72 @@ paths: summary: Home page tags: - 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: delete: deprecated: true - 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. + Requires Bearer authentication with the admin role. produces: - text/plain responses: @@ -354,6 +462,8 @@ paths: description: Server error schema: type: string + security: + - BearerAuth: [] summary: Cleanup updates (deprecated) tags: - maintenance @@ -384,8 +494,9 @@ paths: - maintenance /api/diagnostics/daily-creation: get: - 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. + Requires Bearer authentication with the viewer role (admin also allowed). parameters: - description: Daily date (YYYY-MM-DD) in: query @@ -411,6 +522,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Daily summary CreationTime diagnostics tags: - diagnostics @@ -418,7 +531,9 @@ paths: post: consumes: - 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: - description: Plaintext payload in: body @@ -443,6 +558,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Encrypt data tags: - crypto @@ -451,8 +568,9 @@ paths: consumes: - application/json deprecated: true - description: 'Deprecated: Parses a VM create CloudEvent and stores the event - data.' + description: |- + Deprecated: Parses a VM create CloudEvent and stores the event data. + Requires Bearer authentication with the admin role. parameters: - description: CloudEvent payload in: body @@ -475,6 +593,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Record VM create event (deprecated) tags: - events @@ -483,8 +603,9 @@ paths: consumes: - application/json deprecated: true - 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. + Requires Bearer authentication with the admin role. parameters: - description: CloudEvent payload in: body @@ -507,6 +628,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Record VM delete event (deprecated) tags: - events @@ -515,8 +638,9 @@ paths: consumes: - application/json deprecated: true - 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. + Requires Bearer authentication with the admin role. parameters: - description: CloudEvent payload in: body @@ -539,6 +663,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Record VM modify event (deprecated) tags: - events @@ -547,8 +673,9 @@ paths: consumes: - application/json deprecated: true - description: 'Deprecated: Parses a VM move CloudEvent and creates an update - record.' + description: |- + Deprecated: Parses a VM move CloudEvent and creates an update record. + Requires Bearer authentication with the admin role. parameters: - description: CloudEvent payload in: body @@ -571,6 +698,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Record VM move event (deprecated) tags: - events @@ -579,7 +708,9 @@ paths: consumes: - application/json 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: - description: Bulk import payload in: body @@ -598,14 +729,17 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Import VMs (deprecated) tags: - inventory /api/inventory/vm/delete: delete: deprecated: true - 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. + Requires Bearer authentication with the admin role. parameters: - description: VM ID in: query @@ -628,14 +762,17 @@ paths: description: Invalid request schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Cleanup VM inventory entry (deprecated) tags: - inventory /api/inventory/vm/update: post: deprecated: true - description: 'Deprecated: Queries vCenter and updates inventory records with - missing details.' + description: |- + Deprecated: Queries vCenter and updates inventory records with missing details. + Requires Bearer authentication with the admin role. produces: - application/json responses: @@ -647,12 +784,16 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Refresh VM details (deprecated) tags: - inventory /api/report/inventory: 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: - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet responses: @@ -664,12 +805,16 @@ paths: description: Report generation failed schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Download inventory report tags: - reports /api/report/snapshot: 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: - description: Snapshot table name in: query @@ -691,12 +836,16 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Download snapshot report tags: - snapshots /api/report/updates: 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: - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet responses: @@ -708,13 +857,16 @@ paths: description: Report generation failed schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Download updates report tags: - reports /api/snapshots/aggregate: post: - 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. + Requires Bearer authentication with the admin role. parameters: - description: 'Aggregation type: daily or monthly' in: query @@ -745,6 +897,8 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Force snapshot aggregation tags: - snapshots @@ -752,8 +906,9 @@ paths: post: consumes: - application/json - 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. + Requires Bearer authentication with the admin role. parameters: - description: Confirmation text; must be 'FORCE' in: query @@ -775,13 +930,16 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Trigger hourly snapshot (manual) tags: - snapshots /api/snapshots/migrate: post: - 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. + Requires Bearer authentication with the admin role. produces: - application/json responses: @@ -793,13 +951,16 @@ paths: description: Server error schema: $ref: '#/definitions/models.SnapshotMigrationResponse' + security: + - BearerAuth: [] summary: Migrate snapshot registry tags: - snapshots /api/snapshots/regenerate-hourly-reports: post: - 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. + Requires Bearer authentication with the admin role. produces: - application/json responses: @@ -811,13 +972,16 @@ paths: description: Server error schema: $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] summary: Regenerate hourly snapshot reports tags: - snapshots /api/snapshots/repair: post: - 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. + Requires Bearer authentication with the admin role. produces: - application/json responses: @@ -825,13 +989,16 @@ paths: description: OK schema: $ref: '#/definitions/models.SnapshotRepairResponse' + security: + - BearerAuth: [] summary: Repair daily summaries tags: - snapshots /api/snapshots/repair/all: post: - 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. + Requires Bearer authentication with the admin role. produces: - application/json responses: @@ -839,13 +1006,16 @@ paths: description: OK schema: $ref: '#/definitions/models.SnapshotRepairSuiteResponse' + security: + - BearerAuth: [] summary: Run full snapshot repair suite tags: - snapshots /api/vcenters/cache/rebuild: post: - 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. + Requires Bearer authentication with the admin role. parameters: - description: Optional single vCenter URL to rebuild; defaults to all configured vCenters @@ -871,6 +1041,8 @@ paths: description: All rebuild attempts failed schema: $ref: '#/definitions/models.VcenterCacheRebuildResponse' + security: + - BearerAuth: [] summary: Rebuild vCenter object cache tags: - vcenters @@ -1069,4 +1241,12 @@ paths: summary: Trace VM history tags: - vm +schemes: +- http +- https +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/server/router/router.go b/server/router/router.go index 9dcf3c2..3bbac1b 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -30,6 +30,9 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st mux := http.NewServeMux() 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 { wrapped := http.Handler(http.HandlerFunc(next)) 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/diagnostics/daily-creation", withAuthRole(h.DailyCreationDiagnostics, middleware.RoleViewer)) mux.HandleFunc("/api/auth/login", h.AuthLogin) + mux.Handle("/api/auth/me", withAuth(h.AuthMe)) mux.HandleFunc("/vm/trace", h.VmTrace) mux.HandleFunc("/vcenters", h.VcenterList) mux.HandleFunc("/vcenters/totals", h.VcenterTotals) diff --git a/vctp-service.yml b/vctp-service.yml index 811bdfe..3dfce7e 100644 --- a/vctp-service.yml +++ b/vctp-service.yml @@ -1,7 +1,7 @@ name: "vctp" arch: "amd64" platform: "linux" -version: "v26.1.3" +version: "v26.4.1" version_schema: semver description: vCTP monitors VMware VM inventory and event data to build chargeback reports maintainer: "@coadn"