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,66 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
type apiKeyResolver struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
envByToken map[string]apiKeyRecord
|
||||
byHash map[string]apiKeyRecord
|
||||
}
|
||||
|
||||
func newAPIKeyResolver(envSpec string, st store.Backend) (*apiKeyResolver, error) {
|
||||
r := &apiKeyResolver{
|
||||
envByToken: make(map[string]apiKeyRecord),
|
||||
byHash: make(map[string]apiKeyRecord),
|
||||
}
|
||||
for _, rec := range parseAPIKeysSpec(envSpec) {
|
||||
r.envByToken[rec.token] = rec
|
||||
}
|
||||
return r, r.reloadFromStore(st)
|
||||
}
|
||||
|
||||
func (r *apiKeyResolver) reloadFromStore(st store.Backend) error {
|
||||
rows, err := st.ListActiveAPIKeyHashes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byHash := make(map[string]apiKeyRecord, len(rows))
|
||||
for _, row := range rows {
|
||||
if len(row.TokenHash) != 32 {
|
||||
continue
|
||||
}
|
||||
byHash[hex.EncodeToString(row.TokenHash)] = apiKeyRecord{
|
||||
token: "",
|
||||
tenantID: row.TenantID,
|
||||
role: row.Role,
|
||||
keyID: row.ID,
|
||||
}
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.byHash = byHash
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apiKeyResolver) Reload(st store.Backend) error {
|
||||
return r.reloadFromStore(st)
|
||||
}
|
||||
|
||||
func (r *apiKeyResolver) Lookup(raw string) (apiKeyRecord, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if rec, ok := r.envByToken[raw]; ok {
|
||||
return rec, true
|
||||
}
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
key := hex.EncodeToString(sum[:])
|
||||
rec, ok := r.byHash[key]
|
||||
return rec, ok
|
||||
}
|
||||
+13
-21
@@ -15,6 +15,7 @@ type Auth struct {
|
||||
TenantID string
|
||||
Role string // viewer, editor, operator, node
|
||||
Token string
|
||||
APIKeyID string // non-empty for DB-managed keys
|
||||
}
|
||||
|
||||
func authFromContext(ctx context.Context) (Auth, bool) {
|
||||
@@ -26,6 +27,7 @@ type apiKeyRecord struct {
|
||||
token string
|
||||
tenantID string
|
||||
role string
|
||||
keyID string // set for DB-managed keys (last_used_at)
|
||||
}
|
||||
|
||||
func parseAPIKeysSpec(spec string) []apiKeyRecord {
|
||||
@@ -54,20 +56,6 @@ func parseAPIKeysSpec(spec string) []apiKeyRecord {
|
||||
|
||||
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.insecureDev {
|
||||
h := r.Header.Get("Authorization")
|
||||
const p = "Bearer "
|
||||
if strings.HasPrefix(h, p) {
|
||||
tok := strings.TrimSpace(strings.TrimPrefix(h, p))
|
||||
if tok == "dev" {
|
||||
if a, ok := s.devAuth(); ok {
|
||||
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h := r.Header.Get("Authorization")
|
||||
const p = "Bearer "
|
||||
if !strings.HasPrefix(h, p) {
|
||||
@@ -75,18 +63,22 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
raw := strings.TrimSpace(strings.TrimPrefix(h, p))
|
||||
var matched *apiKeyRecord
|
||||
for i := range s.apiKeys {
|
||||
if s.apiKeys[i].token == raw {
|
||||
matched = &s.apiKeys[i]
|
||||
break
|
||||
if raw == "dev" {
|
||||
if a, ok := s.devAuth(); ok {
|
||||
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if matched == nil {
|
||||
matched, ok := s.keyResolver.Lookup(raw)
|
||||
if !ok {
|
||||
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "unknown api key")
|
||||
return
|
||||
}
|
||||
a := Auth{TenantID: matched.tenantID, Role: matched.role, Token: raw}
|
||||
a := Auth{TenantID: matched.tenantID, Role: matched.role, Token: raw, APIKeyID: matched.keyID}
|
||||
if matched.keyID != "" {
|
||||
go func(id string) { _ = s.store.TouchAPIKeyLastUsed(id) }(matched.keyID)
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBearerDevWithoutInsecureDev(t *testing.T) {
|
||||
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/modules?limit=1", nil)
|
||||
req.Header.Set("Authorization", "Bearer dev")
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("status=%d body=%s", resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeysCRUDAndAuth(t *testing.T) {
|
||||
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv.Close()
|
||||
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
||||
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
base := ts.URL
|
||||
|
||||
reqCreate, _ := http.NewRequest(http.MethodPost, base+"/v1/api-keys", strings.NewReader(`{"name":"ci","role":"editor"}`))
|
||||
reqCreate.Header.Set("Authorization", "Bearer opkey")
|
||||
reqCreate.Header.Set("Content-Type", "application/json")
|
||||
respCreate, err := client.Do(reqCreate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer respCreate.Body.Close()
|
||||
if respCreate.StatusCode != http.StatusCreated {
|
||||
b, _ := io.ReadAll(respCreate.Body)
|
||||
t.Fatalf("create status=%d body=%s", respCreate.StatusCode, b)
|
||||
}
|
||||
var created map[string]any
|
||||
if err := json.NewDecoder(respCreate.Body).Decode(&created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, _ := created["token"].(string)
|
||||
if token == "" {
|
||||
t.Fatal("missing token in create response")
|
||||
}
|
||||
id, _ := created["id"].(string)
|
||||
if id == "" {
|
||||
t.Fatal("missing id")
|
||||
}
|
||||
|
||||
reqMod, _ := http.NewRequest(http.MethodGet, base+"/v1/modules?limit=1", nil)
|
||||
reqMod.Header.Set("Authorization", "Bearer "+token)
|
||||
respMod, err := client.Do(reqMod)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer respMod.Body.Close()
|
||||
if respMod.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(respMod.Body)
|
||||
t.Fatalf("modules status=%d body=%s", respMod.StatusCode, b)
|
||||
}
|
||||
|
||||
reqDel, _ := http.NewRequest(http.MethodDelete, base+"/v1/api-keys/"+id, nil)
|
||||
reqDel.Header.Set("Authorization", "Bearer opkey")
|
||||
respDel, err := client.Do(reqDel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer respDel.Body.Close()
|
||||
if respDel.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("delete status=%d", respDel.StatusCode)
|
||||
}
|
||||
|
||||
reqAfter, _ := http.NewRequest(http.MethodGet, base+"/v1/modules?limit=1", nil)
|
||||
reqAfter.Header.Set("Authorization", "Bearer "+token)
|
||||
respAfter, err := client.Do(reqAfter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer respAfter.Body.Close()
|
||||
if respAfter.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 after revoke, got %d", respAfter.StatusCode)
|
||||
}
|
||||
|
||||
mustSetTestAPIKeys(t, srv, "nodekey|"+tenant+"|node,opkey|"+tenant+"|operator")
|
||||
reqNode2, _ := http.NewRequest(http.MethodGet, base+"/v1/api-keys", nil)
|
||||
reqNode2.Header.Set("Authorization", "Bearer nodekey")
|
||||
respNode, err := client.Do(reqNode2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer respNode.Body.Close()
|
||||
if respNode.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("node list api-keys status=%d want 403", respNode.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,8 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
||||
|
||||
m.HandleFunc("GET /settings", s.handleGetSettings)
|
||||
m.HandleFunc("PATCH /settings", s.handlePatchSettings)
|
||||
|
||||
s.registerAPIKeyRoutes(m)
|
||||
}
|
||||
|
||||
func (s *Server) handlePostModule(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestModuleEntriesCSVImportExportIPRanges(t *testing.T) {
|
||||
defer srv.Close()
|
||||
|
||||
tenant, _, modIP, _, _ := srv.Store().DemoIDs()
|
||||
srv.apiKeys = parseAPIKeysSpec("opkey|" + tenant + "|operator")
|
||||
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestNestedModuleListPagination(t *testing.T) {
|
||||
}
|
||||
defer srv.Close()
|
||||
tenant, _, modIP, _, _ := srv.Store().DemoIDs()
|
||||
srv.apiKeys = parseAPIKeysSpec("edkey|" + tenant + "|editor")
|
||||
mustSetTestAPIKeys(t, srv, "edkey|"+tenant+"|editor")
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -21,8 +21,7 @@ type Server struct {
|
||||
pgPool *pgxpool.Pool
|
||||
jobs *jobs.Registry
|
||||
bundlePriv ed25519.PrivateKey
|
||||
apiKeys []apiKeyRecord
|
||||
insecureDev bool
|
||||
keyResolver *apiKeyResolver
|
||||
corsOrigins []string
|
||||
cdnHTTP *http.Client
|
||||
mux *http.ServeMux
|
||||
@@ -60,13 +59,16 @@ func New(opts Options) (*Server, error) {
|
||||
_, priv, _ = ed25519.GenerateKey(rand.Reader)
|
||||
}
|
||||
|
||||
resolver, err := newAPIKeyResolver(opts.APIKeys, backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{
|
||||
store: backend,
|
||||
pgPool: pool,
|
||||
jobs: reg,
|
||||
bundlePriv: priv,
|
||||
apiKeys: parseAPIKeysSpec(opts.APIKeys),
|
||||
insecureDev: opts.InsecureDev && opts.SeedDemo,
|
||||
keyResolver: resolver,
|
||||
corsOrigins: parseCORSOrigins(opts.CORSAllowedOrigins),
|
||||
cdnHTTP: NewCDNHTTPClient(),
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
||||
}
|
||||
defer srv.Close()
|
||||
tenant, modCDN, modIP, rev, speaker := srv.Store().DemoIDs()
|
||||
srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator,edkey|" + tenant + "|editor")
|
||||
mustSetTestAPIKeys(t, srv, "nodekey|"+tenant+"|node,opkey|"+tenant+"|operator,edkey|"+tenant+"|editor")
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package httpapi
|
||||
|
||||
import "testing"
|
||||
|
||||
func mustSetTestAPIKeys(t *testing.T, srv *Server, spec string) {
|
||||
t.Helper()
|
||||
resolver, err := newAPIKeyResolver(spec, srv.store)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.keyResolver = resolver
|
||||
}
|
||||
Reference in New Issue
Block a user