feat: add `deployment_id` to the ui and licenses (#13096)

* feat: expose `deployment_id` in the user dropdown

* feat: add license deployment_id verification

* Ignore wireguard.com from mlc config
This commit is contained in:
Kyle Carberry 2024-04-29 16:50:11 -04:00 committed by GitHub
parent 0e3dc2a80f
commit 1bda8a0856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 115 additions and 26 deletions

View File

@ -17,6 +17,9 @@
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]

4
coderd/apidoc/docs.go generated
View File

@ -8541,6 +8541,10 @@ const docTemplate = `{
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
"type": "string"
},
"deployment_id": {
"description": "DeploymentID is the unique identifier for this deployment.",
"type": "string"
},
"external_url": {
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"

View File

@ -7599,6 +7599,10 @@
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
"type": "string"
},
"deployment_id": {
"description": "DeploymentID is the unique identifier for this deployment.",
"type": "string"
},
"external_url": {
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"

View File

@ -735,7 +735,7 @@ func New(options *Options) *API {
// All CSP errors will be logged
r.Post("/csp/reports", api.logReportCSPViolations)
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String(), api.DeploymentID))
// /regions is overridden in the enterprise version
r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware)

View File

@ -68,7 +68,7 @@ func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) {
// @Tags General
// @Success 200 {object} codersdk.BuildInfoResponse
// @Router /buildinfo [get]
func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
func buildInfo(accessURL *url.URL, upgradeMessage, deploymentID string) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
@ -77,6 +77,7 @@ func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
DashboardURL: accessURL.String(),
WorkspaceProxy: false,
UpgradeMessage: upgradeMessage,
DeploymentID: deploymentID,
})
}
}

View File

@ -2151,6 +2151,9 @@ type BuildInfoResponse struct {
// UpgradeMessage is the message displayed to users when an outdated client
// is detected.
UpgradeMessage string `json:"upgrade_message"`
// DeploymentID is the unique identifier for this deployment.
DeploymentID string `json:"deployment_id"`
}
type WorkspaceProxyBuildInfo struct {

1
docs/api/general.md generated
View File

@ -55,6 +55,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
{
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",

2
docs/api/schemas.md generated
View File

@ -1178,6 +1178,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",
@ -1191,6 +1192,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| ------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_api_version` | string | false | | Agent api version is the current version of the Agent API (back versions MAY still be supported). |
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
| `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. |
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
| `version` | string | false | | Version returns the semantic version of the build. |

View File

@ -147,13 +147,14 @@ func NewWithAPI(t *testing.T, options *Options) (
}
type LicenseOptions struct {
AccountType string
AccountID string
Trial bool
AllFeatures bool
GraceAt time.Time
ExpiresAt time.Time
Features license.Features
AccountType string
AccountID string
DeploymentIDs []string
Trial bool
AllFeatures bool
GraceAt time.Time
ExpiresAt time.Time
Features license.Features
}
// AddFullLicense generates a license with all features enabled.
@ -190,6 +191,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
LicenseExpires: jwt.NewNumericDate(options.GraceAt),
AccountType: options.AccountType,
AccountID: options.AccountID,
DeploymentIDs: options.DeploymentIDs,
Trial: options.Trial,
Version: license.CurrentVersion,
AllFeatures: options.AllFeatures,

View File

@ -257,14 +257,16 @@ type Claims struct {
// the end of the grace period (identical to LicenseExpires if there is no grace period).
// The reason we use the standard claim for the end of the grace period is that we want JWT
// processing libraries to consider the token "valid" until then.
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
AccountType string `json:"account_type,omitempty"`
AccountID string `json:"account_id,omitempty"`
Trial bool `json:"trial"`
AllFeatures bool `json:"all_features"`
Version uint64 `json:"version"`
Features Features `json:"features"`
RequireTelemetry bool `json:"require_telemetry,omitempty"`
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
AccountType string `json:"account_type,omitempty"`
AccountID string `json:"account_id,omitempty"`
// DeploymentIDs enforces the license can only be used on a set of deployments.
DeploymentIDs []string `json:"deployment_ids,omitempty"`
Trial bool `json:"trial"`
AllFeatures bool `json:"all_features"`
Version uint64 `json:"version"`
Features Features `json:"features"`
RequireTelemetry bool `json:"require_telemetry,omitempty"`
}
// ParseRaw consumes a license and returns the claims.

View File

@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@ -120,6 +121,15 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
// old licenses with a uuid.
id = uuid.New()
}
if len(claims.DeploymentIDs) > 0 && !slices.Contains(claims.DeploymentIDs, api.AGPL.DeploymentID) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "License cannot be used on this deployment!",
Detail: fmt.Sprintf("The provided license is locked to the following deployments: %q. "+
"Your deployment identifier is %q. Please contact sales.", claims.DeploymentIDs, api.AGPL.DeploymentID),
})
return
}
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
UploadedAt: dbtime.Now(),
JWT: addLicense.License,

View File

@ -5,6 +5,7 @@ import (
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
@ -36,6 +37,22 @@ func TestPostLicense(t *testing.T) {
assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog])
})
t.Run("InvalidDeploymentID", func(t *testing.T) {
t.Parallel()
// The generated deployment will start out with a different deployment ID.
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
license := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
DeploymentIDs: []string{uuid.NewString()},
})
_, err := client.AddLicense(context.Background(), codersdk.AddLicenseRequest{
License: license,
})
errResp := &codersdk.Error{}
require.ErrorAs(t, err, &errResp)
require.Equal(t, http.StatusBadRequest, errResp.StatusCode())
require.Contains(t, errResp.Message, "License cannot be used on this deployment!")
})
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})

View File

@ -165,6 +165,7 @@ export interface BuildInfoResponse {
readonly workspace_proxy: boolean;
readonly agent_api_version: string;
readonly upgrade_message: string;
readonly deployment_id: string;
}
// From codersdk/insights.go

View File

@ -12,9 +12,11 @@ import LaunchIcon from "@mui/icons-material/LaunchOutlined";
import DocsIcon from "@mui/icons-material/MenuBook";
import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem";
import Tooltip from "@mui/material/Tooltip";
import type { FC } from "react";
import { Link } from "react-router-dom";
import type * as TypesGen from "api/typesGenerated";
import { CopyButton } from "components/CopyButton/CopyButton";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { usePopover } from "components/Popover/Popover";
import { Stack } from "components/Stack/Stack";
@ -161,15 +163,51 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
<Divider css={{ marginBottom: "0 !important" }} />
<Stack css={styles.info} spacing={0}>
<a
title="Browse Source Code"
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <LaunchIcon />
</a>
<Tooltip title="Coder Version">
<a
title="Browse Source Code"
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <LaunchIcon />
</a>
</Tooltip>
{Boolean(buildInfo?.deployment_id) && (
<div
css={css`
font-size: 12px;
display: flex;
align-items: center;
`}
>
<Tooltip title="Deployment Identifier">
<div
css={css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`}
>
{buildInfo?.deployment_id}
</div>
</Tooltip>
<CopyButton
text={buildInfo!.deployment_id}
buttonStyles={css`
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
`}
/>
</div>
)}
<div css={styles.footerText}>{Language.copyrightText}</div>
</Stack>

View File

@ -201,6 +201,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
dashboard_url: "https:///mock-url",
workspace_proxy: false,
upgrade_message: "My custom upgrade message",
deployment_id: "510d407f-e521-4180-b559-eab4a6d802b8",
};
export const MockSupportLinks: TypesGen.LinkConfig[] = [