Skip to content

Commit

Permalink
add machine components
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg committed Sep 8, 2023
1 parent b373f1f commit d64a65b
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 50 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ details. Then prompt the end-user for their license key or token and set `keygen
or `keygen.Token`, respectively.

The `Validate` method accepts zero or more fingerprints, which can be used to scope a license
validation to a particular fingerprint. It will return a `License` object as well as any
validation errors that occur. The `License` object can be used to perform additional actions,
such as `license.Activate(fingerprint)`.
validation to a particular device fingerprint and its hardware components. It will return a
`License` object as well as any validation errors that occur. The `License` object can be
used to perform additional actions, such as `license.Activate(fingerprint)`.

```go
license, err := keygen.Validate(fingerprint)
Expand Down
4 changes: 4 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ func (c *Client) send(req *http.Request, model interface{}) (*Response, error) {
return response, ErrMachineLimitExceeded
case code == ErrorCodeProcessLimitExceeded:
return response, ErrProcessLimitExceeded
case code == ErrorCodeComponentFingerprintConflict:
return response, ErrComponentConflict
case code == ErrorCodeComponentFingerprintTaken:
return response, ErrComponentAlreadyActivated
case code == ErrorCodeTokenInvalid:
return response, &LicenseTokenError{err}
case code == ErrorCodeLicenseInvalid:
Expand Down
111 changes: 111 additions & 0 deletions component.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package keygen

import (
"time"

"github.com/keygen-sh/jsonapi-go"
)

type component struct {
ID string `json:"-"`
Type string `json:"-"`
Fingerprint string `json:"fingerprint"`
Name string `json:"name"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
MachineID string `json:"-"`
}

// GetID implements the jsonapi.MarshalResourceIdentifier interface.
func (c component) GetID() string {
return c.ID
}

// GetType implements the jsonapi.MarshalResourceIdentifier interface.
func (c component) GetType() string {
return "components"
}

// GetData implements the jsonapi.MarshalData interface.
func (c component) GetData() interface{} {
return c
}

// GetRelationships implements jsonapi.MarshalRelationships interface.
func (c component) GetRelationships() map[string]interface{} {
relationships := make(map[string]interface{})

if c.MachineID != "" {
relationships["machine"] = jsonapi.ResourceObjectIdentifier{
Type: "machines",
ID: c.MachineID,
}
}

if len(relationships) == 0 {
return nil
}

return relationships
}

// Component represents a Keygen component object.
type Component struct {
ID string `json:"-"`
Type string `json:"-"`
Fingerprint string `json:"fingerprint"`
Name string `json:"name"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Metadata map[string]interface{} `json:"metadata"`
MachineID string `json:"-"`
}

// GetID implements the jsonapi.MarshalResourceIdentifier interface.
func (c Component) GetID() string {
return c.ID
}

// GetType implements the jsonapi.MarshalResourceIdentifier interface.
func (c Component) GetType() string {
return "components"
}

// GetData implements the jsonapi.MarshalData interface.
func (c Component) GetData() interface{} {
// Transform public component to private component to only send a subset of attrs
return component{
Fingerprint: c.Fingerprint,
Name: c.Name,
Metadata: c.Metadata,
MachineID: c.MachineID,
}
}

func (c Component) UseExperimentalEmbeddedRelationshipData() bool {
return true
}

// SetID implements the jsonapi.UnmarshalResourceIdentifier interface.
func (c *Component) SetID(id string) error {
c.ID = id
return nil
}

// SetType implements the jsonapi.UnmarshalResourceIdentifier interface.
func (c *Component) SetType(t string) error {
c.Type = t
return nil
}

// SetData implements the jsonapi.UnmarshalData interface.
func (c *Component) SetData(to func(target interface{}) error) error {
return to(c)
}

// Components represents an array of component objects.
type Components []Component

// SetData implements the jsonapi.UnmarshalData interface.
func (c *Components) SetData(to func(target interface{}) error) error {
return to(c)
}
30 changes: 18 additions & 12 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import (
type ErrorCode string

const (
ErrorCodeEnvironmentInvalid ErrorCode = "ENVIRONMENT_INVALID"
ErrorCodeEnvironmentNotSupported ErrorCode = "ENVIRONMENT_NOT_SUPPORTED"
ErrorCodeTokenInvalid ErrorCode = "TOKEN_INVALID"
ErrorCodeLicenseInvalid ErrorCode = "LICENSE_INVALID"
ErrorCodeLicenseExpired ErrorCode = "LICENSE_EXPIRED"
ErrorCodeLicenseSuspended ErrorCode = "LICENSE_SUSPENDED"
ErrorCodeFingerprintTaken ErrorCode = "FINGERPRINT_TAKEN"
ErrorCodeMachineLimitExceeded ErrorCode = "MACHINE_LIMIT_EXCEEDED"
ErrorCodeProcessLimitExceeded ErrorCode = "MACHINE_PROCESS_LIMIT_EXCEEDED"
ErrorCodeMachineHeartbeatDead ErrorCode = "MACHINE_HEARTBEAT_DEAD"
ErrorCodeProcessHeartbeatDead ErrorCode = "PROCESS_HEARTBEAT_DEAD"
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
ErrorCodeEnvironmentInvalid ErrorCode = "ENVIRONMENT_INVALID"
ErrorCodeEnvironmentNotSupported ErrorCode = "ENVIRONMENT_NOT_SUPPORTED"
ErrorCodeTokenInvalid ErrorCode = "TOKEN_INVALID"
ErrorCodeLicenseInvalid ErrorCode = "LICENSE_INVALID"
ErrorCodeLicenseExpired ErrorCode = "LICENSE_EXPIRED"
ErrorCodeLicenseSuspended ErrorCode = "LICENSE_SUSPENDED"
ErrorCodeFingerprintTaken ErrorCode = "FINGERPRINT_TAKEN"
ErrorCodeMachineLimitExceeded ErrorCode = "MACHINE_LIMIT_EXCEEDED"
ErrorCodeProcessLimitExceeded ErrorCode = "MACHINE_PROCESS_LIMIT_EXCEEDED"
ErrorCodeComponentFingerprintConflict ErrorCode = "COMPONENTS_FINGERPRINT_CONFLICT"
ErrorCodeComponentFingerprintTaken ErrorCode = "COMPONENTS_FINGERPRINT_TAKEN"
ErrorCodeMachineHeartbeatDead ErrorCode = "MACHINE_HEARTBEAT_DEAD"
ErrorCodeProcessHeartbeatDead ErrorCode = "PROCESS_HEARTBEAT_DEAD"
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
)

// Error represents an API error response.
Expand Down Expand Up @@ -116,6 +118,7 @@ var (
ErrPublicKeyMissing = errors.New("public key is missing")
ErrPublicKeyInvalid = errors.New("public key is invalid")
ErrValidationFingerprintMissing = errors.New("validation fingerprint scope is missing")
ErrValidationComponentsMissing = errors.New("validation components scope is missing")
ErrValidationProductMissing = errors.New("validation product scope is missing")
ErrHeartbeatPingFailed = errors.New("heartbeat ping failed")
ErrHeartbeatRequired = errors.New("heartbeat is required")
Expand All @@ -128,6 +131,9 @@ var (
ErrMachineFileNotEncrypted = errors.New("machine file is not encrypted")
ErrMachineFileNotGenuine = errors.New("machine file is not genuine")
ErrMachineFileExpired = errors.New("machine file is expired")
ErrComponentConflict = errors.New("component is duplicated")
ErrComponentAlreadyActivated = errors.New("component already exists")
ErrComponentNotActivated = errors.New("component is not activated")
ErrProcessLimitExceeded = errors.New("process limit has been exceeded")
ErrLicenseSchemeNotSupported = errors.New("license scheme is not supported")
ErrLicenseSchemeMissing = errors.New("license scheme is missing")
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/keygen-sh/go-update v1.0.0
github.com/keygen-sh/jsonapi-go v1.2.0
github.com/keygen-sh/jsonapi-go v1.2.1
github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94
)

require (
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/keygen-sh/go-update v1.0.0 h1:M65sTVUHUO07tEK4l1Hq7u5D4kdEqkcgfdzUt3q3S08=
github.com/keygen-sh/go-update v1.0.0/go.mod h1:wn0UWRHLnBP5hwXtj1IdHZqWlHvIadh2Nn+becFf8Ro=
github.com/keygen-sh/jsonapi-go v1.2.0 h1:BXVX9YAZt4itgTs9sV0ame0/+6WQMZHhbF8f62paNwo=
github.com/keygen-sh/jsonapi-go v1.2.0/go.mod h1:8j9vsLiKyJyDqmt8r3tYaYNmXszq2+cFhoO6QdMdAes=
github.com/keygen-sh/jsonapi-go v1.2.1 h1:NTSIAxl2+7S5fPnKgrYwNjQSWbdKRtrFq26SD8AOkiU=
github.com/keygen-sh/jsonapi-go v1.2.1/go.mod h1:8j9vsLiKyJyDqmt8r3tYaYNmXszq2+cFhoO6QdMdAes=
github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94 h1:YXfl+eCNmAQhVbSNQ85bSi1n4qhUBPW8Qq9Rac4pt/s=
github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94/go.mod h1:WUcXjUd98qaCVFb6j8Xc87MsKeMCXDu9Nk8JRJ9SeC8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand All @@ -46,8 +46,9 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
Expand All @@ -64,5 +65,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
105 changes: 100 additions & 5 deletions keygen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func TestValidate(t *testing.T) {
t.Fatalf("Should fingerprint the current machine: err=%v", err)
}

if _, err := Validate(); err != ErrValidationFingerprintMissing {
t.Fatalf("Should have a required scope: err=%v", err)
}

license, err := Validate(fingerprint)
if err == nil {
t.Fatalf("Should not be activated: err=%v", err)
Expand Down Expand Up @@ -107,7 +111,7 @@ func TestValidate(t *testing.T) {
case dataset.License.ID != license.ID:
t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID)
case len(dataset.Entitlements) == 0:
t.Fatalf("Should have at least 1 entitlement: entitlements=%s", dataset.Entitlements)
t.Fatalf("Should have at least 1 entitlement: entitlements=%v", dataset.Entitlements)
case dataset.Issued.IsZero():
t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued)
case dataset.Expiry.IsZero():
Expand Down Expand Up @@ -146,7 +150,7 @@ func TestValidate(t *testing.T) {
case dataset.License.ID != license.ID:
t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID)
case len(dataset.Entitlements) != 0:
t.Fatalf("Should have no entitlements: entitlements=%s", dataset.Entitlements)
t.Fatalf("Should have no entitlements: entitlements=%v", dataset.Entitlements)
case dataset.Issued.IsZero():
t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued)
case time.Until(dataset.Expiry) > 24*time.Hour+30*time.Second: // 30s for network lag
Expand Down Expand Up @@ -189,7 +193,7 @@ func TestValidate(t *testing.T) {
case dataset.License.ID != license.ID:
t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID)
case len(dataset.Entitlements) == 0:
t.Fatalf("Should have at least 1 entitlement: entitlements=%s", dataset.Entitlements)
t.Fatalf("Should have at least 1 entitlement: entitlements=%v", dataset.Entitlements)
case dataset.Issued.IsZero():
t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued)
case dataset.Expiry.IsZero():
Expand All @@ -204,7 +208,7 @@ func TestValidate(t *testing.T) {
// options
{
mic, err := machine.Checkout(
CheckoutInclude("license"),
CheckoutInclude("license", "components"),
CheckoutTTL(24*time.Hour*365),
)
if err != nil {
Expand All @@ -230,7 +234,9 @@ func TestValidate(t *testing.T) {
case dataset.License.ID != license.ID:
t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID)
case len(dataset.Entitlements) != 0:
t.Fatalf("Should have no entitlements: entitlements=%s", dataset.Entitlements)
t.Fatalf("Should have no entitlements: entitlements=%v", dataset.Entitlements)
case len(dataset.Components) != 0:
t.Fatalf("Should have no components: components=%v", dataset.Components)
case dataset.Issued.IsZero():
t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued)
case time.Until(dataset.Expiry) < 24*time.Hour*365:
Expand Down Expand Up @@ -353,6 +359,95 @@ func TestValidate(t *testing.T) {
t.Fatalf("Should not fail to list entitlements: err=%v", err)
}

if len(entitlements) == 0 {
t.Fatalf("Should have entitlements: entitlements=%v", entitlements)
}

// Components
{
board := uuid.NewString()
disk := uuid.NewString()
cpu := uuid.NewString()
gpu := uuid.NewString()

machine, err := license.Activate(fingerprint,
Component{Name: "Board", Fingerprint: board},
Component{Name: "Drive", Fingerprint: disk},
Component{Name: "CPU", Fingerprint: cpu},
Component{Name: "GPU", Fingerprint: gpu},
)
if err != nil {
t.Fatalf("Should not fail reactivation: err=%v", err)
}

components, err := machine.Components()
if err != nil {
t.Fatalf("Should not fail to list components: err=%v", err)
}

if len(components) == 0 {
t.Fatalf("Should have components: components=%v", components)
}

license, err = Validate(fingerprint, board, disk, gpu, cpu)
if err != nil {
t.Fatalf("Should be valid: err=%v", err)
}

switch {
case license.LastValidation.Scope.Fingerprint != fingerprint:
t.Fatalf("Should be scoped to fingerprint: scope=%v", license.LastValidation)
case len(license.LastValidation.Scope.Components) != 4:
t.Fatalf("Should be scoped to components: scope=%v", license.LastValidation)
}

if _, err = Validate(fingerprint, uuid.NewString()); err != ErrComponentNotActivated {
t.Fatalf("Should be invalid: err=%v", err)
}

mic, err := machine.Checkout(
CheckoutInclude("components", "license", "license.entitlements"),
)
if err != nil {
t.Fatalf("Should not fail checkout: err=%v", err)
}

err = mic.Verify()
switch {
case err == ErrLicenseFileNotGenuine:
t.Fatalf("Should be a genuine machine file: err=%v", err)
case err != nil:
t.Fatalf("Should not fail genuine check: err=%v", err)
}

dataset, err := mic.Decrypt(license.Key + machine.Fingerprint)
if err != nil {
t.Fatalf("Should not fail decrypt: err=%v", err)
}

switch {
case dataset.Machine.ID != machine.ID:
t.Fatalf("Should have the correct machine ID: actual=%s expected=%s", dataset.Machine.ID, machine.ID)
case dataset.License.ID != license.ID:
t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID)
case len(dataset.Entitlements) == 0:
t.Fatalf("Should have entitlements: entitlements=%v", dataset.Entitlements)
case len(dataset.Components) != 4:
t.Fatalf("Should have components: components=%v", dataset.Components)
case dataset.Issued.IsZero():
t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued)
case dataset.Expiry.IsZero():
t.Fatalf("Should have an expiry timestamp: ts=%v", dataset.Expiry)
case dataset.TTL == 0:
t.Fatalf("Should have a TTL: ttl=%d", dataset.TTL)
}

err = machine.Deactivate()
if err != nil {
t.Fatalf("Should not fail deactivation: err=%v", err)
}
}

t.Logf(
"license=%+v machines=%+v entitlements=%+v",
license,
Expand Down
Loading

0 comments on commit d64a65b

Please sign in to comment.