Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ func (p *Plugin) Version() string {
return p.version
}

func (p *Plugin) Meta() Meta {
return Meta{
Team: p.team,
Kind: cqapi.PluginKind(p.kind),
Name: p.name,
SkipUsageClient: p.skipUsageClient,
}
}

// SetSkipUsageClient sets whether the usage client should be skipped
func (p *Plugin) SetSkipUsageClient(v bool) {
p.skipUsageClient = v
Expand Down Expand Up @@ -205,12 +214,7 @@ func (p *Plugin) Init(ctx context.Context, spec []byte, options NewClientOptions
}
}

options.PluginMeta = Meta{
Team: p.team,
Kind: cqapi.PluginKind(p.kind),
Name: p.name,
SkipUsageClient: p.skipUsageClient,
}
options.PluginMeta = p.Meta()

p.client, err = p.newClient(ctx, p.logger, spec, options)
if err != nil {
Expand Down
71 changes: 68 additions & 3 deletions premium/offline.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import (
"encoding/json"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"time"

"github.com/cloudquery/plugin-sdk/v4/plugin"
"github.com/rs/zerolog"
)

type License struct {
LicensedTo string `json:"licensed_to"` // Customers name, e.g. "Acme Inc"
LicensedTo string `json:"licensed_to"` // Customers name, e.g. "Acme Inc"
Plugins []string `json:"plugins,omitempty"` // List of plugins, each in the format <org>/<kind>/<name>, e.g. "cloudquery/source/aws". Optional, if empty all plugins are allowed.
IssuedAt time.Time `json:"issued_at"`
ValidFrom time.Time `json:"valid_from"`
ExpiresAt time.Time `json:"expires_at"`
Expand All @@ -28,12 +33,65 @@ var (
ErrInvalidLicenseSignature = errors.New("invalid license signature")
ErrLicenseNotValidYet = errors.New("license not valid yet")
ErrLicenseExpired = errors.New("license expired")
ErrLicenseNotApplicable = errors.New("license not applicable to this plugin")
)

//go:embed offline.key
var publicKey string

func ValidateLicense(logger zerolog.Logger, licenseFile string) error {
var timeFunc = time.Now

func ValidateLicense(logger zerolog.Logger, meta plugin.Meta, licenseFileOrDirectory string) error {
fi, err := os.Stat(licenseFileOrDirectory)
if err != nil {
return err
}
if !fi.IsDir() {
return validateLicenseFile(logger, meta, licenseFileOrDirectory)
}

found := false
var lastError error
err = filepath.WalkDir(licenseFileOrDirectory, func(path string, d os.DirEntry, err error) error {
if d.IsDir() {
if path == licenseFileOrDirectory {
return nil
}
return filepath.SkipDir
}
if err != nil {
return err
}

if filepath.Ext(path) != ".cqlicense" {
return nil
}

logger.Debug().Str("path", path).Msg("considering license file")
lastError = validateLicenseFile(logger, meta, path)
switch lastError {
case nil:
found = true
return filepath.SkipAll
case ErrLicenseNotApplicable:
return nil
default:
return lastError
}
})
if err != nil {
return err
}
if found {
return nil
}
if lastError != nil {
return lastError
}
return errors.New("failed to validate license directory")
}

func validateLicenseFile(logger zerolog.Logger, meta plugin.Meta, licenseFile string) error {
licenseContents, err := os.ReadFile(licenseFile)
if err != nil {
return err
Expand All @@ -44,6 +102,13 @@ func ValidateLicense(logger zerolog.Logger, licenseFile string) error {
return err
}

if len(l.Plugins) > 0 {
ref := strings.Join([]string{meta.Team, string(meta.Kind), meta.Name}, "/")
if !slices.Contains(l.Plugins, ref) {
return ErrLicenseNotApplicable
}
}

return l.IsValid(logger)
}

Expand Down Expand Up @@ -76,7 +141,7 @@ func UnpackLicense(lic []byte) (*License, error) {
}

func (l *License) IsValid(logger zerolog.Logger) error {
now := time.Now().UTC()
now := timeFunc().UTC()
if now.Before(l.ValidFrom) {
return ErrLicenseNotValidYet
}
Expand Down
102 changes: 102 additions & 0 deletions premium/offline_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package premium

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/cloudquery/plugin-sdk/v4/plugin"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)

Expand All @@ -25,3 +29,101 @@ func TestUnpackLicense(t *testing.T) {
require.Nil(t, l)
})
}

func TestValidateLicense(t *testing.T) {
publicKey = "eacdff4866c8bc0d97de3c2d7668d0970c61aa16c3f12d6ba8083147ff92c9a6"
licData := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsImlzc3VlZF9hdCI6IjIwMjMtMTItMjhUMTk6MDI6MjguODM4MzY3WiIsInZhbGlkX2Zyb20iOiIyMDIzLTEyLTI4VDE5OjAyOjI4LjgzODM2N1oiLCJleHBpcmVzX2F0IjoiMjAyMy0xMi0yOVQxOTowMjoyOC44MzgzNjdaIn0=","signature":"8687a858463764b052455b3c783d979d364b5fb653b86d88a7463e495480db62fdec7ae1a84d1e30dddee77eb769a0e498ecfc836538c53e410aeb1a0c04d102"}`
validTime := time.Date(2023, 12, 29, 12, 0, 0, 0, time.UTC)
expiredTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
nopMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}

t.Run("SingleFile", func(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "testlicense.cqlicense")
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
require.NoError(t, err)
}

t.Run("Expired", licenseTest(f, nopMeta, expiredTime, ErrLicenseExpired))
t.Run("Success", licenseTest(f, nopMeta, validTime, nil))
})
t.Run("Dir", func(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "testlicense.cqlicense")
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
require.NoError(t, err)
}
t.Run("Expired", licenseTest(dir, nopMeta, expiredTime, ErrLicenseExpired))
t.Run("Success", licenseTest(dir, nopMeta, validTime, nil))
})
}

func TestValidateSpecificLicense(t *testing.T) {
publicKey = `de452e6028fe488f56ee0dfcf5b387ee773f03d24de66f00c40ec5b17085c549`
licData := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsInBsdWdpbnMiOlsiY2xvdWRxdWVyeS9zb3VyY2UvdGVzdDEiLCJjbG91ZHF1ZXJ5L3NvdXJjZS90ZXN0MiJdLCJpc3N1ZWRfYXQiOiIyMDI0LTAxLTAyVDExOjEwOjA5LjE0OTYwNVoiLCJ2YWxpZF9mcm9tIjoiMjAyNC0wMS0wMlQxMToxMDowOS4xNDk2MDVaIiwiZXhwaXJlc19hdCI6IjIwMjQtMDEtMDNUMTE6MTA6MDkuMTQ5NjA1WiJ9","signature":"e5752577c2b2c5a8920b3277fd11504d9c6820e8acb22bc17ccda524857c1d9fc7534f39b9a122376069ad682a2b616a10d1cfae40a984fb57fee31f13a15302"}`
validTime := time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)
expiredTime := time.Date(2024, 1, 3, 12, 0, 0, 0, time.UTC)
invalidMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}
validMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test1"}

t.Run("SingleFile", func(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "testlicense.cqlicense")
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
require.NoError(t, err)
}

t.Run("Expired", licenseTest(f, validMeta, expiredTime, ErrLicenseExpired))
t.Run("Success", licenseTest(f, validMeta, validTime, nil))
t.Run("NotApplicable", licenseTest(f, invalidMeta, validTime, ErrLicenseNotApplicable))
})
t.Run("SingleDir", func(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "testlicense.cqlicense"), []byte(licData), 0644); err != nil {
require.NoError(t, err)
}
t.Run("Expired", licenseTest(dir, validMeta, expiredTime, ErrLicenseExpired))
t.Run("Success", licenseTest(dir, validMeta, validTime, nil))
t.Run("NotApplicable", licenseTest(dir, invalidMeta, validTime, ErrLicenseNotApplicable))
})
}

func TestValidateSpecificLicenseMultiFile(t *testing.T) {
publicKey = `de452e6028fe488f56ee0dfcf5b387ee773f03d24de66f00c40ec5b17085c549`
licData1 := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsInBsdWdpbnMiOlsiY2xvdWRxdWVyeS9zb3VyY2UvdGVzdDEiLCJjbG91ZHF1ZXJ5L3NvdXJjZS90ZXN0MiJdLCJpc3N1ZWRfYXQiOiIyMDI0LTAxLTAyVDExOjEwOjA5LjE0OTYwNVoiLCJ2YWxpZF9mcm9tIjoiMjAyNC0wMS0wMlQxMToxMDowOS4xNDk2MDVaIiwiZXhwaXJlc19hdCI6IjIwMjQtMDEtMDNUMTE6MTA6MDkuMTQ5NjA1WiJ9","signature":"e5752577c2b2c5a8920b3277fd11504d9c6820e8acb22bc17ccda524857c1d9fc7534f39b9a122376069ad682a2b616a10d1cfae40a984fb57fee31f13a15302"}`
licData3 := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVDMiLCJwbHVnaW5zIjpbImNsb3VkcXVlcnkvc291cmNlL3Rlc3QzIl0sImlzc3VlZF9hdCI6IjIwMjQtMDEtMDJUMTE6MjA6NTcuMzE2NDE0WiIsInZhbGlkX2Zyb20iOiIyMDI0LTAxLTAyVDExOjIwOjU3LjMxNjQxNFoiLCJleHBpcmVzX2F0IjoiMjAyNC0wMS0wM1QxMToyMDo1Ny4zMTY0MTRaIn0=","signature":"9be752d46010af84ec7295ede29915950dab13d4eca3b82b5645f793b39a03a6eef6bc653bee26e2a4f148b4d0fd54df6401059fda6104bc207f6dec2127850f"}`

validTime := time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)
expiredTime := time.Date(2024, 1, 3, 12, 0, 0, 0, time.UTC)
invalidMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}
validMeta1 := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test1"}
validMeta3 := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test3"}

dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "testlicense1.cqlicense"), []byte(licData1), 0644); err != nil {
require.NoError(t, err)
}
if err := os.WriteFile(filepath.Join(dir, "testlicense3.cqlicense"), []byte(licData3), 0644); err != nil {
require.NoError(t, err)
}

t.Run("Expired", licenseTest(dir, validMeta1, expiredTime, ErrLicenseExpired))
t.Run("Success", licenseTest(dir, validMeta1, validTime, nil))
t.Run("SuccessOther", licenseTest(dir, validMeta3, validTime, nil))
t.Run("NotApplicable", licenseTest(dir, invalidMeta, validTime, ErrLicenseNotApplicable))
}

func licenseTest(inputPath string, meta plugin.Meta, timeIs time.Time, expectError error) func(t *testing.T) {
return func(t *testing.T) {
timeFunc = func() time.Time {
return timeIs
}

err := ValidateLicense(zerolog.Nop(), meta, inputPath)
if expectError == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, expectError)
}
}
}
4 changes: 2 additions & 2 deletions serve/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (s *PluginServe) newCmdPluginServe() *cobra.Command {
otel.SetTracerProvider(tp)
}
if licenseFile != "" {
if err := premium.ValidateLicense(logger, licenseFile); err != nil {
if err := premium.ValidateLicense(logger, s.plugin.Meta(), licenseFile); err != nil {
return fmt.Errorf("failed to validate license: %w", err)
}
s.plugin.SetSkipUsageClient(true)
Expand Down Expand Up @@ -303,7 +303,7 @@ func (s *PluginServe) newCmdPluginServe() *cobra.Command {
cmd.Flags().StringArrayVar(&otelEndpointHeaders, "otel-endpoint-headers", []string{}, "Open Telemetry HTTP collector endpoint headers")
cmd.Flags().BoolVar(&otelEndpointInsecure, "otel-endpoint-insecure", false, "use Open Telemetry HTTP endpoint (for development only)")
cmd.Flags().BoolVar(&noSentry, "no-sentry", false, "disable sentry")
cmd.Flags().StringVar(&licenseFile, "license", "", "Path to offline license file")
cmd.Flags().StringVar(&licenseFile, "license", "", "Path to offline license file or directory")
sendErrors := funk.ContainsString([]string{"all", "errors"}, telemetryLevel.String())
if !sendErrors {
noSentry = true
Expand Down