diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 54f7fe0c9d..e2fadd6718 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -7258,7 +7258,8 @@ const docTemplate = `{
"workspace_build",
"git_ssh_key",
"api_key",
- "group"
+ "group",
+ "license"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -7268,7 +7269,8 @@ const docTemplate = `{
"ResourceTypeWorkspaceBuild",
"ResourceTypeGitSSHKey",
"ResourceTypeAPIKey",
- "ResourceTypeGroup"
+ "ResourceTypeGroup",
+ "ResourceTypeLicense"
]
},
"codersdk.Response": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 6eb855dab5..844611bbcf 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -6508,7 +6508,8 @@
"workspace_build",
"git_ssh_key",
"api_key",
- "group"
+ "group",
+ "license"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -6518,7 +6519,8 @@
"ResourceTypeWorkspaceBuild",
"ResourceTypeGitSSHKey",
"ResourceTypeAPIKey",
- "ResourceTypeGroup"
+ "ResourceTypeGroup",
+ "ResourceTypeLicense"
]
},
"codersdk.Response": {
diff --git a/coderd/audit.go b/coderd/audit.go
index 0339264168..8fc144ae0d 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -456,6 +456,8 @@ func resourceTypeFromString(resourceTypeString string) string {
return resourceTypeString
case codersdk.ResourceTypeGroup:
return resourceTypeString
+ case codersdk.ResourceTypeLicense:
+ return resourceTypeString
}
return ""
}
diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index df1f4b334a..1cc6702d1b 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -15,7 +15,8 @@ type Auditable interface {
database.Workspace |
database.GitSSHKey |
database.WorkspaceBuild |
- database.AuditableGroup
+ database.AuditableGroup |
+ database.License
}
// Map is a map of changed fields in an audited resource. It maps field names to
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index 186434345c..b9ec814568 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net"
"net/http"
+ "strconv"
"github.com/google/uuid"
"github.com/tabbed/pqtype"
@@ -71,6 +72,8 @@ func ResourceTarget[T Auditable](tgt T) string {
case database.APIKey:
// this isn't used
return ""
+ case database.License:
+ return strconv.Itoa(int(typed.ID))
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
@@ -94,6 +97,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.Group.ID
case database.APIKey:
return typed.UserID
+ case database.License:
+ return typed.UUID
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
@@ -117,6 +122,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeGroup
case database.APIKey:
return database.ResourceTypeApiKey
+ case database.License:
+ return database.ResourceTypeLicense
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index 4df6cee00c..a2e42b00d8 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -93,7 +93,8 @@ CREATE TYPE resource_type AS ENUM (
'git_ssh_key',
'api_key',
'group',
- 'workspace_build'
+ 'workspace_build',
+ 'license'
);
CREATE TYPE user_status AS ENUM (
diff --git a/coderd/database/migrations/000098_add_resource_type_license.down.sql b/coderd/database/migrations/000098_add_resource_type_license.down.sql
new file mode 100644
index 0000000000..d1d1637f4f
--- /dev/null
+++ b/coderd/database/migrations/000098_add_resource_type_license.down.sql
@@ -0,0 +1,2 @@
+-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
+-- EXISTS".
diff --git a/coderd/database/migrations/000098_add_resource_type_license.up.sql b/coderd/database/migrations/000098_add_resource_type_license.up.sql
new file mode 100644
index 0000000000..43656a940b
--- /dev/null
+++ b/coderd/database/migrations/000098_add_resource_type_license.up.sql
@@ -0,0 +1,3 @@
+ALTER TYPE resource_type
+ ADD VALUE IF NOT EXISTS 'license';
+
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 23c8029f9f..2d1d82341e 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -883,6 +883,7 @@ const (
ResourceTypeApiKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
+ ResourceTypeLicense ResourceType = "license"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -930,7 +931,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeGitSshKey,
ResourceTypeApiKey,
ResourceTypeGroup,
- ResourceTypeWorkspaceBuild:
+ ResourceTypeWorkspaceBuild,
+ ResourceTypeLicense:
return true
}
return false
@@ -947,6 +949,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeApiKey,
ResourceTypeGroup,
ResourceTypeWorkspaceBuild,
+ ResourceTypeLicense,
}
}
diff --git a/codersdk/audit.go b/codersdk/audit.go
index 49648e5e94..464d350fc5 100644
--- a/codersdk/audit.go
+++ b/codersdk/audit.go
@@ -22,6 +22,7 @@ const (
ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
ResourceTypeAPIKey ResourceType = "api_key"
ResourceTypeGroup ResourceType = "group"
+ ResourceTypeLicense ResourceType = "license"
)
func (r ResourceType) FriendlyString() string {
@@ -44,6 +45,8 @@ func (r ResourceType) FriendlyString() string {
return "api key"
case ResourceTypeGroup:
return "group"
+ case ResourceTypeLicense:
+ return "license"
default:
return "unknown"
}
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index a705b9e371..762b2f3762 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -14,6 +14,7 @@ We track the following resources:
| APIKey
write |
| Field | Tracked |
|---|
| created_at | false |
| expires_at | false |
| hashed_secret | false |
| id | false |
| ip_address | false |
| last_used | false |
| lifetime_seconds | false |
| login_type | false |
| scope | false |
| updated_at | false |
| user_id | false |
|
| Group
create, write, delete | | Field | Tracked |
|---|
| avatar_url | true |
| id | true |
| members | true |
| name | true |
| organization_id | false |
| quota_allowance | true |
|
| GitSSHKey
create | | Field | Tracked |
|---|
| created_at | false |
| private_key | true |
| public_key | true |
| updated_at | false |
| user_id | true |
|
+| License
create, delete | | Field | Tracked |
|---|
| exp | true |
| id | false |
| jwt | false |
| uploaded_at | true |
| uuid | true |
|
| Template
write, delete | | Field | Tracked |
|---|
| active_version_id | true |
| allow_user_cancel_workspace_jobs | true |
| created_at | false |
| created_by | true |
| default_ttl | true |
| deleted | false |
| description | true |
| display_name | true |
| group_acl | true |
| icon | true |
| id | true |
| is_private | true |
| min_autostart_interval | true |
| name | true |
| organization_id | false |
| provisioner | true |
| updated_at | false |
| user_acl | true |
|
| TemplateVersion
create, write | | Field | Tracked |
|---|
| created_at | false |
| created_by | true |
| id | true |
| job_id | false |
| name | true |
| organization_id | false |
| readme | true |
| template_id | true |
| updated_at | false |
|
| User
create, write, delete | | Field | Tracked |
|---|
| avatar_url | false |
| created_at | false |
| deleted | true |
| email | true |
| hashed_password | true |
| id | true |
| last_seen_at | false |
| login_type | false |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index afa6e754c7..591c500b5c 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -4054,6 +4054,7 @@ Parameter represents a set value for the scope.
| `git_ssh_key` |
| `api_key` |
| `group` |
+| `license` |
## codersdk.Response
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index ffc0f303bd..239aecfd30 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -13,16 +13,15 @@ import (
// AuditableResources map (below) as our documentation - generated in scripts/auditdocgen/main.go -
// depends upon it.
var AuditActionMap = map[string][]codersdk.AuditAction{
- "GitSSHKey": {codersdk.AuditActionCreate},
- "OrganizationMember": {},
- "Organization": {},
- "Template": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
- "TemplateVersion": {codersdk.AuditActionCreate, codersdk.AuditActionWrite},
- "User": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
- "Workspace": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
- "WorkspaceBuild": {codersdk.AuditActionStart, codersdk.AuditActionStop},
- "Group": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
- "APIKey": {codersdk.AuditActionWrite},
+ "GitSSHKey": {codersdk.AuditActionCreate},
+ "Template": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
+ "TemplateVersion": {codersdk.AuditActionCreate, codersdk.AuditActionWrite},
+ "User": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
+ "Workspace": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
+ "WorkspaceBuild": {codersdk.AuditActionStart, codersdk.AuditActionStop},
+ "Group": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
+ "APIKey": {codersdk.AuditActionWrite},
+ "License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
}
type Action string
@@ -147,6 +146,15 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"ip_address": ActionIgnore,
"scope": ActionIgnore,
},
+ // TODO: track an ID here when the below ticket is completed:
+ // https://github.com/coder/coder/pull/6012
+ &database.License{}: {
+ "id": ActionIgnore,
+ "uploaded_at": ActionTrack,
+ "jwt": ActionIgnore,
+ "exp": ActionTrack,
+ "uuid": ActionTrack,
+ },
})
// auditMap converts a map of struct pointers to a map of struct names as
diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go
index 40f45e1b75..71baa57064 100644
--- a/enterprise/coderd/licenses.go
+++ b/enterprise/coderd/licenses.go
@@ -20,6 +20,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd"
+ "github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
@@ -59,7 +60,18 @@ var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220
// @Success 201 {object} codersdk.License
// @Router /licenses [post]
func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
+ 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
@@ -119,6 +131,8 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
})
return
}
+ aReq.New = dl
+
err = api.updateEntitlements(ctx)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -186,11 +200,10 @@ func (api *API) licenses(rw http.ResponseWriter, r *http.Request) {
// @Success 200
// @Router /licenses/{id} [delete]
func (api *API) deleteLicense(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- if !api.AGPL.Authorize(r, rbac.ActionDelete, rbac.ResourceLicense) {
- httpapi.Forbidden(rw)
- return
- }
+ var (
+ ctx = r.Context()
+ auditor = api.AGPL.Auditor.Load()
+ )
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 32)
@@ -201,6 +214,26 @@ func (api *API) deleteLicense(rw http.ResponseWriter, r *http.Request) {
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 xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 6c2606e0ca..bf79a6122e 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1183,6 +1183,7 @@ export type ResourceType =
| "api_key"
| "git_ssh_key"
| "group"
+ | "license"
| "template"
| "template_version"
| "user"
@@ -1192,6 +1193,7 @@ export const ResourceTypes: ResourceType[] = [
"api_key",
"git_ssh_key",
"group",
+ "license",
"template",
"template_version",
"user",