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 |
FieldTracked
created_atfalse
expires_atfalse
hashed_secretfalse
idfalse
ip_addressfalse
last_usedfalse
lifetime_secondsfalse
login_typefalse
scopefalse
updated_atfalse
user_idfalse
| | Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
group_acltrue
icontrue
idtrue
is_privatetrue
min_autostart_intervaltrue
nametrue
organization_idfalse
provisionertrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write |
FieldTracked
created_atfalse
created_bytrue
idtrue
job_idfalse
nametrue
organization_idfalse
readmetrue
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typefalse
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| 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",