mirror of https://github.com/coder/coder.git
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:
parent
0e3dc2a80f
commit
1bda8a0856
|
@ -17,6 +17,9 @@
|
|||
},
|
||||
{
|
||||
"pattern": "tailscale.com"
|
||||
},
|
||||
{
|
||||
"pattern": "wireguard.com"
|
||||
}
|
||||
],
|
||||
"aliveStatusCodes": [200, 0]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
Loading…
Reference in New Issue