coder/enterprise/coderd/licenses.go

380 lines
11 KiB
Go

package coderd
import (
"bytes"
"context"
"crypto/ed25519"
"database/sql"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/license"
)
const (
PubsubEventLicenses = "licenses"
)
// key20220812 is the Coder license public key with id 2022-08-12 used to validate licenses signed
// by our signing infrastructure
//
//go:embed keys/2022-08-12
var key20220812 []byte
var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220812)}
// postLicense adds a new Enterprise license to the cluster. We allow multiple different licenses
// in the cluster at one time for several reasons:
//
// 1. Upgrades --- if the license format changes from one version of Coder to the next, during a
// rolling update you will have different Coder servers that need different licenses to function.
// 2. Avoid abrupt feature breakage --- when an admin uploads a new license with different features
// we generally don't want the old features to immediately break without warning. With a grace
// period on the license, features will continue to work from the old license until its grace
// period, then the users will get a warning allowing them to gracefully stop using the feature.
//
// @Summary Add new license
// @ID add-new-license
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Organizations
// @Param request body codersdk.AddLicenseRequest true "Add license request"
// @Success 201 {object} codersdk.License
// @Router /licenses [post]
func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.License](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
})
)
defer commitAudit()
if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) {
httpapi.Forbidden(rw)
return
}
var addLicense codersdk.AddLicenseRequest
if !httpapi.Read(ctx, rw, r, &addLicense) {
return
}
rawClaims, err := license.ParseRaw(addLicense.License, api.Keys)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid license",
Detail: err.Error(),
})
return
}
exp, ok := rawClaims["exp"].(float64)
if !ok {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid license",
Detail: "exp claim missing or not parsable",
})
return
}
expTime := time.Unix(int64(exp), 0)
claims, err := license.ParseClaims(addLicense.License, api.Keys)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid license",
Detail: err.Error(),
})
return
}
id, err := uuid.Parse(claims.ID)
if err != nil {
// If no uuid is in the license, we generate a random uuid.
// This is not ideal, and this should be fixed to require a uuid
// for all licenses. We require this patch to support older licenses.
// TODO: In the future (April 2023?) we should remove this and reissue
// old licenses with a uuid.
id = uuid.New()
}
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
UploadedAt: dbtime.Now(),
JWT: addLicense.License,
Exp: expTime,
UUID: id,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Unable to add license to database",
Detail: err.Error(),
})
return
}
aReq.New = dl
err = api.updateEntitlements(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update entitlements",
Detail: err.Error(),
})
return
}
err = api.Pubsub.Publish(PubsubEventLicenses, []byte("add"))
if err != nil {
api.Logger.Error(context.Background(), "failed to publish license add", slog.Error(err))
// don't fail the HTTP request, since we did write it successfully to the database
}
httpapi.Write(ctx, rw, http.StatusCreated, convertLicense(dl, rawClaims))
}
// postRefreshEntitlements forces an `updateEntitlements` call and publishes
// a message to the PubsubEventLicenses topic to force other replicas
// to update their entitlements.
// Updates happen automatically on a timer, however that time is every 10 minutes,
// and we want to be able to force an update immediately in some cases.
//
// @Summary Update license entitlements
// @ID update-license-entitlements
// @Security CoderSessionToken
// @Produce json
// @Tags Organizations
// @Success 201 {object} codersdk.Response
// @Router /licenses/refresh-entitlements [post]
func (api *API) postRefreshEntitlements(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// If the user cannot create a new license, then they cannot refresh entitlements.
// Refreshing entitlements is a way to force a refresh of the license, so it is
// equivalent to creating a new license.
if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) {
httpapi.Forbidden(rw)
return
}
// Prevent abuse by limiting how often we allow a forced refresh.
now := time.Now()
if diff := now.Sub(api.entitlements.RefreshedAt); diff < time.Minute {
wait := time.Minute - diff
rw.Header().Set("Retry-After", strconv.Itoa(int(wait.Seconds())))
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Entitlements already recently refreshed, please wait %d seconds to force a new refresh", int(wait.Seconds())),
Detail: fmt.Sprintf("Last refresh at %s", now.UTC().String()),
})
return
}
err := api.replicaManager.UpdateNow(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to sync replicas",
Detail: err.Error(),
})
return
}
err = api.updateEntitlements(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update entitlements",
Detail: err.Error(),
})
return
}
err = api.Pubsub.Publish(PubsubEventLicenses, []byte("refresh"))
if err != nil {
api.Logger.Error(context.Background(), "failed to publish forced entitlement update", slog.Error(err))
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to publish forced entitlement update. Other replicas might not be updated.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Entitlements updated",
})
}
// @Summary Get licenses
// @ID get-licenses
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Success 200 {array} codersdk.License
// @Router /licenses [get]
func (api *API) licenses(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
licenses, err := api.Database.GetLicenses(ctx)
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusOK, []codersdk.License{})
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching licenses.",
Detail: err.Error(),
})
return
}
licenses, err = coderd.AuthorizeFilter(api.AGPL.HTTPAuth, r, rbac.ActionRead, licenses)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching licenses.",
Detail: err.Error(),
})
return
}
sdkLicenses, err := convertLicenses(licenses)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error parsing licenses.",
Detail: err.Error(),
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, sdkLicenses)
}
// @Summary Delete license
// @ID delete-license
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param id path string true "License ID" format(number)
// @Success 200
// @Router /licenses/{id} [delete]
func (api *API) deleteLicense(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
auditor = api.AGPL.Auditor.Load()
)
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 32)
if err != nil {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "License ID must be an integer",
})
return
}
dl, err := api.Database.GetLicenseByID(ctx, int32(id))
if err != nil {
// don't fail the HTTP request simply because we cannot audit
api.Logger.Warn(context.Background(), "could not retrieve license; cannot audit", slog.Error(err))
}
aReq, commitAudit := audit.InitRequest[database.License](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionDelete,
})
defer commitAudit()
aReq.Old = dl
if !api.AGPL.Authorize(r, rbac.ActionDelete, rbac.ResourceLicense) {
httpapi.Forbidden(rw)
return
}
_, err = api.Database.DeleteLicense(ctx, int32(id))
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: "Unknown license ID",
})
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error deleting license",
Detail: err.Error(),
})
return
}
err = api.updateEntitlements(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update entitlements",
Detail: err.Error(),
})
return
}
err = api.Pubsub.Publish(PubsubEventLicenses, []byte("delete"))
if err != nil {
api.Logger.Error(context.Background(), "failed to publish license delete", slog.Error(err))
// don't fail the HTTP request, since we did write it successfully to the database
}
rw.WriteHeader(http.StatusOK)
}
func convertLicense(dl database.License, c jwt.MapClaims) codersdk.License {
return codersdk.License{
ID: dl.ID,
UUID: dl.UUID,
UploadedAt: dl.UploadedAt,
Claims: c,
}
}
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.RawURLEncoding.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
}