Skip to content

Commit 993e352

Browse files
authored
feat: Support multiple and/or specific plugin licenses (#1451)
as requested.
1 parent 986d733 commit 993e352

File tree

4 files changed

+182
-11
lines changed

4 files changed

+182
-11
lines changed

plugin/plugin.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ func (p *Plugin) Version() string {
128128
return p.version
129129
}
130130

131+
func (p *Plugin) Meta() Meta {
132+
return Meta{
133+
Team: p.team,
134+
Kind: cqapi.PluginKind(p.kind),
135+
Name: p.name,
136+
SkipUsageClient: p.skipUsageClient,
137+
}
138+
}
139+
131140
// SetSkipUsageClient sets whether the usage client should be skipped
132141
func (p *Plugin) SetSkipUsageClient(v bool) {
133142
p.skipUsageClient = v
@@ -205,12 +214,7 @@ func (p *Plugin) Init(ctx context.Context, spec []byte, options NewClientOptions
205214
}
206215
}
207216

208-
options.PluginMeta = Meta{
209-
Team: p.team,
210-
Kind: cqapi.PluginKind(p.kind),
211-
Name: p.name,
212-
SkipUsageClient: p.skipUsageClient,
213-
}
217+
options.PluginMeta = p.Meta()
214218

215219
p.client, err = p.newClient(ctx, p.logger, spec, options)
216220
if err != nil {

premium/offline.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import (
77
"encoding/json"
88
"errors"
99
"os"
10+
"path/filepath"
11+
"slices"
12+
"strings"
1013
"time"
1114

15+
"github.com/cloudquery/plugin-sdk/v4/plugin"
1216
"github.com/rs/zerolog"
1317
)
1418

1519
type License struct {
16-
LicensedTo string `json:"licensed_to"` // Customers name, e.g. "Acme Inc"
20+
LicensedTo string `json:"licensed_to"` // Customers name, e.g. "Acme Inc"
21+
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.
1722
IssuedAt time.Time `json:"issued_at"`
1823
ValidFrom time.Time `json:"valid_from"`
1924
ExpiresAt time.Time `json:"expires_at"`
@@ -28,12 +33,65 @@ var (
2833
ErrInvalidLicenseSignature = errors.New("invalid license signature")
2934
ErrLicenseNotValidYet = errors.New("license not valid yet")
3035
ErrLicenseExpired = errors.New("license expired")
36+
ErrLicenseNotApplicable = errors.New("license not applicable to this plugin")
3137
)
3238

3339
//go:embed offline.key
3440
var publicKey string
3541

36-
func ValidateLicense(logger zerolog.Logger, licenseFile string) error {
42+
var timeFunc = time.Now
43+
44+
func ValidateLicense(logger zerolog.Logger, meta plugin.Meta, licenseFileOrDirectory string) error {
45+
fi, err := os.Stat(licenseFileOrDirectory)
46+
if err != nil {
47+
return err
48+
}
49+
if !fi.IsDir() {
50+
return validateLicenseFile(logger, meta, licenseFileOrDirectory)
51+
}
52+
53+
found := false
54+
var lastError error
55+
err = filepath.WalkDir(licenseFileOrDirectory, func(path string, d os.DirEntry, err error) error {
56+
if d.IsDir() {
57+
if path == licenseFileOrDirectory {
58+
return nil
59+
}
60+
return filepath.SkipDir
61+
}
62+
if err != nil {
63+
return err
64+
}
65+
66+
if filepath.Ext(path) != ".cqlicense" {
67+
return nil
68+
}
69+
70+
logger.Debug().Str("path", path).Msg("considering license file")
71+
lastError = validateLicenseFile(logger, meta, path)
72+
switch lastError {
73+
case nil:
74+
found = true
75+
return filepath.SkipAll
76+
case ErrLicenseNotApplicable:
77+
return nil
78+
default:
79+
return lastError
80+
}
81+
})
82+
if err != nil {
83+
return err
84+
}
85+
if found {
86+
return nil
87+
}
88+
if lastError != nil {
89+
return lastError
90+
}
91+
return errors.New("failed to validate license directory")
92+
}
93+
94+
func validateLicenseFile(logger zerolog.Logger, meta plugin.Meta, licenseFile string) error {
3795
licenseContents, err := os.ReadFile(licenseFile)
3896
if err != nil {
3997
return err
@@ -44,6 +102,13 @@ func ValidateLicense(logger zerolog.Logger, licenseFile string) error {
44102
return err
45103
}
46104

105+
if len(l.Plugins) > 0 {
106+
ref := strings.Join([]string{meta.Team, string(meta.Kind), meta.Name}, "/")
107+
if !slices.Contains(l.Plugins, ref) {
108+
return ErrLicenseNotApplicable
109+
}
110+
}
111+
47112
return l.IsValid(logger)
48113
}
49114

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

78143
func (l *License) IsValid(logger zerolog.Logger) error {
79-
now := time.Now().UTC()
144+
now := timeFunc().UTC()
80145
if now.Before(l.ValidFrom) {
81146
return ErrLicenseNotValidYet
82147
}

premium/offline_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package premium
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57
"time"
68

9+
"github.com/cloudquery/plugin-sdk/v4/plugin"
10+
"github.com/rs/zerolog"
711
"github.com/stretchr/testify/require"
812
)
913

@@ -25,3 +29,101 @@ func TestUnpackLicense(t *testing.T) {
2529
require.Nil(t, l)
2630
})
2731
}
32+
33+
func TestValidateLicense(t *testing.T) {
34+
publicKey = "eacdff4866c8bc0d97de3c2d7668d0970c61aa16c3f12d6ba8083147ff92c9a6"
35+
licData := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsImlzc3VlZF9hdCI6IjIwMjMtMTItMjhUMTk6MDI6MjguODM4MzY3WiIsInZhbGlkX2Zyb20iOiIyMDIzLTEyLTI4VDE5OjAyOjI4LjgzODM2N1oiLCJleHBpcmVzX2F0IjoiMjAyMy0xMi0yOVQxOTowMjoyOC44MzgzNjdaIn0=","signature":"8687a858463764b052455b3c783d979d364b5fb653b86d88a7463e495480db62fdec7ae1a84d1e30dddee77eb769a0e498ecfc836538c53e410aeb1a0c04d102"}`
36+
validTime := time.Date(2023, 12, 29, 12, 0, 0, 0, time.UTC)
37+
expiredTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
38+
nopMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}
39+
40+
t.Run("SingleFile", func(t *testing.T) {
41+
dir := t.TempDir()
42+
f := filepath.Join(dir, "testlicense.cqlicense")
43+
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
44+
require.NoError(t, err)
45+
}
46+
47+
t.Run("Expired", licenseTest(f, nopMeta, expiredTime, ErrLicenseExpired))
48+
t.Run("Success", licenseTest(f, nopMeta, validTime, nil))
49+
})
50+
t.Run("Dir", func(t *testing.T) {
51+
dir := t.TempDir()
52+
f := filepath.Join(dir, "testlicense.cqlicense")
53+
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
54+
require.NoError(t, err)
55+
}
56+
t.Run("Expired", licenseTest(dir, nopMeta, expiredTime, ErrLicenseExpired))
57+
t.Run("Success", licenseTest(dir, nopMeta, validTime, nil))
58+
})
59+
}
60+
61+
func TestValidateSpecificLicense(t *testing.T) {
62+
publicKey = `de452e6028fe488f56ee0dfcf5b387ee773f03d24de66f00c40ec5b17085c549`
63+
licData := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsInBsdWdpbnMiOlsiY2xvdWRxdWVyeS9zb3VyY2UvdGVzdDEiLCJjbG91ZHF1ZXJ5L3NvdXJjZS90ZXN0MiJdLCJpc3N1ZWRfYXQiOiIyMDI0LTAxLTAyVDExOjEwOjA5LjE0OTYwNVoiLCJ2YWxpZF9mcm9tIjoiMjAyNC0wMS0wMlQxMToxMDowOS4xNDk2MDVaIiwiZXhwaXJlc19hdCI6IjIwMjQtMDEtMDNUMTE6MTA6MDkuMTQ5NjA1WiJ9","signature":"e5752577c2b2c5a8920b3277fd11504d9c6820e8acb22bc17ccda524857c1d9fc7534f39b9a122376069ad682a2b616a10d1cfae40a984fb57fee31f13a15302"}`
64+
validTime := time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)
65+
expiredTime := time.Date(2024, 1, 3, 12, 0, 0, 0, time.UTC)
66+
invalidMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}
67+
validMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test1"}
68+
69+
t.Run("SingleFile", func(t *testing.T) {
70+
dir := t.TempDir()
71+
f := filepath.Join(dir, "testlicense.cqlicense")
72+
if err := os.WriteFile(f, []byte(licData), 0644); err != nil {
73+
require.NoError(t, err)
74+
}
75+
76+
t.Run("Expired", licenseTest(f, validMeta, expiredTime, ErrLicenseExpired))
77+
t.Run("Success", licenseTest(f, validMeta, validTime, nil))
78+
t.Run("NotApplicable", licenseTest(f, invalidMeta, validTime, ErrLicenseNotApplicable))
79+
})
80+
t.Run("SingleDir", func(t *testing.T) {
81+
dir := t.TempDir()
82+
if err := os.WriteFile(filepath.Join(dir, "testlicense.cqlicense"), []byte(licData), 0644); err != nil {
83+
require.NoError(t, err)
84+
}
85+
t.Run("Expired", licenseTest(dir, validMeta, expiredTime, ErrLicenseExpired))
86+
t.Run("Success", licenseTest(dir, validMeta, validTime, nil))
87+
t.Run("NotApplicable", licenseTest(dir, invalidMeta, validTime, ErrLicenseNotApplicable))
88+
})
89+
}
90+
91+
func TestValidateSpecificLicenseMultiFile(t *testing.T) {
92+
publicKey = `de452e6028fe488f56ee0dfcf5b387ee773f03d24de66f00c40ec5b17085c549`
93+
licData1 := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVCIsInBsdWdpbnMiOlsiY2xvdWRxdWVyeS9zb3VyY2UvdGVzdDEiLCJjbG91ZHF1ZXJ5L3NvdXJjZS90ZXN0MiJdLCJpc3N1ZWRfYXQiOiIyMDI0LTAxLTAyVDExOjEwOjA5LjE0OTYwNVoiLCJ2YWxpZF9mcm9tIjoiMjAyNC0wMS0wMlQxMToxMDowOS4xNDk2MDVaIiwiZXhwaXJlc19hdCI6IjIwMjQtMDEtMDNUMTE6MTA6MDkuMTQ5NjA1WiJ9","signature":"e5752577c2b2c5a8920b3277fd11504d9c6820e8acb22bc17ccda524857c1d9fc7534f39b9a122376069ad682a2b616a10d1cfae40a984fb57fee31f13a15302"}`
94+
licData3 := `{"license":"eyJsaWNlbnNlZF90byI6IlVOTElDRU5TRUQgVEVTVDMiLCJwbHVnaW5zIjpbImNsb3VkcXVlcnkvc291cmNlL3Rlc3QzIl0sImlzc3VlZF9hdCI6IjIwMjQtMDEtMDJUMTE6MjA6NTcuMzE2NDE0WiIsInZhbGlkX2Zyb20iOiIyMDI0LTAxLTAyVDExOjIwOjU3LjMxNjQxNFoiLCJleHBpcmVzX2F0IjoiMjAyNC0wMS0wM1QxMToyMDo1Ny4zMTY0MTRaIn0=","signature":"9be752d46010af84ec7295ede29915950dab13d4eca3b82b5645f793b39a03a6eef6bc653bee26e2a4f148b4d0fd54df6401059fda6104bc207f6dec2127850f"}`
95+
96+
validTime := time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)
97+
expiredTime := time.Date(2024, 1, 3, 12, 0, 0, 0, time.UTC)
98+
invalidMeta := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test"}
99+
validMeta1 := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test1"}
100+
validMeta3 := plugin.Meta{Team: "cloudquery", Kind: "source", Name: "test3"}
101+
102+
dir := t.TempDir()
103+
if err := os.WriteFile(filepath.Join(dir, "testlicense1.cqlicense"), []byte(licData1), 0644); err != nil {
104+
require.NoError(t, err)
105+
}
106+
if err := os.WriteFile(filepath.Join(dir, "testlicense3.cqlicense"), []byte(licData3), 0644); err != nil {
107+
require.NoError(t, err)
108+
}
109+
110+
t.Run("Expired", licenseTest(dir, validMeta1, expiredTime, ErrLicenseExpired))
111+
t.Run("Success", licenseTest(dir, validMeta1, validTime, nil))
112+
t.Run("SuccessOther", licenseTest(dir, validMeta3, validTime, nil))
113+
t.Run("NotApplicable", licenseTest(dir, invalidMeta, validTime, ErrLicenseNotApplicable))
114+
}
115+
116+
func licenseTest(inputPath string, meta plugin.Meta, timeIs time.Time, expectError error) func(t *testing.T) {
117+
return func(t *testing.T) {
118+
timeFunc = func() time.Time {
119+
return timeIs
120+
}
121+
122+
err := ValidateLicense(zerolog.Nop(), meta, inputPath)
123+
if expectError == nil {
124+
require.NoError(t, err)
125+
} else {
126+
require.ErrorIs(t, err, expectError)
127+
}
128+
}
129+
}

serve/plugin.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (s *PluginServe) newCmdPluginServe() *cobra.Command {
189189
otel.SetTracerProvider(tp)
190190
}
191191
if licenseFile != "" {
192-
if err := premium.ValidateLicense(logger, licenseFile); err != nil {
192+
if err := premium.ValidateLicense(logger, s.plugin.Meta(), licenseFile); err != nil {
193193
return fmt.Errorf("failed to validate license: %w", err)
194194
}
195195
s.plugin.SetSkipUsageClient(true)
@@ -303,7 +303,7 @@ func (s *PluginServe) newCmdPluginServe() *cobra.Command {
303303
cmd.Flags().StringArrayVar(&otelEndpointHeaders, "otel-endpoint-headers", []string{}, "Open Telemetry HTTP collector endpoint headers")
304304
cmd.Flags().BoolVar(&otelEndpointInsecure, "otel-endpoint-insecure", false, "use Open Telemetry HTTP endpoint (for development only)")
305305
cmd.Flags().BoolVar(&noSentry, "no-sentry", false, "disable sentry")
306-
cmd.Flags().StringVar(&licenseFile, "license", "", "Path to offline license file")
306+
cmd.Flags().StringVar(&licenseFile, "license", "", "Path to offline license file or directory")
307307
sendErrors := funk.ContainsString([]string{"all", "errors"}, telemetryLevel.String())
308308
if !sendErrors {
309309
noSentry = true

0 commit comments

Comments
 (0)