diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 06583721c9679..a57971527da0f 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -7255,7 +7255,8 @@ const docTemplate = `{
"workspace_build",
"git_ssh_key",
"api_key",
- "group"
+ "group",
+ "license"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -7265,7 +7266,8 @@ const docTemplate = `{
"ResourceTypeWorkspaceBuild",
"ResourceTypeGitSSHKey",
"ResourceTypeAPIKey",
- "ResourceTypeGroup"
+ "ResourceTypeGroup",
+ "ResourceTypeLicense"
]
},
"codersdk.Response": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 44a964ff435eb..0295d28be5411 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -6505,7 +6505,8 @@
"workspace_build",
"git_ssh_key",
"api_key",
- "group"
+ "group",
+ "license"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -6515,7 +6516,8 @@
"ResourceTypeWorkspaceBuild",
"ResourceTypeGitSSHKey",
"ResourceTypeAPIKey",
- "ResourceTypeGroup"
+ "ResourceTypeGroup",
+ "ResourceTypeLicense"
]
},
"codersdk.Response": {
diff --git a/coderd/audit.go b/coderd/audit.go
index 03392641681c2..8fc144ae0d148 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 df1f4b334ab03..1cc6702d1b06c 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 186434345ce82..b9ec814568dc5 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 4df6cee00c37e..a2e42b00d8107 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 0000000000000..d1d1637f4fa90
--- /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 0000000000000..43656a940b83d
--- /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 23c8029f9f47e..2d1d82341e780 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 49648e5e9440a..464d350fc5532 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 a705b9e371d56..762b2f3762ca7 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 c9d98afbc7e07..1b71043366a50 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -4052,6 +4052,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 ffc0f303bd25a..239aecfd3017e 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 40f45e1b75c17..71baa570645dc 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 64ad8a3e5f8b8..d51e138127ef5 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1182,6 +1182,7 @@ export type ResourceType =
| "api_key"
| "git_ssh_key"
| "group"
+ | "license"
| "template"
| "template_version"
| "user"
@@ -1191,6 +1192,7 @@ export const ResourceTypes: ResourceType[] = [
"api_key",
"git_ssh_key",
"group",
+ "license",
"template",
"template_version",
"user",