mirror of
https://github.com/coder/coder.git
synced 2025-03-14 10:09:57 +00:00
Addresses https://github.com/coder/coder/issues/16455. ## Changes - Initialize default entitlements in a Set to include all features - Initialize entitlements' `Warnings` and `Errors` fields to arrays rather than `nil`s. - Minor changes in formatting on the frontend ## Reasoning I had to change how entitlements are initialized to match the `codersdk` [generated types](33d6261922/site/src/api/typesGenerated.ts (L727)
), which the frontend assumes are correct, and doesn't run additional checks on. - `features: Record<FeatureName, Feature>`: this type signifies that every `FeatureName` is present in the record, but on `main`, that's not true if there's a telemetry required error - `warnings: readonly string[];` and `errors: readonly string[];`: these types mean that the fields are not `null`, but that's not always true With a valid license, the [`LicensesEntitlements` function](33d6261922/enterprise/coderd/license/license.go (L92)
) ensures that all features are present in the entitlements. It's called by the [`Entitlements` function](33d6261922/enterprise/coderd/license/license.go (L42)
), which is called by [`api.updateEnittlements`](33d6261922/enterprise/coderd/coderd.go (L687)
). However, when a license requires telemetry and telemetry is disabled, the entitlements with all features [are discarded](33d6261922/enterprise/coderd/coderd.go (L704)
) in an early exit from the same function. By initializing entitlements with all the features from the get go, we avoid this problem. ## License issue banner after the changes <img width="1512" alt="Screenshot 2025-02-23 at 20 25 42" src="https://github.com/user-attachments/assets/ee0134b3-f745-45d9-8333-bfa1661e33d2" />
137 lines
3.6 KiB
Go
137 lines
3.6 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
const (
|
|
LicenseExpiryClaim = "license_expires"
|
|
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
|
|
)
|
|
|
|
type AddLicenseRequest struct {
|
|
License string `json:"license" validate:"required"`
|
|
}
|
|
|
|
type License struct {
|
|
ID int32 `json:"id"`
|
|
UUID uuid.UUID `json:"uuid" format:"uuid"`
|
|
UploadedAt time.Time `json:"uploaded_at" format:"date-time"`
|
|
// Claims are the JWT claims asserted by the license. Here we use
|
|
// a generic string map to ensure that all data from the server is
|
|
// parsed verbatim, not just the fields this version of Coder
|
|
// understands.
|
|
Claims map[string]interface{} `json:"claims" table:"claims"`
|
|
}
|
|
|
|
// ExpiresAt returns the expiration time of the license.
|
|
// If the claim is missing or has an unexpected type, an error is returned.
|
|
func (l *License) ExpiresAt() (time.Time, error) {
|
|
expClaim, ok := l.Claims[LicenseExpiryClaim]
|
|
if !ok {
|
|
return time.Time{}, xerrors.New("license_expires claim is missing")
|
|
}
|
|
|
|
// This claim should be a unix timestamp.
|
|
// Everything is already an interface{}, so we need to do some type
|
|
// assertions to figure out what we're dealing with.
|
|
if unix, ok := expClaim.(json.Number); ok {
|
|
i64, err := unix.Int64()
|
|
if err != nil {
|
|
return time.Time{}, xerrors.Errorf("license_expires claim is not a valid unix timestamp: %w", err)
|
|
}
|
|
return time.Unix(i64, 0), nil
|
|
}
|
|
|
|
return time.Time{}, xerrors.Errorf("license_expires claim has unexpected type %T", expClaim)
|
|
}
|
|
|
|
func (l *License) Trial() bool {
|
|
if trail, ok := l.Claims["trail"].(bool); ok {
|
|
return trail
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (l *License) AllFeaturesClaim() bool {
|
|
if all, ok := l.Claims["all_features"].(bool); ok {
|
|
return all
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FeaturesClaims provides the feature claims in license.
|
|
// This only returns the explicit claims. If checking for actual usage,
|
|
// also check `AllFeaturesClaim`.
|
|
func (l *License) FeaturesClaims() (map[FeatureName]int64, error) {
|
|
strMap, ok := l.Claims["features"].(map[string]interface{})
|
|
if !ok {
|
|
return nil, xerrors.New("features key is unexpected type")
|
|
}
|
|
fMap := make(map[FeatureName]int64)
|
|
for k, v := range strMap {
|
|
jn, ok := v.(json.Number)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("feature %q has unexpected type", k)
|
|
}
|
|
|
|
n, err := jn.Int64()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fMap[FeatureName(k)] = n
|
|
}
|
|
|
|
return fMap, nil
|
|
}
|
|
|
|
func (c *Client) AddLicense(ctx context.Context, r AddLicenseRequest) (License, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/licenses", r)
|
|
if err != nil {
|
|
return License{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusCreated {
|
|
return License{}, ReadBodyAsError(res)
|
|
}
|
|
var l License
|
|
d := json.NewDecoder(res.Body)
|
|
d.UseNumber()
|
|
return l, d.Decode(&l)
|
|
}
|
|
|
|
func (c *Client) Licenses(ctx context.Context) ([]License, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/licenses", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
var licenses []License
|
|
d := json.NewDecoder(res.Body)
|
|
d.UseNumber()
|
|
return licenses, d.Decode(&licenses)
|
|
}
|
|
|
|
func (c *Client) DeleteLicense(ctx context.Context, id int32) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/licenses/%d", id), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|