Feat(go): add Signer interface + License Server for multi-customer deployments
This commit is contained in:
154
server/go/licensing/factory.go
Normal file
154
server/go/licensing/factory.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package licensing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Env var names — keep these in one place so README and code stay in sync.
|
||||
const (
|
||||
EnvSignPassword = "YAMA_SIGN_PASSWORD" // LocalSigner master HMAC key
|
||||
EnvLicenseServer = "YAMA_LICENSE_SERVER" // RemoteSigner: License Server base URL
|
||||
EnvLicenseToken = "YAMA_LICENSE_TOKEN" // RemoteSigner: customer JWT
|
||||
EnvLicensePubKeyPath = "YAMA_LICENSE_PUBLIC_KEY" // LocalSigner-as-LS: RSA public key PEM path
|
||||
EnvLicenseHTTPAddr = "YAMA_LICENSE_HTTP_ADDR" // LocalSigner-as-LS: listen address, e.g. ":8443"
|
||||
EnvLicenseOfflineHrs = "YAMA_LICENSE_OFFLINE_HRS" // RemoteSigner: cache TTL hours (default 24)
|
||||
)
|
||||
|
||||
// DefaultOfflineGrace mirrors the "24 hours" decision recorded in the
|
||||
// project memory's licensing design.
|
||||
const DefaultOfflineGrace = 24 * time.Hour
|
||||
|
||||
// Mode is what NewFromEnv inspected to pick a Signer; useful for logging.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
ModeLocal Mode = iota
|
||||
ModeRemote
|
||||
ModeNoOp
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case ModeLocal:
|
||||
return "local"
|
||||
case ModeRemote:
|
||||
return "remote"
|
||||
default:
|
||||
return "noop"
|
||||
}
|
||||
}
|
||||
|
||||
// SelectedMode reports which mode NewFromEnv picked based on env vars,
|
||||
// without actually constructing a signer. Useful for startup banners.
|
||||
func SelectedMode() Mode {
|
||||
if os.Getenv(EnvSignPassword) != "" {
|
||||
return ModeLocal
|
||||
}
|
||||
if os.Getenv(EnvLicenseServer) != "" && os.Getenv(EnvLicenseToken) != "" {
|
||||
return ModeRemote
|
||||
}
|
||||
return ModeNoOp
|
||||
}
|
||||
|
||||
// NewFromEnv builds the Signer chosen by env vars:
|
||||
// - YAMA_SIGN_PASSWORD set → LocalSigner
|
||||
// - YAMA_LICENSE_SERVER + YAMA_LICENSE_TOKEN set → RemoteSigner
|
||||
// - neither → NoOpSigner
|
||||
//
|
||||
// If both LocalSigner and RemoteSigner vars are set, LocalSigner wins
|
||||
// (an operator's own server should never accidentally call out to itself).
|
||||
// In that case we log a loud warning so the operator notices the dead config.
|
||||
//
|
||||
// <lg> may be nil — RemoteSigner falls back to a silent logger internally.
|
||||
func NewFromEnv(lg Logger) (Signer, Mode, error) {
|
||||
server := os.Getenv(EnvLicenseServer)
|
||||
token := os.Getenv(EnvLicenseToken)
|
||||
pwd := os.Getenv(EnvSignPassword)
|
||||
|
||||
if pwd != "" {
|
||||
if (server != "" || token != "") && lg != nil {
|
||||
lg.Warn("%s is set; ignoring %s/%s (LocalSigner wins) — "+
|
||||
"if you meant to be a customer deployment, unset %s",
|
||||
EnvSignPassword, EnvLicenseServer, EnvLicenseToken, EnvSignPassword)
|
||||
}
|
||||
s, err := NewLocal(pwd)
|
||||
if err != nil {
|
||||
return nil, ModeNoOp, err
|
||||
}
|
||||
return s, ModeLocal, nil
|
||||
}
|
||||
|
||||
if server != "" && token != "" {
|
||||
if err := ValidateRemoteURL(server); err != nil {
|
||||
return nil, ModeNoOp, fmt.Errorf("%s rejected: %w", EnvLicenseServer, err)
|
||||
}
|
||||
grace := DefaultOfflineGrace
|
||||
if hrs := os.Getenv(EnvLicenseOfflineHrs); hrs != "" {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(hrs))
|
||||
if err != nil {
|
||||
return nil, ModeNoOp, fmt.Errorf(
|
||||
"%s must be an integer (hours), got %q", EnvLicenseOfflineHrs, hrs)
|
||||
}
|
||||
if n < 0 {
|
||||
return nil, ModeNoOp, fmt.Errorf(
|
||||
"%s must be >= 0, got %d", EnvLicenseOfflineHrs, n)
|
||||
}
|
||||
grace = time.Duration(n) * time.Hour
|
||||
}
|
||||
return NewRemote(server, token, grace, lg), ModeRemote, nil
|
||||
}
|
||||
|
||||
if server != "" || token != "" {
|
||||
// Partial config is almost certainly a misconfiguration — fail loudly
|
||||
// rather than silently degrading to NoOp.
|
||||
return nil, ModeNoOp, fmt.Errorf(
|
||||
"%s and %s must be set together (got %s=%q %s=%q)",
|
||||
EnvLicenseServer, EnvLicenseToken,
|
||||
EnvLicenseServer, server, EnvLicenseToken, token)
|
||||
}
|
||||
|
||||
return NewNoOp(), ModeNoOp, nil
|
||||
}
|
||||
|
||||
// LicenseServerFromEnv builds the License Server HTTP handler if (and only
|
||||
// if) the operator wants to expose it: requires both YAMA_LICENSE_PUBLIC_KEY
|
||||
// (to verify customer JWTs) and YAMA_LICENSE_HTTP_ADDR (listen address) set,
|
||||
// AND the Signer must be a *LocalSigner (RemoteSigner customer deployments
|
||||
// must never serve a License Server — they don't have the master key).
|
||||
//
|
||||
// Returns (nil, "", nil) if not configured. Returns (server, addr, nil) when
|
||||
// configured. Returns error on bad / partial config.
|
||||
func LicenseServerFromEnv(signer Signer, lg Logger) (*LicenseServer, string, error) {
|
||||
pubPath := os.Getenv(EnvLicensePubKeyPath)
|
||||
addr := os.Getenv(EnvLicenseHTTPAddr)
|
||||
|
||||
if pubPath == "" && addr == "" {
|
||||
return nil, "", nil // not configured — fine
|
||||
}
|
||||
if pubPath == "" || addr == "" {
|
||||
return nil, "", fmt.Errorf(
|
||||
"%s and %s must be set together to enable License Server (got %s=%q %s=%q)",
|
||||
EnvLicensePubKeyPath, EnvLicenseHTTPAddr,
|
||||
EnvLicensePubKeyPath, pubPath, EnvLicenseHTTPAddr, addr)
|
||||
}
|
||||
|
||||
local, ok := signer.(*LocalSigner)
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf(
|
||||
"License Server requires LocalSigner mode (set %s); current mode is %s",
|
||||
EnvSignPassword, signer.Mode())
|
||||
}
|
||||
|
||||
pubKey, err := LoadRSAPublicKey(pubPath)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 5-minute eviction window — twice a typical heartbeat interval. Matches
|
||||
// the discussion in quota.go.
|
||||
return NewLicenseServer(local, pubKey, 5*time.Minute, lg), addr, nil
|
||||
}
|
||||
Reference in New Issue
Block a user