diff --git a/cmd/cli/license/inspect.go b/cmd/cli/license/inspect.go new file mode 100644 index 0000000000..c12417f87f --- /dev/null +++ b/cmd/cli/license/inspect.go @@ -0,0 +1,137 @@ +package license + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags" + "github.com/bacalhau-project/bacalhau/cmd/util/hook" + "github.com/bacalhau-project/bacalhau/cmd/util/output" + "github.com/bacalhau-project/bacalhau/pkg/lib/collections" + licensepkg "github.com/bacalhau-project/bacalhau/pkg/lib/license" +) + +type InspectOptions struct { + LicenseFile string + OutputOpts output.OutputOptions +} + +func NewInspectOptions() *InspectOptions { + return &InspectOptions{ + OutputOpts: output.OutputOptions{Format: output.TableFormat}, + } +} + +// Add this struct after the LicenseInfo struct +type licenseFile struct { + License string `json:"license"` +} + +func NewInspectCmd() *cobra.Command { + o := NewInspectOptions() + cmd := &cobra.Command{ + Use: "inspect [path]", + Short: "Inspect license information", + Args: cobra.ExactArgs(1), + PreRun: hook.ApplyPorcelainLogLevel, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Get the license file path from args + o.LicenseFile = args[0] + + // Check if license file path is empty or just whitespace + if len(strings.TrimSpace(o.LicenseFile)) == 0 { + return fmt.Errorf("license file path cannot be empty") + } + + // Check if license file exists + if _, err := os.Stat(o.LicenseFile); os.IsNotExist(err) { + return fmt.Errorf("file not found: %s", o.LicenseFile) + } + return o.Run(cmd.Context(), cmd) + }, + } + + // Add output format flags only + cmd.Flags().AddFlagSet(cliflags.OutputFormatFlags(&o.OutputOpts)) + + return cmd +} + +func (o *InspectOptions) Run(ctx context.Context, cmd *cobra.Command) error { + // Read the license file + data, err := os.ReadFile(o.LicenseFile) + if err != nil { + return fmt.Errorf("failed to read license file: %w", err) + } + + // Parse the license file + var license licenseFile + if err := json.Unmarshal(data, &license); err != nil { + return fmt.Errorf("failed to parse license file: %w", err) + } + + // Create offline license validator + validator, err := licensepkg.NewOfflineLicenseValidator() + if err != nil { + return fmt.Errorf("failed to create license validator: %w", err) + } + + // Validate the license token + claims, err := validator.ValidateToken(license.License) + if err != nil { + return fmt.Errorf("invalid license: %w", err) + } + + // For JSON/YAML output + if o.OutputOpts.Format == output.JSONFormat || o.OutputOpts.Format == output.YAMLFormat { + return output.OutputOne(cmd, nil, o.OutputOpts, claims) + } + + // Create header data pairs for key-value output + headerData := []collections.Pair[string, any]{ + {Left: "Product", Right: claims.Product}, + {Left: "License ID", Right: claims.LicenseID}, + {Left: "Customer ID", Right: claims.CustomerID}, + {Left: "Valid Until", Right: claims.ExpiresAt.Format(time.DateOnly)}, + {Left: "Version", Right: claims.LicenseVersion}, + } + + // Always show Capabilities + capabilitiesStr := "{}" + if len(claims.Capabilities) > 0 { + var caps []string + for k, v := range claims.Capabilities { + caps = append(caps, fmt.Sprintf("%s=%s", k, v)) + } + capabilitiesStr = strings.Join(caps, ", ") + } + headerData = append(headerData, collections.Pair[string, any]{ + Left: "Capabilities", + Right: capabilitiesStr, + }) + + // Always show Metadata + metadataStr := "{}" + if len(claims.Metadata) > 0 { + var meta []string + for k, v := range claims.Metadata { + meta = append(meta, fmt.Sprintf("%s=%s", k, v)) + } + metadataStr = strings.Join(meta, ", ") + } + headerData = append(headerData, collections.Pair[string, any]{ + Left: "Metadata", + Right: metadataStr, + }) + + output.KeyValue(cmd, headerData) + return nil +} diff --git a/cmd/cli/license/inspect_test.go b/cmd/cli/license/inspect_test.go new file mode 100644 index 0000000000..4ad7a55d8c --- /dev/null +++ b/cmd/cli/license/inspect_test.go @@ -0,0 +1,476 @@ +//go:build unit || !integration + +package license + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "os" + "path/filepath" + + "encoding/json" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// cSpell:disable +const validOfficialTestLicense = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtHok9tG2OV5ybSZoxFDQW3iqiCg6TqMwCAa6C6MBXtLnv-NP1H9Ytg" + +const validOfficialTestLicenseWithMetadata = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6IjJkNThjN2M5LWVjMjktNDVhNS1hNWNkLWNiOGY3ZmVlNjY3OCIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6eyJzb21lTWV0YWRhdGEiOiJ2YWx1ZU9mU29tZU1ldGFkYXRhIn0sImlhdCI6MTczNjg4OTY4MiwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODg5NjgyLCJqdGkiOiIyZDU4YzdjOS1lYzI5LTQ1YTUtYTVjZC1jYjhmN2ZlZTY2NzgifQ.LDjEcSkGBHT6cHazgYYmviX6jxUPcEzVrkiyJ1QCgwdAswWusC2gWE-H7vu6X4rFFYV8hjycS2oJjaVLm4hLyGNvHPzRedIshGWM5j4GxoQ-p7ulf1HQErVMj5xzJzoyM0IwXN4Vb6h6AxNwYoey948Bduk--DeYBbMVwQAXyZeyb_A1jZeR3JLf1lQhoe6-cjmTnVMCNyzisZqHGYWpXHDYQcqSOm3FvPrBPsP4bVCZSU0pGQBu8lb9A3KhJRobvqNF4YseSz7fFkpuRR3sI7p4zthO6aEk7sXKF0LBU9G1AEdCn5S0gB-7_uFUuH_JQi8bhvXeWvC1dqdQLBzYnA" + +const officialTestTokenWithWrongSignature = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.iambadsignature" + +const officialTokenButExpired = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6IjBkZDA0Yzg0LTA5YjgtNDE3OS04OGY3LWM3MmE5ZDU2YzBhMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6eyJzb21lTWV0YWRhdGEiOiJ2YWx1ZU9mU29tZU1ldGFkYXRhIn0sImlhdCI6MTczNjg5MTEzMSwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoxNzM2MjQxMDk4LCJqdGkiOiIwZGQwNGM4NC0wOWI4LTQxNzktODhmNy1jNzJhOWQ1NmMwYTIifQ.URD1ofoJwrleEkXWQ7vWVv_gCzwM-1cR6_6SOIf-d7Uuh3ttFJdNMDw_gbZp65sgLMycQKkm5ngooxK-FSwVj6jl2c70SvzuEHbdUsSZClLReOSbmY7CO6bOQYzQYVEeoWiykVMdgj2REgnrP3b2n4KGyTFKoqqXYpdjSJ9BXXgw-RfkXmyBV1h8imymcXCZcYxzcKPSDSoZLUrPSqD5ooM021VKaTd4J4jFql3BrLGrvaRgUtSgfQdJjo1alMUalZ7hAEWkmhBlQ_ocdlHeJOR3Rrlk5c-JANOJ4UslMLG465QJ8tmfxaUbbOPB2YPj0f9uEbGW5kGkHW3BKQZbDQ" + +func TestInspectMissingLicenseFlag(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Test with no arguments + cmd.SetArgs([]string{}) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "accepts 1 arg(s), received 0") + + // Test with empty value + cmd.SetArgs([]string{""}) + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "license file path cannot be empty") + + // Test with whitespace value + cmd.SetArgs([]string{" "}) + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "license file path cannot be empty") +} + +func TestInspectFileNotFound(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Test with non-existent file + cmd.SetArgs([]string{"non-existent-file.json"}) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "file not found: non-existent-file.json") + + // Test with non-existent file in a non-existent directory + cmd.SetArgs([]string{"non/existent/path/file.json"}) + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "file not found: non/existent/path/file.json") +} + +func TestInspectCommandOutput(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Create a temporary directory and file + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test-license.json") + + // Create a valid license file + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, validOfficialTestLicense) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test license file") + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Run command with the test file + cmd.SetArgs([]string{filePath}) + err = cmd.Execute() + require.NoError(t, err) + + // Check the output contains expected headers + output := buf.String() + + expectedResult := `Product = Bacalhau +License ID = e66d1f3a-a8d8-4d57-8f14-00722844afe2 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = {}` + + assert.Contains(t, output, expectedResult) +} + +func TestInspectCommandYAMLOutput(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Create a temporary directory and file + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test-license.json") + + // Create a valid license file + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, validOfficialTestLicense) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test license file") + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Run command with the test file and yaml output format + cmd.SetArgs([]string{filePath, "--output", "yaml"}) + err = cmd.Execute() + require.NoError(t, err) + + // Parse actual output + var actualData map[string]interface{} + err = yaml.Unmarshal(buf.Bytes(), &actualData) + require.NoError(t, err, "Failed to parse actual YAML output") + + // Expected data + expectedData := map[string]interface{}{ + "product": "Bacalhau", + "license_id": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "license_type": "standard", + "customer_id": "test-customer-id-123", + "exp": 2384881638, + "iat": 1736881638, + "iss": "https://expanso.io/", + "jti": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "sub": "test-customer-id-123", + "license_version": "v1", + "capabilities": map[string]interface{}{"max_nodes": "1"}, + } + + // Compare the maps + assert.Equal(t, expectedData, actualData) +} + +func TestInspectCommandJSONOutput(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Create a temporary directory and file + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test-license.json") + + // Create a valid license file + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, validOfficialTestLicense) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test license file") + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Run command with the test file and json output format + cmd.SetArgs([]string{filePath, "--output", "json"}) + err = cmd.Execute() + require.NoError(t, err) + + // Parse actual output + var actualData map[string]interface{} + err = json.Unmarshal(buf.Bytes(), &actualData) + require.NoError(t, err, "Failed to parse actual JSON output") + + // Convert the exp and iat values to int64 for consistent comparison + if exp, ok := actualData["exp"].(float64); ok { + actualData["exp"] = int64(exp) + } + if iat, ok := actualData["iat"].(float64); ok { + actualData["iat"] = int64(iat) + } + + // Expected data + expectedData := map[string]interface{}{ + "product": "Bacalhau", + "license_id": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "license_type": "standard", + "customer_id": "test-customer-id-123", + "exp": int64(2384881638), + "iat": int64(1736881638), + "iss": "https://expanso.io/", + "jti": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "sub": "test-customer-id-123", + "license_version": "v1", + "capabilities": map[string]interface{}{"max_nodes": "1"}, + } + + // Compare the maps + assert.Equal(t, expectedData, actualData) +} + +func TestInspectValidLicenseFile(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Create a temporary directory and file + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test-license.json") + + // Create a valid license file with the JWT token + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, validOfficialTestLicense) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test license file") + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Test key-value output + cmd.SetArgs([]string{filePath}) + err = cmd.Execute() + require.NoError(t, err) + + output := buf.String() + expectedOutput := `Product = Bacalhau +License ID = e66d1f3a-a8d8-4d57-8f14-00722844afe2 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = {}` + + assert.Equal(t, expectedOutput, strings.TrimSpace(output)) + + // Test JSON output + buf.Reset() + cmd.SetArgs([]string{filePath, "--output", "json"}) + err = cmd.Execute() + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(buf.Bytes(), &result) + require.NoError(t, err) + + // Convert timestamp fields to int64 for comparison + if exp, ok := result["exp"].(float64); ok { + result["exp"] = int64(exp) + } + if iat, ok := result["iat"].(float64); ok { + result["iat"] = int64(iat) + } + + expectedJSON := map[string]interface{}{ + "product": "Bacalhau", + "license_id": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "license_type": "standard", + "customer_id": "test-customer-id-123", + "exp": int64(2384881638), + "iat": int64(1736881638), + "iss": "https://expanso.io/", + "jti": "e66d1f3a-a8d8-4d57-8f14-00722844afe2", + "sub": "test-customer-id-123", + "license_version": "v1", + "capabilities": map[string]interface{}{"max_nodes": "1"}, + } + + assert.Equal(t, expectedJSON, result) +} + +func TestInspectValidLicenseFileWithMetadata(t *testing.T) { + // Create a new command instance + cmd := NewInspectCmd() + + // Create a temporary directory and file + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test-license.json") + + // Create a valid license file with the JWT token + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, validOfficialTestLicenseWithMetadata) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test license file") + + // Set up buffer to capture output + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + + // Test key-value output + cmd.SetArgs([]string{filePath}) + err = cmd.Execute() + require.NoError(t, err) + + output := buf.String() + expectedOutput := `Product = Bacalhau +License ID = 2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = someMetadata=valueOfSomeMetadata` + + assert.Equal(t, expectedOutput, strings.TrimSpace(output)) + + // Test JSON output + buf.Reset() + cmd.SetArgs([]string{filePath, "--output", "json"}) + err = cmd.Execute() + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(buf.Bytes(), &result) + require.NoError(t, err) + + // Convert timestamp fields to int64 for comparison + if exp, ok := result["exp"].(float64); ok { + result["exp"] = int64(exp) + } + if iat, ok := result["iat"].(float64); ok { + result["iat"] = int64(iat) + } + + expectedJSON := map[string]interface{}{ + "product": "Bacalhau", + "license_id": "2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678", + "license_type": "standard", + "customer_id": "test-customer-id-123", + "exp": int64(2384889682), + "iat": int64(1736889682), + "iss": "https://expanso.io/", + "jti": "2d58c7c9-ec29-45a5-a5cd-cb8f7fee6678", + "sub": "test-customer-id-123", + "license_version": "v1", + "capabilities": map[string]interface{}{"max_nodes": "1"}, + "metadata": map[string]interface{}{"someMetadata": "valueOfSomeMetadata"}, + } + + assert.Equal(t, expectedJSON, result) +} + +func TestInspectInvalidLicenseToken(t *testing.T) { + cmd := NewInspectCmd() + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "invalid-token.json") + + // Create a file with invalid token + licenseContent := `{ + "license": "invalid.jwt.token" + }` + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test file") + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{filePath}) + + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid license") +} + +func TestInspectInvalidSignatureLicenseToken(t *testing.T) { + cmd := NewInspectCmd() + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "invalid-signature.json") + + // Create a file with a token that has invalid signature + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, officialTestTokenWithWrongSignature) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test file") + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{filePath}) + + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse license: token signature is invalid") +} + +func TestInspectExpiredLicenseToken(t *testing.T) { + cmd := NewInspectCmd() + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "expired-license.json") + + // Create a file with a token that has invalid signature + licenseContent := fmt.Sprintf(`{ + "license": "%s" + }`, officialTokenButExpired) + err := os.WriteFile(filePath, []byte(licenseContent), 0644) + require.NoError(t, err, "Failed to create test file") + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{filePath}) + + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid license: failed to parse license: token has invalid claims: token is expired") +} + +func TestInspectMalformedLicenseFile(t *testing.T) { + cmd := NewInspectCmd() + tmpDir := t.TempDir() + + testCases := []struct { + name string + content string + expectedErr string + }{ + { + name: "not json", + content: "this is not json", + expectedErr: "failed to parse license file", + }, + { + name: "missing license key", + content: `{"some_other_key": "value"}`, + expectedErr: "invalid license: failed to parse license: token is malformed", + }, + { + name: "random string as license", + content: `{"license": "some random string"}`, + expectedErr: "invalid license: failed to parse license: token is malformed", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + filePath := filepath.Join(tmpDir, fmt.Sprintf("malformed-%s.json", tc.name)) + err := os.WriteFile(filePath, []byte(tc.content), 0644) + require.NoError(t, err, "Failed to create test file") + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{filePath}) + + err = cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + }) + } +} diff --git a/cmd/cli/license/root.go b/cmd/cli/license/root.go new file mode 100644 index 0000000000..2c7b66616e --- /dev/null +++ b/cmd/cli/license/root.go @@ -0,0 +1,19 @@ +package license + +import ( + "github.com/bacalhau-project/bacalhau/cmd/util/hook" + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "license", + Short: "Commands to inspect and validate local license information. To inspect an " + + "orchestrator license, please use 'bacalhau agent license inspect'.'", + PersistentPreRunE: hook.AfterParentPreRunHook(hook.RemoteCmdPreRunHooks), + PersistentPostRunE: hook.AfterParentPostRunHook(hook.RemoteCmdPostRunHooks), + } + + cmd.AddCommand(NewInspectCmd()) + return cmd +} diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 39c5ac8722..68b06ccbb6 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -16,6 +16,7 @@ import ( "github.com/bacalhau-project/bacalhau/cmd/cli/devstack" "github.com/bacalhau-project/bacalhau/cmd/cli/docker" "github.com/bacalhau-project/bacalhau/cmd/cli/job" + "github.com/bacalhau-project/bacalhau/cmd/cli/license" "github.com/bacalhau-project/bacalhau/cmd/cli/node" "github.com/bacalhau-project/bacalhau/cmd/cli/serve" "github.com/bacalhau-project/bacalhau/cmd/cli/version" @@ -104,6 +105,7 @@ func NewRootCmd() *cobra.Command { node.NewCmd(), serve.NewCmd(), version.NewCmd(), + license.NewCmd(), wasm.NewCmd(), // deprecated command diff --git a/cspell.yaml b/cspell.yaml index 274eb310ff..2c1296c570 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -33,6 +33,7 @@ ignorePaths: - webui/build - webui/node_modules - webui/lib/api/generated/** + - test_integration/common_assets/licenses/** # Patterns to ignore ignoreRegExpList: diff --git a/pkg/lib/license/license_validator.go b/pkg/lib/license/license_validator.go index 2a6b842294..0391b2b2a7 100644 --- a/pkg/lib/license/license_validator.go +++ b/pkg/lib/license/license_validator.go @@ -28,6 +28,31 @@ type LicenseClaims struct { Metadata map[string]string `json:"metadata,omitempty"` } +// Ignoring spell check due to the abundance of JWT slugs +// cSpell:disable +// +//nolint:lll // JWKS Format +const defaultOfflineJWKSVerificationKeys = `{ + "keys": [ + { + "kty": "RSA", + "n": "5iBmcKBkKZTnFDGtLzj1jnKq8Hhbq-Gywu7J2vO-xQwVZUKg4kVkSbl2BoD4ba2Ppy7gymojPFPS2juP2FdirpK0SMN2fs7LPIxEQT_yrlYMaaR658YwG4Q_698XD6Dk5Z6qYmuUu71Y_QbZ-Lsmt3DfKGWJqYt-hElclJ8O757k-Z78bj364Fm_e1ETxMpCqzfqjAhQhdkBaR9Tcm4LDSn3_KvfGtIupnkHdaJMlFLs3hsHZ-CqSBRGzdp5DQCclxXK7K0Ilsmqpc2XBADWGlFehYrG40aM8mv99_Dm9fZWNqjg4h0Z7X1mTOpZgjxKUix9FF3YlcmhLEod2tdE7w", + "e": "AQAB", + "kid": "5nJnFCNSyAT1SQvtzl782YCeGkWqTCtv1fyHUQkxrNU", + "alg": "RS256" + }, + { + "kty": "RSA", + "n": "n5fvf4lV6UnM2MmTCXCIvIC1lEDZdhz6HiUX7x_vWw5VT-RIcgGIMfiGx_A1N1HPUOFRY6C-vZjfroqfYe-rWKKH3_s8bKpgaemmlI0l5ZdA_K4-iZdRIAkHjrHLJbwxqjcSDztW6O8zQ42g9aNkDX6AknojqeJMBWTF0qfcFIvRk8YArqGEOd3XZbkCNvC2c1fejKZ9pTdxq9rsrs0SPXx89c145-GB4Wb7lBST-LLClO3J16My5CZG44DO7LH7neRTGPs5DGdefJHDtO0ixB5vtWwt7HdxPVM9EJWKes78H_KqAPC6my7oxa6hE4Sa4C0ASN21FADS-__a60LwVQ", + "e": "AQAB", + "kid": "CLo1sWpJA57y0L2SEJB6Pu_VJdGV6WbaaA_pbHao8qs", + "alg": "RS256" + } + ] +}` + +// cSpell:enable + // NewLicenseValidatorFromFile creates a new validator from a JWKS file func NewLicenseValidatorFromFile(jwksPath string) (*LicenseValidator, error) { jwksData, err := os.ReadFile(jwksPath) @@ -71,6 +96,11 @@ func NewLicenseValidatorFromJSON(jwksJSON json.RawMessage) (*LicenseValidator, e }, nil } +// NewOfflineLicenseValidator creates a new validator using hardcoded JWKS Public Keys +func NewOfflineLicenseValidator() (*LicenseValidator, error) { + return NewLicenseValidatorFromJSON(json.RawMessage(defaultOfflineJWKSVerificationKeys)) +} + // ValidateToken validates a license token and returns the claims func (v *LicenseValidator) ValidateToken(tokenString string) (*LicenseClaims, error) { var claims LicenseClaims @@ -78,11 +108,11 @@ func (v *LicenseValidator) ValidateToken(tokenString string) (*LicenseClaims, er // Parse and validate the token token, err := jwt.ParseWithClaims(tokenString, &claims, v.keyFunc) if err != nil { - return nil, fmt.Errorf("failed to parse token: %w", err) + return nil, fmt.Errorf("failed to parse license: %w", err) } if !token.Valid { - return nil, fmt.Errorf("invalid token") + return nil, fmt.Errorf("invalid license") } // Additional validation can be added here diff --git a/pkg/lib/license/license_validator_test.go b/pkg/lib/license/license_validator_test.go index 36c88bcf56..6ddfd6c00c 100644 --- a/pkg/lib/license/license_validator_test.go +++ b/pkg/lib/license/license_validator_test.go @@ -110,20 +110,23 @@ var testJWKSFOrInvalidTokens = json.RawMessage(`{ // Test tokens const ( + // This valid test token was signed by the offical key with id: 5nJnFCNSyAT1SQvtzl782YCeGkWqTCtv1fyHUQkxrNU + validOfficialTestToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtHok9tG2OV5ybSZoxFDQW3iqiCg6TqMwCAa6C6MBXtLnv-NP1H9Ytg" validRSASignedJWTToken1 = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleV8xIiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4NzM1NywiaWF0IjoxNzM2MjkwOTU3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMTgxNTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.KapDttXfD6WuBrm2OXY2-CNUYI0qFcs7WzgzvGuqU0llZWT9qusKJq5fVFKJbf1Ug9_Bhv9FqvtDQUhJMbasnfkGUiy384WWLsP5V4lLCLZSBVcydd6N5_XR430Q_2oxXGjMv1ZXp_VAUxNTbq15FWG6wNVR88xQZBK1gyZEYe7-uhua-FwTp1LjRF8h3-f0qbtDEVlDiTsZMbIaODqLTwsTrkZT5bqDO4H9u2cqv3d3XDjBn-aLIvgxrHYzPA5Im2nIcXovAFaD-SSHe6vqSfm1SYhBJywFrVwXya5rSLNR7CBS9vy2hUjnGT4Uprt93unEfSqq3znrKtVXbmsYew" validRSASignedJWTToken2 = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleV8yIiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8yIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8yIiwiZXhwIjoxMDM3NjI4NzM1NywiaWF0IjoxNzM2MjkwOTU3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8yIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMiIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8yIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcyIjoic29tZXRoaW5nMl92YWx1ZSJ9LCJuYmYiOjE3MzYxMTgxNTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzIifQ.BnJV8nFHzhSYb04MIwlUSUnsC9I-mT3TleAAmboFfTw_3I75gqxPh2nq_Eyr7pCgbKhB1Tlao5hizznCdCLqw8FQIRo9Efl4vrX0ehT092-C8pNtjOHxeMlXkbROr8iChJsRghhAkCrQWXvNIFyBe03vKri7xCnHGDEMCL7217FBSXYMDMxB9oHs6Lc7Kv4oCizNBk4yYlJ7hl8rngL78HwrjpN7Y91YjXBfGDoKmgOPe_15Pohryjx0CXs7X3pX9dX--z0qFt5S5PYd3RFfG9iBbgB99OJiaEstsAb_RdsvyibkJfeX6GjZoM6LtOGrPb_u-I2yQVgi7rqUFZiG0w" expiredRSASignedJWTToken3 = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImV4cGlyZWRfa2V5IiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxNzM2MjA1MTgzLCJpYXQiOjE3MzYyOTE1ODMsImlzcyI6Imh0dHBzOi8vZXhwYW5zby5pby8iLCJqdGkiOiJsaWNlbnNlXzEiLCJsaWNlbnNlX2lkIjoibGljZW5zZV8xIiwibGljZW5zZV90eXBlIjoicHJvZF90aWVyXzEiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsIm1ldGFkYXRhIjp7InNvbWV0aGluZzEiOiJzb21ldGhpbmcxX3ZhbHVlIn0sIm5iZiI6MTczNjExODc4MywicHJvZHVjdCI6IkJhY2FsaGF1Iiwic3ViIjoiY3VzdG9tZXJfMSJ9.QUKIHKugJ_tLyVyiXak5HO6Wroy1mNWUMECO4D4Kaj3PFZL7Q37IkOSucECT5swHL1L3frQIogCVL8tlwDXb9MJwEs3mUiUiXk3iYTKi68YQqE_PyD8UbezaSUn0xCKvCqugWV_tptpmxSIyvqoGPuLCj35jBBb5qhXotgY150PQlkEG4FhjOkyxNjQQEgYr8a3BvgqgHdm2FoCyS46QBp3TrSNHO4ogUui6qlLLDjVp3WWs_HXNBeEjzDxHjSeAItTxppM_e0hYMI7vzDHg4lOub1UMm-f0bg3ivTnw3Gp5Ht0zc6ScEJ8fuxONNxTYgb5kkAKKzT8YtITYadk5DA" ) const ( - emptyCustomerIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2N1c3RvbWVyX2lkX2tleSIsInR5cCI6IkpXVCJ9.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiIiLCJleHAiOjEwMzc2Mjg5OTI4LCJpYXQiOjE3MzYyOTM1MjgsImlzcyI6Imh0dHBzOi8vZXhwYW5zby5pby8iLCJqdGkiOiJsaWNlbnNlXzEiLCJsaWNlbnNlX2lkIjoibGljZW5zZV8xIiwibGljZW5zZV90eXBlIjoicHJvZF90aWVyXzEiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsIm1ldGFkYXRhIjp7InNvbWV0aGluZzEiOiJzb21ldGhpbmcxX3ZhbHVlIn0sIm5iZiI6MTczNjEyMDcyOCwicHJvZHVjdCI6IkJhY2FsaGF1Iiwic3ViIjoiY3VzdG9tZXJfMSJ9.RoFVlCt0IgL04rs68YWsJNxtjKY3zkBCDAAD3TaBAJV8dduC4DUNwfCqS93Xy9G-vEFDqzh2dIhBsb2rTDsnDz7A1YYferksUz-cFAzvISbzvYGzGEobmvZXpsQTFQ_Iq3MFzrjzh61I_Mv5qA2QjDDqyPVskvUQx_Sl3up_5TbXVCl_57rxFMpiYoCR0q4zxmPFRLKyzo59UXjqlTTCX2vJ0zjZrLGh-fctCFbr3hUU_ZdELfvUcO8biKEPplHvSBel-VYyYEmwhGnzDpBFT7CMLiYJhbbO32dUAaeJ3CKtz0tl3EXyAMl0o-rxTVvWnFnphO4V7lLJN7HxE3RKew" - emptyJtiToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2p0aV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk5NywiaWF0IjoxNzM2MjkzNTk3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoiIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3OTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.BNdeY4ifXz1F7PUF52RxEHUIasz6NTTL-cbVok6EOlPSdAXikKozGdiZFIdoLxyCh75OTKBLevGq3TDTfcnevfcTJXATM_dQibr_yR2Ke9JblU3YrLM9h87f0SunZ2scZmL22CCDIbIN4kYY9ZBdIxWFVUru0C-T3qLP_0LzL_CrvJNAqWwNPXgLkJADwNupMBQBpT4qOmMOfbC8EvPN89VHshvjXyJCzLI7bcu2Byi5S60QPQq5tmx3A3uwvGfZl3ZF81a546u-j7CCqBdBYFPfJzx7xbZCAYfS20QUN72zSz9hve0Uy096FcndOvE6zrEOvNHUwmI5149yvfSJBA" - emptyLicenseIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2xpY2Vuc2VfaWRfa2V5IiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTg5NywiaWF0IjoxNzM2MjkzNDk3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6IiIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA2OTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.axh_Njc28CcS3KS6Kr5_XP674Hy_t1X7zq8LA2hGvvS8Q8cjLOBN4D71f4NbmTSNGG--xF07VooJhaHV1D_MZ8ftJCk-T3HavvUq7Hg56j71r6WG9nRm8zIpfTN0oaNoDuRliWBqrwDvDQyt0oDz5btJtQ5JrzFyWyIuZH3jygcyU_VKFP7o2yRO9WmVcRbGQRcFLLNBhhaqWs-M62BMJZeYaY_0U54ZIXlK5kn4PXl0JQtScBhdipXAQGFVePnzCJ-6du3V4n5fhXVtFSPPfd_-66ci3PiJZP4IjGrXRDioq2Wd1LcZ_KFkhRlaldHkBPDPsDRcda6wpLmgqOMjxQ" - emptyLicenseTypeToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2xpY2Vuc2VfdHlwZV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTkxMywiaWF0IjoxNzM2MjkzNTEzLCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6IiIsImxpY2Vuc2VfdmVyc2lvbiI6InYxIiwibWV0YWRhdGEiOnsic29tZXRoaW5nMSI6InNvbWV0aGluZzFfdmFsdWUifSwibmJmIjoxNzM2MTIwNzEzLCJwcm9kdWN0IjoiQmFjYWxoYXUiLCJzdWIiOiJjdXN0b21lcl8xIn0.BycKgLsf6AwVyiIdrnb33XxzKUqIq0_mtRo18-g_K2aydrO_YBBoGKEm7ZCYKPZaCM3Q0wbotrh4fFKWZ9oqX3yzHblTQBfMnmaYGpjG9FO7hCLHcyeLSnxjuR_qYXdXRT77Sr5PdlTBUY31tBXtv2rlrs55C76hVSJ8o4o_qUiAWIFhRswT_r9R8yY9S25G_4bN-sXapZSck94QfBnIteFizxSJgK3cZoAncOfZRW8CR93aWTWbZt3nESQqYMndKnDY9rZKCkB3hYDK0bb7xozjopu1th8eKRqiGQr-URJNc9anqRdwF8yZOVgtvs-9zutpjniqPA3UhiXu6XUxFg" - emptySubjectKeyToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X3N1YmplY3Rfa2V5IiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk4NCwiaWF0IjoxNzM2MjkzNTg0LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3ODQsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6IiJ9.umbMJkvE3p4Pl22De_tvMwYpQ5tKBvQPpcQV_NAm2NbyJmLFMCaP-MaP-gDQEl-vUoabp6V0u6pTJUbCBosVdMlc9EGzBCMhG1ifEzfw1QbhLWPoxFqzRLbNOF28g9RLuk4-8MkASPmagr7JTOd7xjJfepqDVnCSeuWzehhM1VA9OMw3YWfxOpY7rgXBf-zujcu9noBNA1ADPrG3WZX_udY02poqyG1wr8nqdT-7d1jnff_Ov3r4sWygmO83CQ2mNVZk_N1lvTVUZsrNjzvMLqiXxHTvt3LdNEgO1yRhA7hpCIOANycIrwqXv2SD0uN7bYbojbidK-u0JbkTjAg5zQ" - invalidVersionToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImludmFsaWRfdmVyc2lvbl9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTgzMCwiaWF0IjoxNzM2MjkzNDMwLCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjMzIiwibWV0YWRhdGEiOnsic29tZXRoaW5nMSI6InNvbWV0aGluZzFfdmFsdWUifSwibmJmIjoxNzM2MTIwNjMwLCJwcm9kdWN0IjoiQmFjYWxoYXUiLCJzdWIiOiJjdXN0b21lcl8xIn0.WZZTS18gSSQBDKYQr6fHK15C1tpoQKDqf8Th8KFBfDRR2Sx_jiJ3ON9U6pq-dxqj3Ko0zhdgZy91vx0Gv2u7wxZ54SAOA-Vu8gQMtElNLEMjeJTx345iMy-uXYKt4nbF4blzXZGoKmocv_OisWyN04b0RHn_UVz8nKYD78CwjTEI2iAgwa5moJ9dxjmbHvXGDaV6FG3Dk76CWQ6sxshuRa9nijV9XdF2vaVNgBg3x4g14lHfd3fta9Q9ik9t41zgVEykamOqT9DbNC7G3bX1I7MEYJFNIox-THLv1Ty-nWGnjpq83tAJuhCrKNTuEJuAjpFPZaQx-gJ8NKO1_sPTog" - wrongIssuerToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX2lzc3Vlcl9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk0NiwiaWF0IjoxNzM2MjkzNTQ2LCJpc3MiOiJodHRwczovL2V4YW1wbGUuaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3NDYsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.Yno49gv4LpTtFodpHwItRTR_JUUgCOa5BWODNSUaGGOd9U1WVyAO8p-GwImEwgm4DcmeK7W1JCZ6Ij82ctR-8owq7VTD6Cg4jD6E_4gSTWWlsxPGNyPWohXa_IzsmPQJ7FKNadmz4Ux60Edo5rrQe_ESjyumabit7CsTXSfmPZGTqpEun7fI-scu1xZM39X5L5Ghw-GwXODFhIHtLdLaglL2SncCNr-nYw4_Bzil8iwGoQBUVjS9E8tf8gKPZpP_wwQPUYqlDMg4ffsCaz1x3OS-lp7BcKGvhuE5-s4xbhNktVosvOn64-c2OYPOqv1xIjryCdp5UphajgwEyrJVgw" - wrongProductNameToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX3Byb2R1Y3RfbmFtZV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTg1NSwiaWF0IjoxNzM2MjkzNDU1LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA2NTUsInByb2R1Y3QiOiJNZW93Iiwic3ViIjoiY3VzdG9tZXJfMSJ9.cWGUJonGpqF732VQ6V6pf2x76lvcOTq0DOD91Amkg4CIU-Qxd8GvqJMeYhMMCC_a-VTAIA5-0JBEyucDZCLuBVdqDizJ--5q6AFgmHIc6XuqoOJCLE9bDlC5POQM_kZ2Rled-qgGbN9FzOrJ4Spx1NPJINh8r3Tbz9y-rRyoSbJOHwkheMI3teY-SgLYBiHsukZLN9u52Jq6ixV10H7dlA2Au8H8rJhBBomV_bPO3o3QcgDvvCRrX5RpYdotjxgMqeU_w4nF96UlzkuAo3cQkfumSpaCbxSOEDD1zgGACGwuWFaoyubBCidrCbBujyV5szzPthuHNZ95RSk4aS_mjg" + officialTestTokenWithWrongSignature = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.iambadsignature" + emptyCustomerIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2N1c3RvbWVyX2lkX2tleSIsInR5cCI6IkpXVCJ9.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiIiLCJleHAiOjEwMzc2Mjg5OTI4LCJpYXQiOjE3MzYyOTM1MjgsImlzcyI6Imh0dHBzOi8vZXhwYW5zby5pby8iLCJqdGkiOiJsaWNlbnNlXzEiLCJsaWNlbnNlX2lkIjoibGljZW5zZV8xIiwibGljZW5zZV90eXBlIjoicHJvZF90aWVyXzEiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsIm1ldGFkYXRhIjp7InNvbWV0aGluZzEiOiJzb21ldGhpbmcxX3ZhbHVlIn0sIm5iZiI6MTczNjEyMDcyOCwicHJvZHVjdCI6IkJhY2FsaGF1Iiwic3ViIjoiY3VzdG9tZXJfMSJ9.RoFVlCt0IgL04rs68YWsJNxtjKY3zkBCDAAD3TaBAJV8dduC4DUNwfCqS93Xy9G-vEFDqzh2dIhBsb2rTDsnDz7A1YYferksUz-cFAzvISbzvYGzGEobmvZXpsQTFQ_Iq3MFzrjzh61I_Mv5qA2QjDDqyPVskvUQx_Sl3up_5TbXVCl_57rxFMpiYoCR0q4zxmPFRLKyzo59UXjqlTTCX2vJ0zjZrLGh-fctCFbr3hUU_ZdELfvUcO8biKEPplHvSBel-VYyYEmwhGnzDpBFT7CMLiYJhbbO32dUAaeJ3CKtz0tl3EXyAMl0o-rxTVvWnFnphO4V7lLJN7HxE3RKew" + emptyJtiToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2p0aV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk5NywiaWF0IjoxNzM2MjkzNTk3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoiIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3OTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.BNdeY4ifXz1F7PUF52RxEHUIasz6NTTL-cbVok6EOlPSdAXikKozGdiZFIdoLxyCh75OTKBLevGq3TDTfcnevfcTJXATM_dQibr_yR2Ke9JblU3YrLM9h87f0SunZ2scZmL22CCDIbIN4kYY9ZBdIxWFVUru0C-T3qLP_0LzL_CrvJNAqWwNPXgLkJADwNupMBQBpT4qOmMOfbC8EvPN89VHshvjXyJCzLI7bcu2Byi5S60QPQq5tmx3A3uwvGfZl3ZF81a546u-j7CCqBdBYFPfJzx7xbZCAYfS20QUN72zSz9hve0Uy096FcndOvE6zrEOvNHUwmI5149yvfSJBA" + emptyLicenseIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2xpY2Vuc2VfaWRfa2V5IiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTg5NywiaWF0IjoxNzM2MjkzNDk3LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6IiIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA2OTcsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.axh_Njc28CcS3KS6Kr5_XP674Hy_t1X7zq8LA2hGvvS8Q8cjLOBN4D71f4NbmTSNGG--xF07VooJhaHV1D_MZ8ftJCk-T3HavvUq7Hg56j71r6WG9nRm8zIpfTN0oaNoDuRliWBqrwDvDQyt0oDz5btJtQ5JrzFyWyIuZH3jygcyU_VKFP7o2yRO9WmVcRbGQRcFLLNBhhaqWs-M62BMJZeYaY_0U54ZIXlK5kn4PXl0JQtScBhdipXAQGFVePnzCJ-6du3V4n5fhXVtFSPPfd_-66ci3PiJZP4IjGrXRDioq2Wd1LcZ_KFkhRlaldHkBPDPsDRcda6wpLmgqOMjxQ" + emptyLicenseTypeToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X2xpY2Vuc2VfdHlwZV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTkxMywiaWF0IjoxNzM2MjkzNTEzLCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6IiIsImxpY2Vuc2VfdmVyc2lvbiI6InYxIiwibWV0YWRhdGEiOnsic29tZXRoaW5nMSI6InNvbWV0aGluZzFfdmFsdWUifSwibmJmIjoxNzM2MTIwNzEzLCJwcm9kdWN0IjoiQmFjYWxoYXUiLCJzdWIiOiJjdXN0b21lcl8xIn0.BycKgLsf6AwVyiIdrnb33XxzKUqIq0_mtRo18-g_K2aydrO_YBBoGKEm7ZCYKPZaCM3Q0wbotrh4fFKWZ9oqX3yzHblTQBfMnmaYGpjG9FO7hCLHcyeLSnxjuR_qYXdXRT77Sr5PdlTBUY31tBXtv2rlrs55C76hVSJ8o4o_qUiAWIFhRswT_r9R8yY9S25G_4bN-sXapZSck94QfBnIteFizxSJgK3cZoAncOfZRW8CR93aWTWbZt3nESQqYMndKnDY9rZKCkB3hYDK0bb7xozjopu1th8eKRqiGQr-URJNc9anqRdwF8yZOVgtvs-9zutpjniqPA3UhiXu6XUxFg" + emptySubjectKeyToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVtcHR5X3N1YmplY3Rfa2V5IiwidHlwIjoiSldUIn0.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk4NCwiaWF0IjoxNzM2MjkzNTg0LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3ODQsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6IiJ9.umbMJkvE3p4Pl22De_tvMwYpQ5tKBvQPpcQV_NAm2NbyJmLFMCaP-MaP-gDQEl-vUoabp6V0u6pTJUbCBosVdMlc9EGzBCMhG1ifEzfw1QbhLWPoxFqzRLbNOF28g9RLuk4-8MkASPmagr7JTOd7xjJfepqDVnCSeuWzehhM1VA9OMw3YWfxOpY7rgXBf-zujcu9noBNA1ADPrG3WZX_udY02poqyG1wr8nqdT-7d1jnff_Ov3r4sWygmO83CQ2mNVZk_N1lvTVUZsrNjzvMLqiXxHTvt3LdNEgO1yRhA7hpCIOANycIrwqXv2SD0uN7bYbojbidK-u0JbkTjAg5zQ" + invalidVersionToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImludmFsaWRfdmVyc2lvbl9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTgzMCwiaWF0IjoxNzM2MjkzNDMwLCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjMzIiwibWV0YWRhdGEiOnsic29tZXRoaW5nMSI6InNvbWV0aGluZzFfdmFsdWUifSwibmJmIjoxNzM2MTIwNjMwLCJwcm9kdWN0IjoiQmFjYWxoYXUiLCJzdWIiOiJjdXN0b21lcl8xIn0.WZZTS18gSSQBDKYQr6fHK15C1tpoQKDqf8Th8KFBfDRR2Sx_jiJ3ON9U6pq-dxqj3Ko0zhdgZy91vx0Gv2u7wxZ54SAOA-Vu8gQMtElNLEMjeJTx345iMy-uXYKt4nbF4blzXZGoKmocv_OisWyN04b0RHn_UVz8nKYD78CwjTEI2iAgwa5moJ9dxjmbHvXGDaV6FG3Dk76CWQ6sxshuRa9nijV9XdF2vaVNgBg3x4g14lHfd3fta9Q9ik9t41zgVEykamOqT9DbNC7G3bX1I7MEYJFNIox-THLv1Ty-nWGnjpq83tAJuhCrKNTuEJuAjpFPZaQx-gJ8NKO1_sPTog" + wrongIssuerToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX2lzc3Vlcl9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTk0NiwiaWF0IjoxNzM2MjkzNTQ2LCJpc3MiOiJodHRwczovL2V4YW1wbGUuaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA3NDYsInByb2R1Y3QiOiJCYWNhbGhhdSIsInN1YiI6ImN1c3RvbWVyXzEifQ.Yno49gv4LpTtFodpHwItRTR_JUUgCOa5BWODNSUaGGOd9U1WVyAO8p-GwImEwgm4DcmeK7W1JCZ6Ij82ctR-8owq7VTD6Cg4jD6E_4gSTWWlsxPGNyPWohXa_IzsmPQJ7FKNadmz4Ux60Edo5rrQe_ESjyumabit7CsTXSfmPZGTqpEun7fI-scu1xZM39X5L5Ghw-GwXODFhIHtLdLaglL2SncCNr-nYw4_Bzil8iwGoQBUVjS9E8tf8gKPZpP_wwQPUYqlDMg4ffsCaz1x3OS-lp7BcKGvhuE5-s4xbhNktVosvOn64-c2OYPOqv1xIjryCdp5UphajgwEyrJVgw" + wrongProductNameToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX3Byb2R1Y3RfbmFtZV9rZXkiLCJ0eXAiOiJKV1QifQ.eyJjYXBhYmlsaXRpZXMiOnsidGllcl8xIjoibm8ifSwiY3VzdG9tZXJfaWQiOiJjdXN0b21lcl8xIiwiZXhwIjoxMDM3NjI4OTg1NSwiaWF0IjoxNzM2MjkzNDU1LCJpc3MiOiJodHRwczovL2V4cGFuc28uaW8vIiwianRpIjoibGljZW5zZV8xIiwibGljZW5zZV9pZCI6ImxpY2Vuc2VfMSIsImxpY2Vuc2VfdHlwZSI6InByb2RfdGllcl8xIiwibGljZW5zZV92ZXJzaW9uIjoidjEiLCJtZXRhZGF0YSI6eyJzb21ldGhpbmcxIjoic29tZXRoaW5nMV92YWx1ZSJ9LCJuYmYiOjE3MzYxMjA2NTUsInByb2R1Y3QiOiJNZW93Iiwic3ViIjoiY3VzdG9tZXJfMSJ9.cWGUJonGpqF732VQ6V6pf2x76lvcOTq0DOD91Amkg4CIU-Qxd8GvqJMeYhMMCC_a-VTAIA5-0JBEyucDZCLuBVdqDizJ--5q6AFgmHIc6XuqoOJCLE9bDlC5POQM_kZ2Rled-qgGbN9FzOrJ4Spx1NPJINh8r3Tbz9y-rRyoSbJOHwkheMI3teY-SgLYBiHsukZLN9u52Jq6ixV10H7dlA2Au8H8rJhBBomV_bPO3o3QcgDvvCRrX5RpYdotjxgMqeU_w4nF96UlzkuAo3cQkfumSpaCbxSOEDD1zgGACGwuWFaoyubBCidrCbBujyV5szzPthuHNZ95RSk4aS_mjg" ) func TestNewLicenseValidatorFromJSON(t *testing.T) { @@ -214,7 +217,7 @@ func TestValidateToken(t *testing.T) { name: "Invalid format", token: "not.a.jwt", wantErr: true, - errString: "failed to parse token: token is malformed:", + errString: "failed to parse license: token is malformed:", }, { name: "Unknown key ID", @@ -422,3 +425,58 @@ func TestValidateAdditionalConstraints(t *testing.T) { }) } } + +func TestNewOfflineLicenseValidator(t *testing.T) { + // Create validator using offline public keys + // Those are actual officially signed keys for testing + validator, err := NewOfflineLicenseValidator() + require.NoError(t, err) + require.NotNil(t, validator) + + // Test with a valid token signed by the offline key + claims, err := validator.ValidateToken(validOfficialTestToken) + require.NoError(t, err) + require.NotNil(t, claims) + + // Verify the claims + assert.Equal(t, "Bacalhau", claims.Product) + assert.Equal(t, "v1", claims.LicenseVersion) + assert.Equal(t, "standard", claims.LicenseType) + assert.Equal(t, "e66d1f3a-a8d8-4d57-8f14-00722844afe2", claims.LicenseID) + assert.Equal(t, "test-customer-id-123", claims.CustomerID) + assert.Equal(t, "test-customer-id-123", claims.Subject) + assert.Equal(t, "https://expanso.io/", claims.Issuer) + assert.Equal(t, "e66d1f3a-a8d8-4d57-8f14-00722844afe2", claims.ID) // jti claim + assert.Equal(t, map[string]string{"max_nodes": "1"}, claims.Capabilities) + assert.Equal(t, map[string]string{}, claims.Metadata) + + // Verify timestamps + assert.Equal(t, int64(1736881638), claims.IssuedAt.Unix()) + assert.Equal(t, int64(2384881638), claims.ExpiresAt.Unix()) +} + +func TestOfflineLicenseValidatorWithInvalidKey(t *testing.T) { + // Create validator using offline public keys + validator, err := NewOfflineLicenseValidator() + require.NoError(t, err) + require.NotNil(t, validator) + + // Test with a token signed by a different key + claims, err := validator.ValidateToken(validRSASignedJWTToken1) + require.Error(t, err) + assert.Contains(t, err.Error(), "key not found") + require.Nil(t, claims) +} + +func TestOfflineLicenseValidatorWithWrongSignature(t *testing.T) { + // Create validator using offline public keys + validator, err := NewOfflineLicenseValidator() + require.NoError(t, err) + require.NotNil(t, validator) + + // Test with a token that has the correct key ID but wrong signature + claims, err := validator.ValidateToken(officialTestTokenWithWrongSignature) + require.Error(t, err) + assert.Contains(t, err.Error(), "token signature is invalid") + require.Nil(t, claims) +} diff --git a/test_integration/12_local_license_inspect_suite_test.go b/test_integration/12_local_license_inspect_suite_test.go new file mode 100644 index 0000000000..2c54bdd07d --- /dev/null +++ b/test_integration/12_local_license_inspect_suite_test.go @@ -0,0 +1,114 @@ +package test_integration + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" +) + +type LocalLicenseInspectSuite struct { + BaseDockerComposeTestSuite +} + +func NewLocalLicenseInspectSuite() *LocalLicenseInspectSuite { + s := &LocalLicenseInspectSuite{} + s.GlobalRunIdentifier = globalTestExecutionId + s.SuiteRunIdentifier = strings.ToLower(strings.Split(uuid.New().String(), "-")[0]) + return s +} + +func (s *LocalLicenseInspectSuite) SetupSuite() { + rawDockerComposeFilePath := "./common_assets/docker_compose_files/orchestrator-node-with-custom-start-command.yml" + s.Context, s.Cancel = context.WithCancel(context.Background()) + + orchestratorConfigFile := s.commonAssets("nodes_configs/12_basic_orchestrator_config.yaml") + orchestratorStartCommand := fmt.Sprintf("bacalhau serve --config=%s", orchestratorConfigFile) + extraRenderingData := map[string]interface{}{ + "OrchestratorStartCommand": orchestratorStartCommand, + } + s.BaseDockerComposeTestSuite.SetupSuite(rawDockerComposeFilePath, extraRenderingData) +} + +func (s *LocalLicenseInspectSuite) TearDownSuite() { + s.T().Log("Tearing down [Test Suite] in LocalLicenseInspectSuite...") + s.BaseDockerComposeTestSuite.TearDownSuite() +} + +func (s *LocalLicenseInspectSuite) TestValidateLocalLicense() { + licenseFile := s.commonAssets("licenses/test-license.json") + + licenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", "license", "inspect", licenseFile, + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + expectedOutput := `Product = Bacalhau +License ID = e66d1f3a-a8d8-4d57-8f14-00722844afe2 +Customer ID = test-customer-id-123 +Valid Until = 2045-07-28 +Version = v1 +Capabilities = max_nodes=1 +Metadata = {}` + + s.Require().Contains(licenseInspectionOutput, expectedOutput) +} + +func (s *LocalLicenseInspectSuite) TestValidateLocalLicenseJSONOutput() { + licenseFile := s.commonAssets("licenses/test-license.json") + + licenseInspectionOutput, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", + "license", + "inspect", + licenseFile, + "--output=json", + }, + ) + s.Require().NoErrorf(err, "Error inspecting license: %q", err) + + output, err := s.convertStringToDynamicJSON(licenseInspectionOutput) + s.Require().NoError(err) + + productName, err := output.Query("$.product") + s.Require().NoError(err) + s.Require().Equal("Bacalhau", productName.String()) + + licenseID, err := output.Query("$.license_id") + s.Require().NoError(err) + s.Require().Equal("e66d1f3a-a8d8-4d57-8f14-00722844afe2", licenseID.String()) + + customerID, err := output.Query("$.customer_id") + s.Require().NoError(err) + s.Require().Equal("test-customer-id-123", customerID.String()) + + licenseVersion, err := output.Query("$.license_version") + s.Require().NoError(err) + s.Require().Equal("v1", licenseVersion.String()) + + capabilitiesMaxNodes, err := output.Query("$.capabilities.max_nodes") + s.Require().NoError(err) + s.Require().Equal("1", capabilitiesMaxNodes.String()) +} + +func (s *LocalLicenseInspectSuite) TestInValidateLocalLicense() { + licenseFile := s.commonAssets("licenses/test-license-invalid.json") + + _, err := s.executeCommandInDefaultJumpbox( + []string{ + "bacalhau", "license", "inspect", licenseFile, + }, + ) + + s.Require().ErrorContains(err, "invalid license: failed to parse license: token signature is invalid") +} + +func TestLocalLicenseInspectSuite(t *testing.T) { + suite.Run(t, NewLocalLicenseInspectSuite()) +} diff --git a/test_integration/common_assets/licenses/test-license-invalid.json b/test_integration/common_assets/licenses/test-license-invalid.json new file mode 100644 index 0000000000..1b855c2b99 --- /dev/null +++ b/test_integration/common_assets/licenses/test-license-invalid.json @@ -0,0 +1,3 @@ +{ + "license": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtH" +} \ No newline at end of file diff --git a/test_integration/common_assets/licenses/test-license.json b/test_integration/common_assets/licenses/test-license.json new file mode 100644 index 0000000000..dc9611bbb8 --- /dev/null +++ b/test_integration/common_assets/licenses/test-license.json @@ -0,0 +1,3 @@ +{ + "license": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVuSm5GQ05TeUFUMVNRdnR6bDc4MllDZUdrV3FUQ3R2MWZ5SFVRa3hyTlUiLCJ0eXAiOiJKV1QifQ.eyJwcm9kdWN0IjoiQmFjYWxoYXUiLCJsaWNlbnNlX3ZlcnNpb24iOiJ2MSIsImxpY2Vuc2VfdHlwZSI6InN0YW5kYXJkIiwibGljZW5zZV9pZCI6ImU2NmQxZjNhLWE4ZDgtNGQ1Ny04ZjE0LTAwNzIyODQ0YWZlMiIsImN1c3RvbWVyX25hbWUiOiJiYWNhbGhhdS1pbnRlZ3JhdGlvbi10ZXN0cyIsImN1c3RvbWVyX2lkIjoidGVzdC1jdXN0b21lci1pZC0xMjMiLCJjYXBhYmlsaXRpZXMiOnsibWF4X25vZGVzIjoiMSJ9LCJtZXRhZGF0YSI6e30sImlhdCI6MTczNjg4MTYzOCwiaXNzIjoiaHR0cHM6Ly9leHBhbnNvLmlvLyIsInN1YiI6InRlc3QtY3VzdG9tZXItaWQtMTIzIiwiZXhwIjoyMzg0ODgxNjM4LCJqdGkiOiJlNjZkMWYzYS1hOGQ4LTRkNTctOGYxNC0wMDcyMjg0NGFmZTIifQ.U6qkWmki2wp3RbPdn8d0zzsy4FchZIyUDmJi2bJ4w4vhwJlJ0_F2_317v4iPzy9q69eJOKNaqj8P3xYaPbpiooFm15OdJ3ecbMy8bKvvWVj43stw6HNP_uoW-RlZnY2zTOQ9WhlOhjnUPPC-UXOcaMwxiLBwMo5n3Rs0W9uAQHGQIptGg0sKiZvIrMZZ3vww2PZ3wJDiDvznE2lPtI7jAbcFFKDlhY3UiXed2ihGTWvLW8Zwj4veCR4PAUoEDu-nfQDvlqNeAvABT-KrKY2M-d5T_WzK1WwXtHok9tG2OV5ybSZoxFDQW3iqiCg6TqMwCAa6C6MBXtLnv-NP1H9Ytg" +} \ No newline at end of file diff --git a/test_integration/common_assets/nodes_configs/12_basic_orchestrator_config.yaml b/test_integration/common_assets/nodes_configs/12_basic_orchestrator_config.yaml new file mode 100644 index 0000000000..ff3627b25d --- /dev/null +++ b/test_integration/common_assets/nodes_configs/12_basic_orchestrator_config.yaml @@ -0,0 +1,10 @@ +NameProvider: "uuid" +API: + Port: 1234 +Orchestrator: + Enabled: true + Auth: + Token: "i_am_very_secret_token" +Labels: + label1: label1Value + label2: label2Value