196b543bee
CI / build-and-test (push) Successful in 1m47s
* Implement tenant and user creation in IdentityPage * Add API calls for managing tenants and users * Introduce tenant-scoped API requests * Update sidebar to include identity navigation * Create BooleanStatusBadge component for key status
141 lines
3.2 KiB
Go
141 lines
3.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.warky.dev/wdevs/amcs/internal/config"
|
|
)
|
|
|
|
type Keyring struct {
|
|
mu sync.RWMutex
|
|
keys []config.APIKey
|
|
tenantsByKeyID map[string]string
|
|
managed map[string]managedKey
|
|
}
|
|
|
|
type managedKey struct {
|
|
secretHash string
|
|
enabled bool
|
|
}
|
|
|
|
func NewKeyring(keys []config.APIKey) (*Keyring, error) {
|
|
if len(keys) == 0 {
|
|
return nil, fmt.Errorf("keyring requires at least one key")
|
|
}
|
|
|
|
tenantsByKeyID := make(map[string]string)
|
|
for _, key := range keys {
|
|
if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" {
|
|
tenantsByKeyID[key.ID] = tenantID
|
|
}
|
|
}
|
|
return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil
|
|
}
|
|
|
|
// NewManagedKeyring is used when API credentials are administered in the
|
|
// database rather than supplied through static configuration.
|
|
func NewManagedKeyring() *Keyring {
|
|
return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)}
|
|
}
|
|
|
|
func (k *Keyring) Lookup(value string) (string, bool) {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
for _, key := range k.keys {
|
|
if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 {
|
|
return key.ID, true
|
|
}
|
|
}
|
|
hash := secretHash(value)
|
|
for keyID, key := range k.managed {
|
|
if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 {
|
|
return keyID, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// TenantForKey returns the tenant boundary assigned to keyID. Unassigned
|
|
// configured keys retain the historical key-ID boundary for compatibility.
|
|
func (k *Keyring) TenantForKey(keyID string) string {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" {
|
|
return tenantID
|
|
}
|
|
return keyID
|
|
}
|
|
|
|
func (k *Keyring) AssignTenant(keyID, tenantID string) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if tenantID == "" {
|
|
delete(k.tenantsByKeyID, keyID)
|
|
return
|
|
}
|
|
k.tenantsByKeyID[keyID] = tenantID
|
|
}
|
|
|
|
func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled}
|
|
}
|
|
|
|
func (k *Keyring) ConfiguredKeys() []config.APIKey {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
return append([]config.APIKey(nil), k.keys...)
|
|
}
|
|
|
|
func (k *Keyring) IsConfigured(keyID string) bool {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
for _, key := range k.keys {
|
|
if key.ID == keyID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (k *Keyring) IsSuperadmin(keyID string) bool {
|
|
k.mu.RLock()
|
|
defer k.mu.RUnlock()
|
|
for _, key := range k.keys {
|
|
if key.ID == keyID {
|
|
return key.Superadmin
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if key, ok := k.managed[keyID]; ok {
|
|
key.enabled = enabled
|
|
k.managed[keyID] = key
|
|
}
|
|
}
|
|
|
|
func GenerateSecret() (string, string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", "", err
|
|
}
|
|
secret := "amcs_" + hex.EncodeToString(b)
|
|
return secret, secretHash(secret), nil
|
|
}
|
|
|
|
func secretHash(secret string) string {
|
|
sum := sha256.Sum256([]byte(secret))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|