diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index 8f6059b0dceff..09ae80c9ddf90 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -23,7 +23,8 @@ type Auditable interface {
database.OAuth2ProviderApp |
database.OAuth2ProviderAppSecret |
database.CustomRole |
- database.AuditableOrganizationMember
+ database.AuditableOrganizationMember |
+ database.Organization
}
// 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 d8d6ce094b4ea..1c027fc85527f 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -107,6 +107,8 @@ func ResourceTarget[T Auditable](tgt T) string {
return typed.Name
case database.AuditableOrganizationMember:
return typed.Username
+ case database.Organization:
+ return typed.Name
default:
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
}
@@ -148,6 +150,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
return typed.ID
case database.AuditableOrganizationMember:
return typed.UserID
+ case database.Organization:
+ return typed.ID
default:
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
}
@@ -187,6 +191,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeCustomRole
case database.AuditableOrganizationMember:
return database.ResourceTypeOrganizationMember
+ case database.Organization:
+ return database.ResourceTypeOrganization
default:
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
}
@@ -227,6 +233,8 @@ func ResourceRequiresOrgID[T Auditable]() bool {
return true
case database.AuditableOrganizationMember:
return true
+ case database.Organization:
+ return true
default:
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
}
diff --git a/coderd/organizations.go b/coderd/organizations.go
index 6d40b765141ba..24d55fa950c65 100644
--- a/coderd/organizations.go
+++ b/coderd/organizations.go
@@ -9,6 +9,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
+ "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
@@ -41,8 +42,22 @@ func (*API) organization(rw http.ResponseWriter, r *http.Request) {
// @Success 201 {object} codersdk.Organization
// @Router /organizations [post]
func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- apiKey := httpmw.APIKey(r)
+ var (
+ // organizationID is required before the audit log entry is created.
+ organizationID = uuid.New()
+ ctx = r.Context()
+ apiKey = httpmw.APIKey(r)
+ auditor = api.Auditor.Load()
+ aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
+ Audit: *auditor,
+ Log: api.Logger,
+ Request: r,
+ Action: database.AuditActionCreate,
+ OrganizationID: organizationID,
+ })
+ )
+ aReq.Old = database.Organization{}
+ defer commitAudit()
var req codersdk.CreateOrganizationRequest
if !httpapi.Read(ctx, rw, r, &req) {
@@ -78,7 +93,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
}
organization, err = tx.InsertOrganization(ctx, database.InsertOrganizationParams{
- ID: uuid.New(),
+ ID: organizationID,
Name: req.Name,
DisplayName: req.DisplayName,
Description: req.Description,
@@ -119,6 +134,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
return
}
+ aReq.New = organization
httpapi.Write(ctx, rw, http.StatusCreated, convertOrganization(organization))
}
@@ -133,8 +149,20 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.Organization
// @Router /organizations/{organization} [patch]
func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- organization := httpmw.OrganizationParam(r)
+ var (
+ ctx = r.Context()
+ organization = httpmw.OrganizationParam(r)
+ auditor = api.Auditor.Load()
+ aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
+ Audit: *auditor,
+ Log: api.Logger,
+ Request: r,
+ Action: database.AuditActionWrite,
+ OrganizationID: organization.ID,
+ })
+ )
+ aReq.Old = organization
+ defer commitAudit()
var req codersdk.UpdateOrganizationRequest
if !httpapi.Read(ctx, rw, r, &req) {
@@ -208,6 +236,7 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
return
}
+ aReq.New = organization
httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization))
}
@@ -220,8 +249,20 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.Response
// @Router /organizations/{organization} [delete]
func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- organization := httpmw.OrganizationParam(r)
+ var (
+ ctx = r.Context()
+ organization = httpmw.OrganizationParam(r)
+ auditor = api.Auditor.Load()
+ aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
+ Audit: *auditor,
+ Log: api.Logger,
+ Request: r,
+ Action: database.AuditActionDelete,
+ OrganizationID: organization.ID,
+ })
+ )
+ aReq.Old = organization
+ defer commitAudit()
if organization.IsDefault {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -239,6 +280,7 @@ func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
return
}
+ aReq.New = database.Organization{}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Organization has been deleted.",
})
diff --git a/coderd/users_test.go b/coderd/users_test.go
index 65a16cef2dedd..c85b35e49ea81 100644
--- a/coderd/users_test.go
+++ b/coderd/users_test.go
@@ -525,7 +525,7 @@ func TestPostUsers(t *testing.T) {
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
- require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)
+ require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-3].Action)
require.Len(t, user.OrganizationIDs, 1)
assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index ff216b3da73d2..52ed2d34e1a97 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -20,6 +20,7 @@ We track the following resources:
| License
create, delete |
Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
| OAuth2ProviderApp
| Field | Tracked |
---|
callback_url | true |
created_at | false |
icon | true |
id | false |
name | true |
updated_at | false |
|
| OAuth2ProviderAppSecret
| Field | Tracked |
---|
app_id | false |
created_at | false |
display_secret | false |
hashed_secret | false |
id | false |
last_used_at | false |
secret_prefix | false |
|
+| Organization
| Field | Tracked |
---|
created_at | false |
description | true |
display_name | true |
icon | true |
id | false |
is_default | true |
name | true |
updated_at | true |
|
| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_id | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
---|
archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | 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 | true |
name | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
|
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index d5f7dfed70fb5..72012bf224167 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -254,6 +254,16 @@ var auditableResourcesTypes = map[any]map[string]Action{
"app_id": ActionIgnore,
"secret_prefix": ActionIgnore,
},
+ &database.Organization{}: {
+ "id": ActionIgnore,
+ "name": ActionTrack,
+ "description": ActionTrack,
+ "created_at": ActionIgnore,
+ "updated_at": ActionTrack,
+ "is_default": ActionTrack,
+ "display_name": ActionTrack,
+ "icon": ActionTrack,
+ },
}
// auditMap converts a map of struct pointers to a map of struct names as