Files
ez-api/internal/service/master.go
zenfun 2e3b471533 feat(core): implement namespace support and routing bindings
Introduce namespace-aware routing capabilities through a new Binding
model and updates to Master/Key entities.

- Add Binding model for configuring model routes per namespace
- Add DefaultNamespace and Namespaces fields to Master and Key models
- Update auto-migration to include Binding table
- Implement Redis synchronization for binding configurations
- Propagate namespace settings during master creation and key issuance
2025-12-17 00:33:36 +08:00

185 lines
5.2 KiB
Go

package service
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/ez-api/ez-api/internal/model"
"github.com/ez-api/foundation/tokenhash"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var (
ErrMasterNotFound = errors.New("master not found")
ErrMasterNotActive = errors.New("master is not active")
ErrChildKeyLimitReached = errors.New("child key limit reached")
ErrChildKeyGroupForbidden = errors.New("cannot issue key for a different group")
)
type MasterService struct {
db *gorm.DB
}
func NewMasterService(db *gorm.DB) *MasterService {
return &MasterService{db: db}
}
func (s *MasterService) CreateMaster(name, group string, maxChildKeys, globalQPS int) (*model.Master, string, error) {
rawMasterKey, err := generateRandomKey(32)
if err != nil {
return nil, "", fmt.Errorf("failed to generate master key: %w", err)
}
hashedMasterKey, err := bcrypt.GenerateFromPassword([]byte(rawMasterKey), bcrypt.DefaultCost)
if err != nil {
return nil, "", fmt.Errorf("failed to hash master key: %w", err)
}
masterKeyDigest := tokenhash.HashToken(rawMasterKey)
master := &model.Master{
Name: name,
MasterKey: string(hashedMasterKey),
MasterKeyDigest: masterKeyDigest,
Group: group,
DefaultNamespace: "default",
Namespaces: "default",
MaxChildKeys: maxChildKeys,
GlobalQPS: globalQPS,
Status: "active",
Epoch: 1,
}
if err := s.db.Create(master).Error; err != nil {
return nil, "", err
}
return master, rawMasterKey, nil
}
func (s *MasterService) ValidateMasterKey(masterKey string) (*model.Master, error) {
digest := tokenhash.HashToken(masterKey)
var master model.Master
if err := s.db.Where("master_key_digest = ?", digest).First(&master).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// Backward compatibility: look for legacy rows without digest.
var masters []model.Master
if err := s.db.Where("master_key_digest = '' OR master_key_digest IS NULL").Find(&masters).Error; err != nil {
return nil, err
}
for _, m := range masters {
if bcrypt.CompareHashAndPassword([]byte(m.MasterKey), []byte(masterKey)) == nil {
master = m
// Opportunistically backfill digest for next time.
if strings.TrimSpace(m.MasterKeyDigest) == "" {
_ = s.db.Model(&m).Update("master_key_digest", digest).Error
}
goto verified
}
}
return nil, errors.New("invalid master key")
}
if bcrypt.CompareHashAndPassword([]byte(master.MasterKey), []byte(masterKey)) != nil {
return nil, errors.New("invalid master key")
}
verified:
if master.Status != "active" {
return nil, fmt.Errorf("master is not active")
}
return &master, nil
}
func (s *MasterService) IssueChildKey(masterID uint, group string, scopes string) (*model.Key, string, error) {
return s.issueChildKey(masterID, group, scopes, "master")
}
func (s *MasterService) IssueChildKeyAsAdmin(masterID uint, group string, scopes string) (*model.Key, string, error) {
return s.issueChildKey(masterID, group, scopes, "admin")
}
func (s *MasterService) issueChildKey(masterID uint, group string, scopes string, issuedBy string) (*model.Key, string, error) {
var master model.Master
if err := s.db.First(&master, masterID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", fmt.Errorf("%w: %d", ErrMasterNotFound, masterID)
}
return nil, "", fmt.Errorf("load master: %w", err)
}
if master.Status != "active" {
return nil, "", fmt.Errorf("%w", ErrMasterNotActive)
}
group = strings.TrimSpace(group)
if group == "" {
group = master.Group
}
if group != master.Group {
return nil, "", fmt.Errorf("%w", ErrChildKeyGroupForbidden)
}
var count int64
s.db.Model(&model.Key{}).Where("master_id = ?", masterID).Count(&count)
if count >= int64(master.MaxChildKeys) {
return nil, "", fmt.Errorf("%w for master %d", ErrChildKeyLimitReached, masterID)
}
rawChildKey, err := generateRandomKey(32)
if err != nil {
return nil, "", fmt.Errorf("failed to generate child key: %w", err)
}
tokenHash := tokenhash.HashToken(rawChildKey)
hashedChildKey, err := bcrypt.GenerateFromPassword([]byte(rawChildKey), bcrypt.DefaultCost)
if err != nil {
return nil, "", fmt.Errorf("failed to hash child key: %w", err)
}
key := &model.Key{
MasterID: masterID,
KeySecret: string(hashedChildKey),
TokenHash: tokenHash,
Group: group,
Scopes: scopes,
DefaultNamespace: strings.TrimSpace(master.DefaultNamespace),
Namespaces: strings.TrimSpace(master.Namespaces),
IssuedAtEpoch: master.Epoch,
Status: "active",
IssuedBy: strings.TrimSpace(issuedBy),
}
if strings.TrimSpace(key.DefaultNamespace) == "" {
key.DefaultNamespace = "default"
}
if strings.TrimSpace(key.Namespaces) == "" {
key.Namespaces = key.DefaultNamespace
}
if key.IssuedBy == "" {
key.IssuedBy = "master"
}
if err := s.db.Create(key).Error; err != nil {
return nil, "", err
}
return key, rawChildKey, nil
}
func generateRandomKey(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}