GET license endpoint (#3651)

* GET license endpoint

Signed-off-by: Spike Curtis <spike@coder.com>

* SDK GetLicenses -> Licenses

Signed-off-by: Spike Curtis <spike@coder.com>

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2022-08-24 11:44:22 -07:00
committed by GitHub
parent da54874958
commit c9bce19d88
13 changed files with 223 additions and 11 deletions

View File

@ -1,10 +1,15 @@
package coderd
import (
"bytes"
"context"
"crypto/ed25519"
"database/sql"
_ "embed"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
"golang.org/x/xerrors"
@ -119,6 +124,7 @@ func newLicenseAPI(
r := chi.NewRouter()
a := &licenseAPI{router: r, logger: l, database: db, pubsub: ps, auth: auth}
r.Post("/", a.postLicense)
r.Get("/", a.licenses)
return a
}
@ -192,3 +198,70 @@ func convertLicense(dl database.License, c jwt.MapClaims) codersdk.License {
Claims: c,
}
}
func (a *licenseAPI) licenses(rw http.ResponseWriter, r *http.Request) {
licenses, err := a.database.GetLicenses(r.Context())
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusOK, []codersdk.License{})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching licenses.",
Detail: err.Error(),
})
return
}
licenses, err = coderd.AuthorizeFilter(a.auth, r, rbac.ActionRead, licenses)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching licenses.",
Detail: err.Error(),
})
return
}
sdkLicenses, err := convertLicenses(licenses)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error parsing licenses.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, sdkLicenses)
}
func convertLicenses(licenses []database.License) ([]codersdk.License, error) {
var out []codersdk.License
for _, l := range licenses {
c, err := decodeClaims(l)
if err != nil {
return nil, err
}
out = append(out, convertLicense(l, c))
}
return out, nil
}
// decodeClaims decodes the JWT claims from the stored JWT. Note here we do not validate the JWT
// and just return the claims verbatim. We want to include all licenses on the GET response, even
// if they are expired, or signed by a key this version of Coder no longer considers valid.
//
// Also, we do not return the whole JWT itself because a signed JWT is a bearer token and we
// want to limit the chance of it being accidentally leaked.
func decodeClaims(l database.License) (jwt.MapClaims, error) {
parts := strings.Split(l.JWT, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("Unable to parse license %d as JWT", l.ID)
}
cb, err := base64.URLEncoding.DecodeString(parts[1])
if err != nil {
return nil, xerrors.Errorf("Unable to decode license %d claims: %w", l.ID, err)
}
c := make(jwt.MapClaims)
d := json.NewDecoder(bytes.NewBuffer(cb))
d.UseNumber()
err = d.Decode(&c)
return c, err
}

View File

@ -142,6 +142,76 @@ func TestPostLicense(t *testing.T) {
})
}
// these tests patch the map of license keys, so cannot be run in parallel
// nolint:paralleltest
func TestGetLicense(t *testing.T) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := "testing"
oldKeys := keys
defer func() {
t.Log("restoring keys")
keys = oldKeys
}()
keys = map[string]ed25519.PublicKey{keyID: pubKey}
t.Run("GET", func(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{APIBuilder: NewEnterprise})
_ = coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
claims := &Claims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "test@coder.test",
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
},
LicenseExpires: jwt.NewNumericDate(time.Now().Add(time.Hour)),
AccountType: AccountTypeSalesforce,
AccountID: "testing",
Version: CurrentVersion,
Features: Features{
UserLimit: 0,
AuditLog: 1,
},
}
lic, err := makeLicense(claims, privKey, keyID)
require.NoError(t, err)
_, err = client.AddLicense(ctx, codersdk.AddLicenseRequest{
License: lic,
})
require.NoError(t, err)
// 2nd license
claims.AccountID = "testing2"
claims.Features.UserLimit = 200
lic2, err := makeLicense(claims, privKey, keyID)
require.NoError(t, err)
_, err = client.AddLicense(ctx, codersdk.AddLicenseRequest{
License: lic2,
})
require.NoError(t, err)
licenses, err := client.Licenses(ctx)
require.NoError(t, err)
require.Len(t, licenses, 2)
assert.Equal(t, int32(1), licenses[0].ID)
assert.Equal(t, "testing", licenses[0].Claims["account_id"])
assert.Equal(t, map[string]interface{}{
codersdk.FeatureUserLimit: json.Number("0"),
codersdk.FeatureAuditLog: json.Number("1"),
}, licenses[0].Claims["features"])
assert.Equal(t, int32(2), licenses[1].ID)
assert.Equal(t, "testing2", licenses[1].Claims["account_id"])
assert.Equal(t, map[string]interface{}{
codersdk.FeatureUserLimit: json.Number("200"),
codersdk.FeatureAuditLog: json.Number("1"),
}, licenses[1].Claims["features"])
})
}
func makeLicense(c *Claims, privateKey ed25519.PrivateKey, keyID string) (string, error) {
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c)
tok.Header[HeaderKeyID] = keyID