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:
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -88,6 +89,15 @@ type Backend interface {
|
||||
ListGlobalSettings(tenantID string) (map[string]any, error)
|
||||
PatchGlobalSettings(tenantID string, patch map[string]any) error
|
||||
|
||||
ListAPIKeys(tenantID string) ([]*APIKey, error)
|
||||
GetAPIKey(tenantID, id string) (*APIKey, error)
|
||||
CreateAPIKey(tenantID string, in *APIKeyCreate) (*APIKeyWithSecret, error)
|
||||
UpdateAPIKey(tenantID, id string, patch *APIKeyPatch) (*APIKey, error)
|
||||
RevokeAPIKey(tenantID, id string) error
|
||||
RotateAPIKey(tenantID, id string) (*APIKeyWithSecret, error)
|
||||
ListActiveAPIKeyHashes() ([]APIKeyAuthRow, error)
|
||||
TouchAPIKeyLastUsed(id string) error
|
||||
|
||||
// Module prefix snapshots cache last successful collect per module (pipeline ingest/render).
|
||||
GetModulePrefixSnapshot(tenantID, moduleID string) (*ModulePrefixSnapshot, bool, error)
|
||||
SetModulePrefixSnapshot(tenantID, moduleID, inputHash string, prefixes []PrefixRow) error
|
||||
@@ -227,6 +237,59 @@ type CommunityPatch struct {
|
||||
ValueJSON *string `json:"value_json,omitempty"`
|
||||
}
|
||||
|
||||
// APIKey is tenant-scoped API key metadata (secret never stored in plaintext).
|
||||
type APIKey struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
Prefix string `json:"prefix"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
}
|
||||
|
||||
// APIKeyCreate is input for issuing a new key.
|
||||
type APIKeyCreate struct {
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// APIKeyPatch is a partial update (role change affects auth after resolver reload).
|
||||
type APIKeyPatch struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
ClearExpiresAt bool `json:"-"`
|
||||
}
|
||||
|
||||
// APIKeyWithSecret is returned only on create/rotate.
|
||||
type APIKeyWithSecret struct {
|
||||
APIKey
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// APIKeyAuthRow is used to build the in-process auth index.
|
||||
type APIKeyAuthRow struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Role string
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
// ValidAPIKeyRole reports whether role is allowed for API keys.
|
||||
func ValidAPIKeyRole(role string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "viewer", "editor", "operator", "node":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type PeerPatch struct {
|
||||
Neighbor *string `json:"neighbor,omitempty"`
|
||||
RemoteASN *int64 `json:"remote_asn,omitempty"`
|
||||
|
||||
@@ -43,6 +43,7 @@ type Memory struct {
|
||||
revPrefixes map[string][]PrefixRow
|
||||
moduleSnapshots map[string]*moduleSnapshotRec
|
||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||
apiKeys map[string]*apiKeyRec
|
||||
|
||||
// DemoIDs valid after SeedDemo()
|
||||
demoTenantID string
|
||||
@@ -57,6 +58,11 @@ type publishedInfo struct {
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
type apiKeyRec struct {
|
||||
APIKey
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID string
|
||||
Name string
|
||||
@@ -133,6 +139,7 @@ func NewMemory() *Memory {
|
||||
revPrefixes: make(map[string][]PrefixRow),
|
||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||
apiKeys: make(map[string]*apiKeyRec),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/authkey"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (m *Memory) ListAPIKeys(tenantID string) ([]*APIKey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []*APIKey
|
||||
for _, rec := range m.apiKeys {
|
||||
if rec.TenantID == tenantID {
|
||||
out = append(out, apiKeyCopy(&rec.APIKey))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetAPIKey(tenantID, id string) (*APIKey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
rec, ok := m.apiKeys[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return apiKeyCopy(&rec.APIKey), nil
|
||||
}
|
||||
|
||||
func (m *Memory) CreateAPIKey(tenantID string, in *APIKeyCreate) (*APIKeyWithSecret, error) {
|
||||
if in == nil || strings.TrimSpace(in.Name) == "" || !ValidAPIKeyRole(in.Role) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
tok, err := authkey.GenerateToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.tenants[tenantID]; !ok {
|
||||
return nil, ErrTenantScope
|
||||
}
|
||||
id := uuid.NewString()
|
||||
k := &apiKeyRec{
|
||||
APIKey: APIKey{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
Name: strings.TrimSpace(in.Name),
|
||||
Role: strings.ToLower(strings.TrimSpace(in.Role)),
|
||||
Prefix: authkey.Prefix(tok),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ExpiresAt: in.ExpiresAt,
|
||||
},
|
||||
TokenHash: authkey.HashToken(tok),
|
||||
}
|
||||
m.apiKeys[id] = k
|
||||
return &APIKeyWithSecret{APIKey: *apiKeyCopy(&k.APIKey), Token: tok}, nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdateAPIKey(tenantID, id string, patch *APIKeyPatch) (*APIKey, error) {
|
||||
if patch == nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.apiKeys[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if rec.RevokedAt != nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if patch.Name != nil {
|
||||
n := strings.TrimSpace(*patch.Name)
|
||||
if n == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
rec.Name = n
|
||||
}
|
||||
if patch.Role != nil {
|
||||
if !ValidAPIKeyRole(*patch.Role) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
rec.Role = strings.ToLower(strings.TrimSpace(*patch.Role))
|
||||
}
|
||||
if patch.ClearExpiresAt {
|
||||
rec.ExpiresAt = nil
|
||||
} else if patch.ExpiresAt != nil {
|
||||
rec.ExpiresAt = patch.ExpiresAt
|
||||
}
|
||||
rec.UpdatedAt = time.Now().UTC()
|
||||
return apiKeyCopy(&rec.APIKey), nil
|
||||
}
|
||||
|
||||
func (m *Memory) RevokeAPIKey(tenantID, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.apiKeys[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return ErrNotFound
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.RevokedAt = &now
|
||||
rec.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) RotateAPIKey(tenantID, id string) (*APIKeyWithSecret, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.apiKeys[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if rec.RevokedAt != nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
tok, err := authkey.GenerateToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.TokenHash = authkey.HashToken(tok)
|
||||
rec.Prefix = authkey.Prefix(tok)
|
||||
rec.UpdatedAt = now
|
||||
return &APIKeyWithSecret{APIKey: *apiKeyCopy(&rec.APIKey), Token: tok}, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListActiveAPIKeyHashes() ([]APIKeyAuthRow, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
now := time.Now().UTC()
|
||||
var out []APIKeyAuthRow
|
||||
for _, rec := range m.apiKeys {
|
||||
if rec.RevokedAt != nil {
|
||||
continue
|
||||
}
|
||||
if rec.ExpiresAt != nil && !rec.ExpiresAt.After(now) {
|
||||
continue
|
||||
}
|
||||
out = append(out, APIKeyAuthRow{
|
||||
ID: rec.ID,
|
||||
TenantID: rec.TenantID,
|
||||
Role: rec.Role,
|
||||
TokenHash: append([]byte(nil), rec.TokenHash...),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) TouchAPIKeyLastUsed(id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.apiKeys[id]
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.LastUsedAt = &now
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiKeyCopy(k *APIKey) *APIKey {
|
||||
cp := *k
|
||||
if k.ExpiresAt != nil {
|
||||
t := *k.ExpiresAt
|
||||
cp.ExpiresAt = &t
|
||||
}
|
||||
if k.RevokedAt != nil {
|
||||
t := *k.RevokedAt
|
||||
cp.RevokedAt = &t
|
||||
}
|
||||
if k.LastUsedAt != nil {
|
||||
t := *k.LastUsedAt
|
||||
cp.LastUsedAt = &t
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
Reference in New Issue
Block a user