mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
fix(go): resync tenant id on license change; show license error on any error while license validation;
This commit is contained in:
@@ -10,9 +10,9 @@ import (
|
||||
"github.com/gravitl/netmaker/scope"
|
||||
)
|
||||
|
||||
var SyncOrgAndTenants = CreateLocalDefaults
|
||||
var MigrateOrgAndTenants = migrateOrgAndTenants
|
||||
|
||||
func CreateLocalDefaults(ctx context.Context) error {
|
||||
func migrateOrgAndTenants(ctx context.Context) error {
|
||||
org, err := EnsureLocalOrganization(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestToSQLSchema_StuckServerCompletesV160AndV170(t *testing.T) {
|
||||
ctx := setupMigrationTest(t)
|
||||
networkName, nodeID := seedLegacyKVData(t, ctx)
|
||||
|
||||
require.NoError(t, CreateLocalDefaults(ctx))
|
||||
require.NoError(t, migrateOrgAndTenants(ctx))
|
||||
markMigrationJobComplete(t, ctx, "migration-multitenancy")
|
||||
markMigrationJobComplete(t, ctx, "migration-v1.5.1")
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestToSQLSchema_StuckServerCompletesV160AndV170(t *testing.T) {
|
||||
func TestToSQLSchema_SkipsCompletedPreMTJobs(t *testing.T) {
|
||||
ctx := setupMigrationTest(t)
|
||||
|
||||
require.NoError(t, CreateLocalDefaults(ctx))
|
||||
require.NoError(t, migrateOrgAndTenants(ctx))
|
||||
markMigrationJobComplete(t, ctx, "migration-multitenancy")
|
||||
markMigrationJobComplete(t, ctx, "migration-v1.5.1")
|
||||
markMigrationJobComplete(t, ctx, "migration-v1.6.0")
|
||||
@@ -200,19 +200,19 @@ func TestToSQLSchema_SkipsCompletedPreMTJobs(t *testing.T) {
|
||||
require.Len(t, tenants, 1)
|
||||
}
|
||||
|
||||
// TestMigrateV1_7_0_UsesSyncOrgAndTenantsHook ensures v1.7.0 step 0 goes through
|
||||
// SyncOrgAndTenants (EE overrides this with license sync) rather than always
|
||||
// calling CreateLocalDefaults, which breaks MSP installs that need multiple
|
||||
// TestMigrateV1_7_0_UsesMigrateOrgAndTenantsHook ensures v1.7.0 step 0 goes through
|
||||
// MigrateOrgAndTenants (EE overrides this with license sync) rather than always
|
||||
// calling migrateOrgAndTenants, which breaks MSP installs that need multiple
|
||||
// tenants from the account server.
|
||||
func TestMigrateV1_7_0_UsesSyncOrgAndTenantsHook(t *testing.T) {
|
||||
func TestMigrateV1_7_0_UsesMigrateOrgAndTenantsHook(t *testing.T) {
|
||||
ctx := setupMigrationTest(t)
|
||||
markMigrationJobComplete(t, ctx, migrationJobV160)
|
||||
|
||||
orig := SyncOrgAndTenants
|
||||
t.Cleanup(func() { SyncOrgAndTenants = orig })
|
||||
orig := MigrateOrgAndTenants
|
||||
t.Cleanup(func() { MigrateOrgAndTenants = orig })
|
||||
|
||||
called := false
|
||||
SyncOrgAndTenants = func(ctx context.Context) error {
|
||||
MigrateOrgAndTenants = func(ctx context.Context) error {
|
||||
called = true
|
||||
org := &schema.Organization{
|
||||
ID: "license-org-id",
|
||||
@@ -231,7 +231,7 @@ func TestMigrateV1_7_0_UsesSyncOrgAndTenantsHook(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(t, migrateV1_7_0(ctx))
|
||||
assert.True(t, called, "expected SyncOrgAndTenants hook to run")
|
||||
assert.True(t, called, "expected MigrateOrgAndTenants hook to run")
|
||||
|
||||
tenants, err := (&schema.Tenant{}).List(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -35,10 +35,10 @@ const (
|
||||
|
||||
func migrateV1_7_0(ctx context.Context) error {
|
||||
// Step 0: bootstrap org/tenants (was migration-multitenancy).
|
||||
// Goes through SyncOrgAndTenants so EE/MSP can sync from license validation
|
||||
// Goes through MigrateOrgAndTenants so EE/MSP can sync from license validation
|
||||
// instead of always creating a local UUID default tenant. CE keeps the
|
||||
// CreateLocalDefaults default; idempotent when org/tenant already exist.
|
||||
if err := SyncOrgAndTenants(ctx); err != nil {
|
||||
// migrateOrgAndTenants default; idempotent when org/tenant already exist.
|
||||
if err := MigrateOrgAndTenants(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ func InitPro() {
|
||||
proControllers.IntegrationHandlers,
|
||||
)
|
||||
controller.ListRoles = proControllers.ListRoles
|
||||
migrate.SyncOrgAndTenants = license.SyncOrgAndTenants
|
||||
migrate.MigrateOrgAndTenants = license.MigrateOrgAndTenants
|
||||
servercfg.ErrLicenseValidation = license.ErrLicenseValidation
|
||||
logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
logger.Log(0, "starting license checker")
|
||||
|
||||
@@ -9,11 +9,6 @@ import (
|
||||
"github.com/gravitl/netmaker/schema"
|
||||
)
|
||||
|
||||
func hasCachedResponse(ctx context.Context) bool {
|
||||
cached := &schema.Internal{Key: schema.InternalKey_LicenseValidationCachedResponse}
|
||||
return cached.Get(ctx) == nil
|
||||
}
|
||||
|
||||
var cachedResponse atomic.Pointer[ValidatedLicense]
|
||||
|
||||
func cacheResponse(ctx context.Context, response []byte) error {
|
||||
|
||||
+51
-70
@@ -42,21 +42,22 @@ func AddLicenseHooks() {
|
||||
}
|
||||
}
|
||||
|
||||
func SyncOrgAndTenants(ctx context.Context) error {
|
||||
licenseResponse, isCachedResp, err := fetchValidatedLicense(ctx)
|
||||
func MigrateOrgAndTenants(ctx context.Context) error {
|
||||
if err := clearCachedResponse(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the cache was just cleared, so the response is never a cached one.
|
||||
licenseResponse, _, err := fetchValidatedLicense(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isCachedResp {
|
||||
return nil
|
||||
}
|
||||
|
||||
return syncOrgAndTenantsFromResponse(ctx, licenseResponse, false)
|
||||
return syncOrgAndTenantsFromResponse(ctx, licenseResponse)
|
||||
}
|
||||
|
||||
func syncOrgAndTenantsFromResponse(ctx context.Context, licenseResponse ValidatedLicense, hadCache bool) error {
|
||||
if hadCache {
|
||||
func syncOrgAndTenantsFromResponse(ctx context.Context, licenseResponse ValidatedLicense) error {
|
||||
if licenseResponse.Organization.ID != "" {
|
||||
return upsertOrgAndTenants(ctx, licenseResponse)
|
||||
}
|
||||
return reconcileOrgAndTenants(ctx, licenseResponse)
|
||||
@@ -84,15 +85,6 @@ func upsertOrgAndTenants(ctx context.Context, licenseResponse ValidatedLicense)
|
||||
}
|
||||
|
||||
func upsertOrganization(ctx context.Context, licenseOrg LicenseOrg) (string, error) {
|
||||
if licenseOrg.ID == "" {
|
||||
// single-tenant PRO: the license carries no organization.
|
||||
org, err := migrate.EnsureLocalOrganization(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return org.ID, nil
|
||||
}
|
||||
|
||||
org := &schema.Organization{ID: licenseOrg.ID}
|
||||
err := org.Get(ctx)
|
||||
if err != nil {
|
||||
@@ -160,62 +152,20 @@ func teardownDeletedTenant(ctx context.Context, tenantID string) error {
|
||||
}
|
||||
|
||||
func reconcileOrgAndTenants(ctx context.Context, licenseResponse ValidatedLicense) error {
|
||||
orgID, err := reconcileOrganization(ctx, licenseResponse.Organization)
|
||||
// non-MSP: the license carries no organization, so the local one is used.
|
||||
org, err := migrate.EnsureLocalOrganization(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconcile organization: %w", err)
|
||||
}
|
||||
|
||||
if err := reconcileTenants(ctx, licenseResponse.Tenants, orgID); err != nil {
|
||||
if err := reconcileTenants(ctx, licenseResponse.Tenants, org.ID); err != nil {
|
||||
return fmt.Errorf("failed to reconcile tenants: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func reconcileOrganization(ctx context.Context, licenseOrg LicenseOrg) (string, error) {
|
||||
if licenseOrg.ID == "" {
|
||||
org, err := migrate.EnsureLocalOrganization(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return org.ID, nil
|
||||
}
|
||||
|
||||
orgs, err := (&schema.Organization{}).ListAll(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch len(orgs) {
|
||||
case 0:
|
||||
org := &schema.Organization{ID: licenseOrg.ID, Name: licenseOrg.Name, Metadata: licenseOrg.Metadata}
|
||||
if err := org.Create(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return org.ID, nil
|
||||
case 1:
|
||||
existing := orgs[0]
|
||||
if existing.ID != licenseOrg.ID {
|
||||
if err := migrate.RekeyOrganization(ctx, existing.ID, licenseOrg.ID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
org := &schema.Organization{ID: licenseOrg.ID, Name: licenseOrg.Name, Metadata: licenseOrg.Metadata}
|
||||
if err := org.Update(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return licenseOrg.ID, nil
|
||||
default:
|
||||
return "", fmt.Errorf("cannot reconcile license organization: %d local organizations already exist", len(orgs))
|
||||
}
|
||||
}
|
||||
|
||||
func reconcileTenants(ctx context.Context, licenseTenants []LicenseTenant, orgID string) error {
|
||||
existing, err := (&schema.Tenant{}).List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activeTenants := make([]LicenseTenant, 0, len(licenseTenants))
|
||||
for _, licenseTenant := range licenseTenants {
|
||||
if licenseTenant.Status == TenantStatusDeleted {
|
||||
@@ -227,6 +177,12 @@ func reconcileTenants(ctx context.Context, licenseTenants []LicenseTenant, orgID
|
||||
activeTenants = append(activeTenants, licenseTenant)
|
||||
}
|
||||
|
||||
// list after teardown so a tenant that was just removed isn't counted.
|
||||
existing, err := (&schema.Tenant{}).List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch len(existing) {
|
||||
case 0:
|
||||
// nothing pre-existing locally -- nothing to rekey.
|
||||
@@ -333,14 +289,16 @@ func getLicensePublicKey(licensePubKeyEncoded string) (*[32]byte, error) {
|
||||
// if license is free_tier and limits exceeds, then function should error
|
||||
// if license is not valid, function should error
|
||||
func ValidateLicense(ctx context.Context, clearCache bool) (err error) {
|
||||
var isCachedResp bool
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %s", errValidation, err.Error())
|
||||
setLicenseInvalidErr(err)
|
||||
} else if !isCachedResp {
|
||||
licenseInvalidErr.Store(nil)
|
||||
}
|
||||
}()
|
||||
|
||||
hadCache := hasCachedResponse(ctx)
|
||||
|
||||
if clearCache {
|
||||
err = clearCachedResponse(ctx)
|
||||
if err != nil {
|
||||
@@ -356,7 +314,7 @@ func ValidateLicense(ctx context.Context, clearCache bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = syncOrgAndTenantsFromResponse(ctx, licenseResponse, hadCache); err != nil {
|
||||
if err = syncOrgAndTenantsFromResponse(ctx, licenseResponse); err != nil {
|
||||
err = fmt.Errorf("failed to sync organization/tenants from license: %w", err)
|
||||
return err
|
||||
}
|
||||
@@ -381,6 +339,14 @@ func ValidateLicense(ctx context.Context, clearCache bool) (err error) {
|
||||
}
|
||||
|
||||
func fetchValidatedLicense(ctx context.Context) (licenseResponse ValidatedLicense, isCachedResp bool, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
setLicenseInvalidErr(err)
|
||||
} else if !isCachedResp {
|
||||
licenseInvalidErr.Store(nil)
|
||||
}
|
||||
}()
|
||||
|
||||
licenseKeyValue := servercfg.GetLicenseKey()
|
||||
netmakerTenantID := servercfg.GetNetmakerTenantID()
|
||||
slog.Info("proceeding with Netmaker license validation...")
|
||||
@@ -430,6 +396,11 @@ func fetchValidatedLicense(ctx context.Context) (licenseResponse ValidatedLicens
|
||||
validationResponse, apiErr := callLicenseValidationApi(ctx, encryptedData, tempPubKey)
|
||||
if apiErr != nil {
|
||||
slog.Warn("failed to validate license key, falling back to cached response", "error", apiErr)
|
||||
var rejected *licenseRejectedError
|
||||
if errors.As(apiErr, &rejected) {
|
||||
// a rejected license stays invalid even if a cached response exists.
|
||||
setLicenseInvalidErr(rejected)
|
||||
}
|
||||
validationResponse, err = getCachedResponseFromDB(ctx)
|
||||
if err != nil {
|
||||
return licenseResponse, false, fmt.Errorf("failed to validate license key: %w", apiErr)
|
||||
@@ -483,8 +454,21 @@ type licenseErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// licenseRejectedError is returned when the validation backend rejects the license.
|
||||
type licenseRejectedError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *licenseRejectedError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
var licenseInvalidErr atomic.Pointer[error]
|
||||
|
||||
func setLicenseInvalidErr(err error) {
|
||||
licenseInvalidErr.Store(&err)
|
||||
}
|
||||
|
||||
func callLicenseValidationApi(ctx context.Context, encryptedData []byte, publicKey *[32]byte) ([]byte, error) {
|
||||
publicKeyBytes, err := ncutils.ConvertKeyToBytes(publicKey)
|
||||
if err != nil {
|
||||
@@ -537,14 +521,11 @@ func callLicenseValidationApi(ctx context.Context, encryptedData []byte, publicK
|
||||
var errResp licenseErrorResponse
|
||||
jsonErr := json.Unmarshal(body, &errResp)
|
||||
if jsonErr == nil && errResp.Code == http.StatusUnauthorized {
|
||||
invalidErr := errors.New(errResp.Message)
|
||||
licenseInvalidErr.Store(&invalidErr)
|
||||
return nil, &licenseRejectedError{message: errResp.Message}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not validate license with validation backend (status={%d})", resp.StatusCode)
|
||||
}
|
||||
|
||||
licenseInvalidErr.Store(nil)
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user