Skip to content

Commit 1e5c653

Browse files
authored
feat: add license handshake and header injection (#681)
1 parent 04d34cf commit 1e5c653

File tree

3 files changed

+294
-0
lines changed

3 files changed

+294
-0
lines changed

descope/api/client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ var (
8888
exchangeAccessKey: "auth/accesskey/exchange",
8989
},
9090
mgmt: mgmtEndpoints{
91+
license: "mgmt/license",
9192
tenantCreate: "mgmt/tenant/create",
9293
tenantUpdate: "mgmt/tenant/update",
9394
tenantDelete: "mgmt/tenant/delete",
@@ -346,6 +347,7 @@ type authEndpoints struct {
346347
}
347348

348349
type mgmtEndpoints struct {
350+
license string
349351
tenantCreate string
350352
tenantUpdate string
351353
tenantDelete string
@@ -1482,6 +1484,10 @@ func (e *endpoints) ManagementDescoperSearch() string {
14821484
return path.Join(e.version, e.mgmt.descoperSearch)
14831485
}
14841486

1487+
func (e *endpoints) ManagementLicense() string {
1488+
return path.Join(e.version, e.mgmt.license)
1489+
}
1490+
14851491
type sdkInfo struct {
14861492
name string
14871493
version string
@@ -1526,6 +1532,7 @@ type Client struct {
15261532
externalRequestID func(context.Context) string
15271533
Conf ClientParams
15281534
sdkInfo *sdkInfo
1535+
licenseType string // License type from handshake (free/pro/enterprise)
15291536
}
15301537
type HTTPResponse struct {
15311538
Req *http.Request
@@ -1801,6 +1808,28 @@ func (c *Client) addDescopeHeaders(req *http.Request) {
18011808
req.Header.Set("x-descope-sdk-sha", c.sdkInfo.sha)
18021809
req.Header.Set("x-descope-sdk-uuid", instanceUUID)
18031810
req.Header.Set("x-descope-project-id", c.Conf.ProjectID)
1811+
if c.licenseType != "" {
1812+
req.Header.Set("x-descope-license", c.licenseType)
1813+
}
1814+
}
1815+
1816+
func (c *Client) FetchLicense(ctx context.Context) (string, error) {
1817+
var resp struct {
1818+
LicenseType string `json:"licenseType"`
1819+
}
1820+
opts := &HTTPRequest{ResBodyObj: &resp}
1821+
_, err := c.DoGetRequest(ctx, Routes.ManagementLicense(), opts, "")
1822+
if err != nil {
1823+
return "", err
1824+
}
1825+
if resp.LicenseType == "" {
1826+
return "", fmt.Errorf("empty license type returned from server")
1827+
}
1828+
return resp.LicenseType, nil
1829+
}
1830+
1831+
func (c *Client) SetLicenseType(licenseType string) {
1832+
c.licenseType = licenseType
18041833
}
18051834

18061835
func getSDKInfo() *sdkInfo {

descope/api/client_test.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,254 @@ func TestBaseURLForProjectID(t *testing.T) {
329329
assert.EqualValues(t, useURL, baseURLForProjectID("Puse12aAc4T2V93bddihGEx2Ryhc8e5Zfoobar"))
330330
assert.EqualValues(t, useURL, baseURLForProjectID("Puse12aAc4T2V93bddihGEx2Ryhc8e5Z"))
331331
}
332+
333+
// License Header Tests
334+
335+
func TestFetchLicense_Success(t *testing.T) {
336+
projectID := "test-project"
337+
expectedLicenseType := "enterprise"
338+
339+
c := NewClient(ClientParams{
340+
ProjectID: projectID,
341+
ManagementKey: "test-key",
342+
DefaultClient: mocks.NewTestClient(func(r *http.Request) (*http.Response, error) {
343+
// Verify the request is to the correct endpoint
344+
assert.Contains(t, r.URL.Path, "mgmt/license")
345+
assert.EqualValues(t, http.MethodGet, r.Method)
346+
347+
// Return a successful response with license type
348+
responseBody := fmt.Sprintf(`{"licenseType": "%s"}`, expectedLicenseType)
349+
return &http.Response{
350+
StatusCode: http.StatusOK,
351+
Body: io.NopCloser(strings.NewReader(responseBody)),
352+
}, nil
353+
}),
354+
})
355+
356+
licenseType, err := c.FetchLicense(context.Background())
357+
require.NoError(t, err)
358+
assert.EqualValues(t, expectedLicenseType, licenseType)
359+
}
360+
361+
func TestFetchLicense_APIError(t *testing.T) {
362+
projectID := "test-project"
363+
364+
c := NewClient(ClientParams{
365+
ProjectID: projectID,
366+
ManagementKey: "test-key",
367+
DefaultClient: mocks.NewTestClient(func(_ *http.Request) (*http.Response, error) {
368+
// Return an error response
369+
return &http.Response{
370+
StatusCode: http.StatusInternalServerError,
371+
Body: io.NopCloser(strings.NewReader(`{"errorCode": "E999999", "errorDescription": "Internal error"}`)),
372+
}, nil
373+
}),
374+
})
375+
376+
licenseType, err := c.FetchLicense(context.Background())
377+
require.Error(t, err)
378+
assert.Empty(t, licenseType)
379+
}
380+
381+
func TestFetchLicense_NetworkError(t *testing.T) {
382+
projectID := "test-project"
383+
expectedErr := fmt.Errorf("network error")
384+
385+
c := NewClient(ClientParams{
386+
ProjectID: projectID,
387+
ManagementKey: "test-key",
388+
DefaultClient: mocks.NewTestClient(func(_ *http.Request) (*http.Response, error) {
389+
return nil, expectedErr
390+
}),
391+
})
392+
393+
licenseType, err := c.FetchLicense(context.Background())
394+
require.Error(t, err)
395+
assert.Empty(t, licenseType)
396+
assert.Contains(t, err.Error(), "network error")
397+
}
398+
399+
func TestSetLicenseType(t *testing.T) {
400+
projectID := "test-project"
401+
c := NewClient(ClientParams{ProjectID: projectID})
402+
403+
// Initially, licenseType should be empty
404+
assert.Empty(t, c.licenseType)
405+
406+
// Set license type
407+
expectedLicenseType := "pro"
408+
c.SetLicenseType(expectedLicenseType)
409+
assert.EqualValues(t, expectedLicenseType, c.licenseType)
410+
411+
newLicenseType := "enterprise"
412+
c.SetLicenseType(newLicenseType)
413+
assert.EqualValues(t, newLicenseType, c.licenseType)
414+
415+
// Set to empty string
416+
c.SetLicenseType("")
417+
assert.Empty(t, c.licenseType)
418+
}
419+
420+
func TestLicenseHeader_AddedWhenSet(t *testing.T) {
421+
projectID := "test-project"
422+
expectedLicenseType := "enterprise"
423+
headerChecked := false
424+
425+
c := NewClient(ClientParams{
426+
ProjectID: projectID,
427+
DefaultClient: mocks.NewTestClient(func(r *http.Request) (*http.Response, error) {
428+
// Verify the license header is present
429+
actualLicense := r.Header.Get("x-descope-license")
430+
assert.EqualValues(t, expectedLicenseType, actualLicense)
431+
headerChecked = true
432+
return &http.Response{StatusCode: http.StatusOK}, nil
433+
}),
434+
})
435+
436+
// Set the license type
437+
c.SetLicenseType(expectedLicenseType)
438+
439+
// Make a request and verify the header is added
440+
_, err := c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
441+
require.NoError(t, err)
442+
assert.True(t, headerChecked, "License header check was not performed")
443+
}
444+
445+
func TestLicenseHeader_NotAddedWhenEmpty(t *testing.T) {
446+
projectID := "test-project"
447+
headerChecked := false
448+
449+
c := NewClient(ClientParams{
450+
ProjectID: projectID,
451+
DefaultClient: mocks.NewTestClient(func(r *http.Request) (*http.Response, error) {
452+
// Verify the license header is NOT present
453+
actualLicense := r.Header.Get("x-descope-license")
454+
assert.Empty(t, actualLicense, "License header should not be present when licenseType is empty")
455+
headerChecked = true
456+
return &http.Response{StatusCode: http.StatusOK}, nil
457+
}),
458+
})
459+
460+
// Do NOT set license type (should remain empty)
461+
assert.Empty(t, c.licenseType)
462+
463+
// Make a request and verify the header is NOT added
464+
_, err := c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
465+
require.NoError(t, err)
466+
assert.True(t, headerChecked, "License header check was not performed")
467+
}
468+
469+
func TestLicenseHeader_AddedToAllRequestTypes(t *testing.T) {
470+
projectID := "test-project"
471+
expectedLicenseType := "pro"
472+
473+
tests := []struct {
474+
name string
475+
requestFunc func(*Client) error
476+
}{
477+
{
478+
name: "POST request",
479+
requestFunc: func(c *Client) error {
480+
_, err := c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
481+
return err
482+
},
483+
},
484+
{
485+
name: "GET request",
486+
requestFunc: func(c *Client) error {
487+
_, err := c.DoGetRequest(context.Background(), "test-path", nil, "")
488+
return err
489+
},
490+
},
491+
{
492+
name: "PUT request",
493+
requestFunc: func(c *Client) error {
494+
_, err := c.DoPutRequest(context.Background(), "test-path", nil, nil, "")
495+
return err
496+
},
497+
},
498+
{
499+
name: "DELETE request",
500+
requestFunc: func(c *Client) error {
501+
_, err := c.DoDeleteRequest(context.Background(), "test-path", nil, "")
502+
return err
503+
},
504+
},
505+
}
506+
507+
for _, tt := range tests {
508+
t.Run(tt.name, func(t *testing.T) {
509+
headerChecked := false
510+
511+
c := NewClient(ClientParams{
512+
ProjectID: projectID,
513+
DefaultClient: mocks.NewTestClient(func(r *http.Request) (*http.Response, error) {
514+
// Verify the license header is present
515+
actualLicense := r.Header.Get("x-descope-license")
516+
assert.EqualValues(t, expectedLicenseType, actualLicense)
517+
headerChecked = true
518+
return &http.Response{StatusCode: http.StatusOK}, nil
519+
}),
520+
})
521+
522+
// Set the license type
523+
c.SetLicenseType(expectedLicenseType)
524+
525+
// Execute the request
526+
err := tt.requestFunc(c)
527+
require.NoError(t, err)
528+
assert.True(t, headerChecked, "License header check was not performed")
529+
})
530+
}
531+
}
532+
533+
func TestLicenseHeader_UpdatedDynamically(t *testing.T) {
534+
projectID := "test-project"
535+
requestCount := 0
536+
537+
c := NewClient(ClientParams{
538+
ProjectID: projectID,
539+
DefaultClient: mocks.NewTestClient(func(r *http.Request) (*http.Response, error) {
540+
requestCount++
541+
actualLicense := r.Header.Get("x-descope-license")
542+
543+
switch requestCount {
544+
case 1:
545+
// First request: no license header
546+
assert.Empty(t, actualLicense)
547+
case 2:
548+
// Second request: "free" license
549+
assert.EqualValues(t, "free", actualLicense)
550+
case 3:
551+
// Third request: "enterprise" license
552+
assert.EqualValues(t, "enterprise", actualLicense)
553+
case 4:
554+
// Fourth request: no license header again
555+
assert.Empty(t, actualLicense)
556+
}
557+
558+
return &http.Response{StatusCode: http.StatusOK}, nil
559+
}),
560+
})
561+
562+
// Request 1: No license set
563+
_, err := c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
564+
require.NoError(t, err)
565+
566+
// Request 2: Set to "free"
567+
c.SetLicenseType("free")
568+
_, err = c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
569+
require.NoError(t, err)
570+
571+
// Request 3: Update to "enterprise"
572+
c.SetLicenseType("enterprise")
573+
_, err = c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
574+
require.NoError(t, err)
575+
576+
// Request 4: Clear license
577+
c.SetLicenseType("")
578+
_, err = c.DoPostRequest(context.Background(), "test-path", nil, nil, "")
579+
require.NoError(t, err)
580+
581+
assert.EqualValues(t, 4, requestCount, "Expected 4 requests to be made")
582+
}

descope/client/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package client
22

33
import (
4+
"context"
5+
"time"
6+
47
"github.com/descope/go-sdk/descope"
58
"github.com/descope/go-sdk/descope/api"
69
"github.com/descope/go-sdk/descope/internal/auth"
@@ -86,6 +89,17 @@ func NewWithConfig(config *Config) (*DescopeClient, error) {
8689
CertificateVerify: config.CertificateVerify,
8790
RequestTimeout: config.RequestTimeout,
8891
})
92+
93+
if config.ManagementKey != "" {
94+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
95+
defer cancel()
96+
if licenseType, err := mgmtClient.FetchLicense(ctx); err != nil {
97+
logger.LogInfo("License handshake failed, continuing without header: %v", err)
98+
} else {
99+
mgmtClient.SetLicenseType(licenseType)
100+
}
101+
}
102+
89103
managementService := mgmt.NewManagement(mgmt.ManagementParams{ProjectID: config.ProjectID, FGACacheURL: config.FGACacheURL}, provider, mgmtClient)
90104

91105
return &DescopeClient{Auth: authService, Management: managementService, config: config}, nil

0 commit comments

Comments
 (0)