feat(api): implement API key management and authentication enhancements
CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 28s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 28s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
- Added endpoints for managing API keys, including creation, retrieval, updating, and revocation. - Introduced a new Auth session endpoint to retrieve current tenant and role information. - Updated the authentication middleware to support API key-based authentication and track last used timestamps. - Enhanced documentation to reflect new API key functionalities and usage guidelines. - Improved logging for demo authentication scenarios.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
func (s *Server) registerAPIKeyRoutes(m *http.ServeMux) {
|
||||
m.HandleFunc("GET /auth/session", s.handleAuthSession)
|
||||
m.HandleFunc("GET /api-keys", s.handleListAPIKeys)
|
||||
m.HandleFunc("POST /api-keys", s.handlePostAPIKey)
|
||||
m.HandleFunc("GET /api-keys/{id}", s.handleGetAPIKey)
|
||||
m.HandleFunc("PATCH /api-keys/{id}", s.handlePatchAPIKey)
|
||||
m.HandleFunc("DELETE /api-keys/{id}", s.handleDeleteAPIKey)
|
||||
m.HandleFunc("POST /api-keys/{id}/rotate", s.handleRotateAPIKey)
|
||||
}
|
||||
|
||||
func (s *Server) handleAuthSession(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"tenant_id": a.TenantID,
|
||||
"role": a.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func apiKeyJSON(k *store.APIKey) map[string]any {
|
||||
m := map[string]any{
|
||||
"id": k.ID,
|
||||
"name": k.Name,
|
||||
"role": k.Role,
|
||||
"prefix": k.Prefix,
|
||||
"created_at": k.CreatedAt.UTC().Format(time.RFC3339),
|
||||
"updated_at": k.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if k.ExpiresAt != nil {
|
||||
m["expires_at"] = k.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
m["expires_at"] = nil
|
||||
}
|
||||
if k.RevokedAt != nil {
|
||||
m["revoked_at"] = k.RevokedAt.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
m["revoked_at"] = nil
|
||||
}
|
||||
if k.LastUsedAt != nil {
|
||||
m["last_used_at"] = k.LastUsedAt.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
m["last_used_at"] = nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
list, err := s.store.ListAPIKeys(a.TenantID)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
writePaginatedListJSON(w, r, list, func(k *store.APIKey) map[string]any {
|
||||
return apiKeyJSON(k)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
k, err := s.store.GetAPIKey(a.TenantID, r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiKeyJSON(k))
|
||||
}
|
||||
|
||||
func (s *Server) handlePostAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
in := &store.APIKeyCreate{
|
||||
Name: strings.TrimSpace(body.Name),
|
||||
Role: strings.TrimSpace(body.Role),
|
||||
}
|
||||
if body.ExpiresAt != nil && strings.TrimSpace(*body.ExpiresAt) != "" {
|
||||
t, err := time.Parse(time.RFC3339, strings.TrimSpace(*body.ExpiresAt))
|
||||
if err != nil {
|
||||
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at")
|
||||
return
|
||||
}
|
||||
in.ExpiresAt = &t
|
||||
}
|
||||
created, err := s.store.CreateAPIKey(a.TenantID, in)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
if err := s.keyResolver.Reload(s.store); err != nil {
|
||||
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
|
||||
return
|
||||
}
|
||||
out := apiKeyJSON(&created.APIKey)
|
||||
out["token"] = created.Token
|
||||
writeJSON(w, http.StatusCreated, out)
|
||||
}
|
||||
|
||||
func (s *Server) handlePatchAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||
return
|
||||
}
|
||||
patch := &store.APIKeyPatch{}
|
||||
if v, ok := raw["name"]; ok {
|
||||
var name string
|
||||
if err := json.Unmarshal(v, &name); err != nil {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid name")
|
||||
return
|
||||
}
|
||||
patch.Name = &name
|
||||
}
|
||||
if v, ok := raw["role"]; ok {
|
||||
var role string
|
||||
if err := json.Unmarshal(v, &role); err != nil {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid role")
|
||||
return
|
||||
}
|
||||
patch.Role = &role
|
||||
}
|
||||
if v, ok := raw["expires_at"]; ok {
|
||||
if string(v) == "null" {
|
||||
patch.ClearExpiresAt = true
|
||||
} else {
|
||||
var s string
|
||||
if err := json.Unmarshal(v, &s); err != nil {
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid expires_at")
|
||||
return
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at")
|
||||
return
|
||||
}
|
||||
patch.ExpiresAt = &t
|
||||
}
|
||||
}
|
||||
k, err := s.store.UpdateAPIKey(a.TenantID, r.PathValue("id"), patch)
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
if err := s.keyResolver.Reload(s.store); err != nil {
|
||||
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiKeyJSON(k))
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
if err := s.store.RevokeAPIKey(a.TenantID, r.PathValue("id")); err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
if err := s.keyResolver.Reload(s.store); err != nil {
|
||||
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleRotateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||
return
|
||||
}
|
||||
rotated, err := s.store.RotateAPIKey(a.TenantID, r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeStoreErr(w, err)
|
||||
return
|
||||
}
|
||||
if err := s.keyResolver.Reload(s.store); err != nil {
|
||||
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
|
||||
return
|
||||
}
|
||||
out := apiKeyJSON(&rotated.APIKey)
|
||||
out["token"] = rotated.Token
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
Reference in New Issue
Block a user