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 |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | OAuth2ProviderApp
|
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| | OAuth2ProviderAppSecret
|
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| +| Organization
|
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_idfalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
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_typetrue
nametrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| 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