335 lines
9.9 KiB
Go
335 lines
9.9 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
SharedPublicTenantEdition = "shared_public"
|
|
TenantPrivateEdition = "tenant_private"
|
|
|
|
TenantMembershipRoleAdmin = "admin"
|
|
TenantMembershipRoleUser = "user"
|
|
|
|
TenantDomainKindGenerated = "generated"
|
|
TenantDomainKindCustom = "custom"
|
|
|
|
TenantDomainStatusPending = "pending"
|
|
TenantDomainStatusVerified = "verified"
|
|
|
|
XWorkmateProfileScopeTenantShared = "tenant-shared"
|
|
XWorkmateProfileScopeUserPrivate = "user-private"
|
|
|
|
XWorkmateSecretLocatorProviderVault = "vault"
|
|
|
|
XWorkmateSecretLocatorTargetBridgeAuthToken = "bridge.auth_token"
|
|
XWorkmateSecretLocatorTargetOpenclawGatewayToken = XWorkmateSecretLocatorTargetBridgeAuthToken
|
|
XWorkmateSecretLocatorTargetVaultRootToken = "vault.root_token"
|
|
XWorkmateSecretLocatorTargetAIGatewayAccessToken = "ai_gateway.access_token"
|
|
XWorkmateSecretLocatorTargetOllamaCloudAPIKey = "ollama_cloud.api_key"
|
|
|
|
SharedXWorkmateTenantID = "svc-plus-xworkmate"
|
|
SharedXWorkmateTenantName = "svc.plus XWorkmate"
|
|
SharedXWorkmateDomain = "svc.plus"
|
|
)
|
|
|
|
var (
|
|
ErrTenantNotFound = errors.New("tenant not found")
|
|
ErrTenantMembershipNotFound = errors.New("tenant membership not found")
|
|
ErrXWorkmateProfileNotFound = errors.New("xworkmate profile not found")
|
|
)
|
|
|
|
type Tenant struct {
|
|
ID string
|
|
Name string
|
|
Edition string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type TenantDomain struct {
|
|
ID string
|
|
TenantID string
|
|
Domain string
|
|
Kind string
|
|
IsPrimary bool
|
|
Status string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type TenantMembership struct {
|
|
TenantID string
|
|
UserID string
|
|
Role string
|
|
TenantName string
|
|
TenantEdition string
|
|
Domain string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type XWorkmateProfile struct {
|
|
ID string
|
|
TenantID string
|
|
UserID string
|
|
Scope string
|
|
BridgeServerURL string
|
|
BridgeServerOrigin string
|
|
VaultURL string
|
|
VaultNamespace string
|
|
VaultSecretPath string
|
|
VaultSecretKey string
|
|
SecretLocators []XWorkmateSecretLocator
|
|
ApisixURL string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type XWorkmateSecretLocator struct {
|
|
ID string `json:"id"`
|
|
Provider string `json:"provider"`
|
|
SecretPath string `json:"secretPath"`
|
|
SecretKey string `json:"secretKey"`
|
|
Target string `json:"target"`
|
|
Required bool `json:"required"`
|
|
}
|
|
|
|
func NormalizeTenantEdition(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case SharedPublicTenantEdition:
|
|
return SharedPublicTenantEdition
|
|
default:
|
|
return TenantPrivateEdition
|
|
}
|
|
}
|
|
|
|
func NormalizeTenantMembershipRole(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case TenantMembershipRoleAdmin:
|
|
return TenantMembershipRoleAdmin
|
|
default:
|
|
return TenantMembershipRoleUser
|
|
}
|
|
}
|
|
|
|
func NormalizeTenantDomainKind(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case TenantDomainKindCustom:
|
|
return TenantDomainKindCustom
|
|
default:
|
|
return TenantDomainKindGenerated
|
|
}
|
|
}
|
|
|
|
func NormalizeTenantDomainStatus(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case TenantDomainStatusPending:
|
|
return TenantDomainStatusPending
|
|
default:
|
|
return TenantDomainStatusVerified
|
|
}
|
|
}
|
|
|
|
func NormalizeXWorkmateProfileScope(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case XWorkmateProfileScopeTenantShared:
|
|
return XWorkmateProfileScopeTenantShared
|
|
default:
|
|
return XWorkmateProfileScopeUserPrivate
|
|
}
|
|
}
|
|
|
|
func NormalizeXWorkmateSecretLocator(locator *XWorkmateSecretLocator) {
|
|
if locator == nil {
|
|
return
|
|
}
|
|
|
|
locator.ID = strings.TrimSpace(locator.ID)
|
|
if locator.ID == "" {
|
|
locator.ID = uuid.NewString()
|
|
}
|
|
locator.Provider = strings.ToLower(strings.TrimSpace(locator.Provider))
|
|
if locator.Provider == "" {
|
|
locator.Provider = XWorkmateSecretLocatorProviderVault
|
|
}
|
|
locator.SecretPath = strings.Trim(strings.TrimSpace(locator.SecretPath), "/")
|
|
locator.SecretKey = strings.TrimSpace(locator.SecretKey)
|
|
locator.Target = strings.ToLower(strings.TrimSpace(locator.Target))
|
|
}
|
|
|
|
func cloneXWorkmateSecretLocators(locators []XWorkmateSecretLocator) []XWorkmateSecretLocator {
|
|
if len(locators) == 0 {
|
|
return []XWorkmateSecretLocator{}
|
|
}
|
|
|
|
cloned := make([]XWorkmateSecretLocator, len(locators))
|
|
copy(cloned, locators)
|
|
return cloned
|
|
}
|
|
|
|
func legacyXWorkmateSecretLocatorID(profile *XWorkmateProfile) string {
|
|
if profile == nil {
|
|
return "legacy|xworkmate|bridge.auth_token"
|
|
}
|
|
|
|
return strings.Join([]string{
|
|
"legacy",
|
|
strings.TrimSpace(profile.TenantID),
|
|
strings.TrimSpace(profile.UserID),
|
|
NormalizeXWorkmateProfileScope(profile.Scope),
|
|
XWorkmateSecretLocatorTargetBridgeAuthToken,
|
|
}, "|")
|
|
}
|
|
|
|
func synthesizeXWorkmateSecretLocatorFromLegacy(profile *XWorkmateProfile) XWorkmateSecretLocator {
|
|
return XWorkmateSecretLocator{
|
|
ID: legacyXWorkmateSecretLocatorID(profile),
|
|
Provider: XWorkmateSecretLocatorProviderVault,
|
|
SecretPath: profile.VaultSecretPath,
|
|
SecretKey: profile.VaultSecretKey,
|
|
Target: XWorkmateSecretLocatorTargetBridgeAuthToken,
|
|
}
|
|
}
|
|
|
|
func compatibilityXWorkmateSecretLocator(locators []XWorkmateSecretLocator) (string, string, bool) {
|
|
for _, locator := range locators {
|
|
if locator.Target == XWorkmateSecretLocatorTargetBridgeAuthToken &&
|
|
locator.SecretPath != "" && locator.SecretKey != "" {
|
|
return locator.SecretPath, locator.SecretKey, true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func NormalizeHostname(value string) string {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
if comma := strings.Index(trimmed, ","); comma >= 0 {
|
|
trimmed = strings.TrimSpace(trimmed[:comma])
|
|
}
|
|
|
|
if parsed, err := url.Parse(trimmed); err == nil && parsed.Host != "" {
|
|
trimmed = parsed.Host
|
|
}
|
|
|
|
if host, _, err := net.SplitHostPort(trimmed); err == nil {
|
|
trimmed = host
|
|
}
|
|
|
|
return strings.Trim(strings.ToLower(trimmed), ".")
|
|
}
|
|
|
|
func IsSharedTenantHost(host string) bool {
|
|
normalized := NormalizeHostname(host)
|
|
if normalized == "" {
|
|
return true
|
|
}
|
|
switch normalized {
|
|
case "svc.plus", "www.svc.plus", "console.svc.plus", "localhost", "127.0.0.1", "[::1]":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func GenerateRandomTenantDomain() (string, error) {
|
|
buffer := make([]byte, 4)
|
|
if _, err := rand.Read(buffer); err != nil {
|
|
return "", fmt.Errorf("generate tenant domain: %w", err)
|
|
}
|
|
return fmt.Sprintf("xw-%s.svc.plus", hex.EncodeToString(buffer)), nil
|
|
}
|
|
|
|
func NormalizeTenant(tenant *Tenant) {
|
|
if tenant == nil {
|
|
return
|
|
}
|
|
tenant.ID = strings.TrimSpace(tenant.ID)
|
|
tenant.Name = strings.TrimSpace(tenant.Name)
|
|
tenant.Edition = NormalizeTenantEdition(tenant.Edition)
|
|
}
|
|
|
|
func NormalizeTenantDomain(domain *TenantDomain) {
|
|
if domain == nil {
|
|
return
|
|
}
|
|
domain.ID = strings.TrimSpace(domain.ID)
|
|
domain.TenantID = strings.TrimSpace(domain.TenantID)
|
|
domain.Domain = NormalizeHostname(domain.Domain)
|
|
domain.Kind = NormalizeTenantDomainKind(domain.Kind)
|
|
domain.Status = NormalizeTenantDomainStatus(domain.Status)
|
|
}
|
|
|
|
func NormalizeTenantMembership(membership *TenantMembership) {
|
|
if membership == nil {
|
|
return
|
|
}
|
|
membership.TenantID = strings.TrimSpace(membership.TenantID)
|
|
membership.UserID = strings.TrimSpace(membership.UserID)
|
|
membership.Role = NormalizeTenantMembershipRole(membership.Role)
|
|
membership.TenantName = strings.TrimSpace(membership.TenantName)
|
|
membership.TenantEdition = NormalizeTenantEdition(membership.TenantEdition)
|
|
membership.Domain = NormalizeHostname(membership.Domain)
|
|
}
|
|
|
|
func NormalizeXWorkmateProfile(profile *XWorkmateProfile) {
|
|
if profile == nil {
|
|
return
|
|
}
|
|
profile.ID = strings.TrimSpace(profile.ID)
|
|
profile.TenantID = strings.TrimSpace(profile.TenantID)
|
|
profile.UserID = strings.TrimSpace(profile.UserID)
|
|
profile.Scope = NormalizeXWorkmateProfileScope(profile.Scope)
|
|
profile.BridgeServerURL = strings.TrimSpace(profile.BridgeServerURL)
|
|
profile.BridgeServerOrigin = strings.TrimSpace(profile.BridgeServerOrigin)
|
|
profile.VaultURL = strings.TrimSpace(profile.VaultURL)
|
|
profile.VaultNamespace = strings.TrimSpace(profile.VaultNamespace)
|
|
profile.VaultSecretPath = strings.Trim(strings.TrimSpace(profile.VaultSecretPath), "/")
|
|
profile.VaultSecretKey = strings.TrimSpace(profile.VaultSecretKey)
|
|
profile.SecretLocators = cloneXWorkmateSecretLocators(profile.SecretLocators)
|
|
for i := range profile.SecretLocators {
|
|
NormalizeXWorkmateSecretLocator(&profile.SecretLocators[i])
|
|
}
|
|
if len(profile.SecretLocators) == 0 && profile.VaultSecretPath != "" && profile.VaultSecretKey != "" {
|
|
profile.SecretLocators = []XWorkmateSecretLocator{synthesizeXWorkmateSecretLocatorFromLegacy(profile)}
|
|
}
|
|
if (profile.VaultSecretPath == "" || profile.VaultSecretKey == "") && len(profile.SecretLocators) > 0 {
|
|
if secretPath, secretKey, ok := compatibilityXWorkmateSecretLocator(profile.SecretLocators); ok {
|
|
if profile.VaultSecretPath == "" {
|
|
profile.VaultSecretPath = secretPath
|
|
}
|
|
if profile.VaultSecretKey == "" {
|
|
profile.VaultSecretKey = secretKey
|
|
}
|
|
}
|
|
}
|
|
if profile.SecretLocators == nil {
|
|
profile.SecretLocators = []XWorkmateSecretLocator{}
|
|
}
|
|
profile.ApisixURL = strings.TrimSpace(profile.ApisixURL)
|
|
}
|
|
|
|
type TenantResolver interface {
|
|
EnsureTenant(ctx context.Context, tenant *Tenant) error
|
|
EnsureTenantDomain(ctx context.Context, domain *TenantDomain) error
|
|
UpsertTenantMembership(ctx context.Context, membership *TenantMembership) error
|
|
ResolveTenantByHost(ctx context.Context, host string) (*Tenant, *TenantDomain, error)
|
|
ListTenantMembershipsByUser(ctx context.Context, userID string) ([]TenantMembership, error)
|
|
GetTenantMembership(ctx context.Context, tenantID, userID string) (*TenantMembership, error)
|
|
GetXWorkmateProfile(ctx context.Context, tenantID, userID, scope string) (*XWorkmateProfile, error)
|
|
UpsertXWorkmateProfile(ctx context.Context, profile *XWorkmateProfile) error
|
|
}
|