From 61708398bf68d1b8096f1568661f706b06de213c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 16:32:10 -0500 Subject: [PATCH 1/6] chore: audit organization member add/removals --- coderd/audit/diff.go | 3 +- coderd/audit/request.go | 6 ++++ coderd/database/dump.sql | 3 +- .../000220_audit_org_member.down.sql | 0 .../migrations/000220_audit_org_member.up.sql | 1 + coderd/database/models.go | 5 ++- coderd/members.go | 33 +++++++++++++++---- codersdk/audit.go | 3 ++ 8 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 coderd/database/migrations/000220_audit_org_member.down.sql create mode 100644 coderd/database/migrations/000220_audit_org_member.up.sql diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index dd5205c0afb42..5340b6fa8acd7 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -22,7 +22,8 @@ type Auditable interface { database.HealthSettings | database.OAuth2ProviderApp | database.OAuth2ProviderAppSecret | - database.CustomRole + database.CustomRole | + database.OrganizationMember } // 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 2171366f4c66f..6019d7a858bdc 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -144,6 +144,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.CustomRole: return typed.ID + case database.OrganizationMember: + return typed.UserID default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) } @@ -181,6 +183,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeOauth2ProviderAppSecret case database.CustomRole: return database.ResourceTypeCustomRole + case database.OrganizationMember: + return database.ResourceTypeOrganizationMember default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) } @@ -219,6 +223,8 @@ func ResourceRequiresOrgID[T Auditable]() bool { return false case database.CustomRole: return true + case database.OrganizationMember: + return true default: panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ca063f4b08eb1..0ca4c7ac18c99 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -148,7 +148,8 @@ CREATE TYPE resource_type AS ENUM ( 'health_settings', 'oauth2_provider_app', 'oauth2_provider_app_secret', - 'custom_role' + 'custom_role', + 'organization_member' ); CREATE TYPE startup_script_behavior AS ENUM ( diff --git a/coderd/database/migrations/000220_audit_org_member.down.sql b/coderd/database/migrations/000220_audit_org_member.down.sql new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/coderd/database/migrations/000220_audit_org_member.up.sql b/coderd/database/migrations/000220_audit_org_member.up.sql new file mode 100644 index 0000000000000..c6f0f799a367d --- /dev/null +++ b/coderd/database/migrations/000220_audit_org_member.up.sql @@ -0,0 +1 @@ +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'organization_member'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 963587875b372..aea9837e92e89 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1223,6 +1223,7 @@ const ( ResourceTypeOauth2ProviderApp ResourceType = "oauth2_provider_app" ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" ResourceTypeCustomRole ResourceType = "custom_role" + ResourceTypeOrganizationMember ResourceType = "organization_member" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1277,7 +1278,8 @@ func (e ResourceType) Valid() bool { ResourceTypeHealthSettings, ResourceTypeOauth2ProviderApp, ResourceTypeOauth2ProviderAppSecret, - ResourceTypeCustomRole: + ResourceTypeCustomRole, + ResourceTypeOrganizationMember: return true } return false @@ -1301,6 +1303,7 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeOauth2ProviderApp, ResourceTypeOauth2ProviderAppSecret, ResourceTypeCustomRole, + ResourceTypeOrganizationMember, } } diff --git a/coderd/members.go b/coderd/members.go index 2528a17878f3b..4cb3211d2d8d9 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -7,6 +7,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/db2sdk" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -27,10 +28,19 @@ import ( // @Router /organizations/{organization}/members/{user} [post] func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - user = httpmw.UserParam(r) + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + user = httpmw.UserParam(r) + auditor = api.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + }) ) + aReq.Old = database.OrganizationMember{} + defer commitAudit() member, err := api.Database.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ OrganizationID: organization.ID, @@ -54,6 +64,7 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) return } + aReq.New = member resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{member}) if err != nil { httpapi.InternalServerError(rw, err) @@ -79,10 +90,19 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) // @Router /organizations/{organization}/members/{user} [delete] func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - member = httpmw.OrganizationMemberParam(r) + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + member = httpmw.OrganizationMemberParam(r) + auditor = api.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionDelete, + }) ) + aReq.Old = member.OrganizationMember + defer commitAudit() err := api.Database.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{ OrganizationID: organization.ID, @@ -97,6 +117,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request return } + aReq.New = database.OrganizationMember{} httpapi.Write(ctx, rw, http.StatusOK, "organization member removed") } diff --git a/codersdk/audit.go b/codersdk/audit.go index 837ef729e4a58..b1d525d69179f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -31,6 +31,7 @@ const ( // nolint:gosec // This is not a secret. ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" ResourceTypeCustomRole ResourceType = "custom_role" + ResourceTypeOrganizationMember = "organization_member" ) func (r ResourceType) FriendlyString() string { @@ -69,6 +70,8 @@ func (r ResourceType) FriendlyString() string { return "oauth2 app secret" case ResourceTypeCustomRole: return "custom role" + case ResourceTypeOrganizationMember: + return "organization member" default: return "unknown" } From ebb7eadabe88aa23c6bb2129601fe3c097e6d64b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 16:50:16 -0500 Subject: [PATCH 2/6] Add required audit functions --- coderd/audit/request.go | 2 ++ coderd/members.go | 18 ++++++++++++++---- enterprise/audit/table.go | 7 +++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 6019d7a858bdc..1c9a9e6be6709 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -105,6 +105,8 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.DisplaySecret case database.CustomRole: return typed.Name + case database.OrganizationMember: + return typed.UserID.String() default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } diff --git a/coderd/members.go b/coderd/members.go index 4cb3211d2d8d9..21f51533a9e91 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -170,11 +170,20 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Router /organizations/{organization}/members/{user}/roles [put] func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { var ( - ctx = r.Context() - organization = httpmw.OrganizationParam(r) - member = httpmw.OrganizationMemberParam(r) - apiKey = httpmw.APIKey(r) + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + member = httpmw.OrganizationMemberParam(r) + apiKey = httpmw.APIKey(r) + auditor = api.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionDelete, + }) ) + aReq.Old = member.OrganizationMember + defer commitAudit() if apiKey.UserID == member.UserID { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -203,6 +212,7 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { }) return } + aReq.New = updatedUser resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{updatedUser}) if err != nil { diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index e2788959e3275..d68a8777cde94 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -50,6 +50,13 @@ type Table map[string]map[string]Action var AuditableResources = auditMap(auditableResourcesTypes) var auditableResourcesTypes = map[any]map[string]Action{ + &database.OrganizationMember{}: { + "user_id": ActionTrack, + "organization_id": ActionTrack, + "created_at": ActionTrack, + "updated_at": ActionTrack, + "roles": ActionTrack, + }, &database.CustomRole{}: { "name": ActionTrack, "display_name": ActionTrack, From 04ee1b022d4861474039ada2da71d28f29ac22ca Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 18:02:38 -0500 Subject: [PATCH 3/6] chore: change audit type to include username --- coderd/audit/diff.go | 2 +- coderd/audit/request.go | 10 +++++----- coderd/database/modelmethods.go | 12 ++++++++++++ enterprise/audit/table.go | 3 ++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 5340b6fa8acd7..8f6059b0dceff 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -23,7 +23,7 @@ type Auditable interface { database.OAuth2ProviderApp | database.OAuth2ProviderAppSecret | database.CustomRole | - database.OrganizationMember + database.AuditableOrganizationMember } // 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 1c9a9e6be6709..d8d6ce094b4ea 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -105,8 +105,8 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.DisplaySecret case database.CustomRole: return typed.Name - case database.OrganizationMember: - return typed.UserID.String() + case database.AuditableOrganizationMember: + return typed.Username default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } @@ -146,7 +146,7 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.CustomRole: return typed.ID - case database.OrganizationMember: + case database.AuditableOrganizationMember: return typed.UserID default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) @@ -185,7 +185,7 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeOauth2ProviderAppSecret case database.CustomRole: return database.ResourceTypeCustomRole - case database.OrganizationMember: + case database.AuditableOrganizationMember: return database.ResourceTypeOrganizationMember default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) @@ -225,7 +225,7 @@ func ResourceRequiresOrgID[T Auditable]() bool { return false case database.CustomRole: return true - case database.OrganizationMember: + case database.AuditableOrganizationMember: return true default: panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index ee22ae1ad42ba..0ae838894aa8b 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -60,6 +60,18 @@ func (s WorkspaceAgentStatus) Valid() bool { } } +type AuditableOrganizationMember struct { + OrganizationMember + Username string `json:"username"` +} + +func (m OrganizationMember) Auditable(username string) AuditableOrganizationMember { + return AuditableOrganizationMember{ + OrganizationMember: m, + Username: username, + } +} + type AuditableGroup struct { Group Members []GroupMember `json:"members"` diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index d68a8777cde94..d5f7dfed70fb5 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -50,7 +50,8 @@ type Table map[string]map[string]Action var AuditableResources = auditMap(auditableResourcesTypes) var auditableResourcesTypes = map[any]map[string]Action{ - &database.OrganizationMember{}: { + &database.AuditableOrganizationMember{}: { + "username": ActionTrack, "user_id": ActionTrack, "organization_id": ActionTrack, "created_at": ActionTrack, From 0e9684d9177e4bda48a2855c587a7c21c4b339ee Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 18:03:18 -0500 Subject: [PATCH 4/6] fixup! chore: change audit type to include username --- coderd/members.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/coderd/members.go b/coderd/members.go index 21f51533a9e91..c573a4179f636 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -32,14 +32,14 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) organization = httpmw.OrganizationParam(r) user = httpmw.UserParam(r) auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionCreate, }) ) - aReq.Old = database.OrganizationMember{} + aReq.Old = database.AuditableOrganizationMember{} defer commitAudit() member, err := api.Database.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ @@ -64,7 +64,7 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) return } - aReq.New = member + aReq.New = member.Auditable(user.Username) resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{member}) if err != nil { httpapi.InternalServerError(rw, err) @@ -94,14 +94,14 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request organization = httpmw.OrganizationParam(r) member = httpmw.OrganizationMemberParam(r) auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionDelete, }) ) - aReq.Old = member.OrganizationMember + aReq.Old = member.OrganizationMember.Auditable(member.Username) defer commitAudit() err := api.Database.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{ @@ -117,7 +117,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request return } - aReq.New = database.OrganizationMember{} + aReq.New = database.AuditableOrganizationMember{} httpapi.Write(ctx, rw, http.StatusOK, "organization member removed") } @@ -175,17 +175,17 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { member = httpmw.OrganizationMemberParam(r) apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() - aReq, commitAudit = audit.InitRequest[database.OrganizationMember](rw, &audit.RequestParams{ + aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionDelete, }) ) - aReq.Old = member.OrganizationMember + aReq.Old = member.OrganizationMember.Auditable(member.Username) defer commitAudit() - if apiKey.UserID == member.UserID { + if apiKey.UserID == member.OrganizationMember.UserID { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "You cannot change your own organization roles.", }) @@ -212,7 +212,10 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { }) return } - aReq.New = updatedUser + aReq.New = database.AuditableOrganizationMember{ + OrganizationMember: updatedUser, + Username: member.Username, + } resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{updatedUser}) if err != nil { From 3fb0467e8819b5205568c2f24538f8a3156e19f8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 18:12:57 -0500 Subject: [PATCH 5/6] fixup! fixup! chore: change audit type to include username --- coderd/members.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/members.go b/coderd/members.go index c573a4179f636..e15aa9d4821f9 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -179,7 +179,7 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { Audit: *auditor, Log: api.Logger, Request: r, - Action: database.AuditActionDelete, + Action: database.AuditActionWrite, }) ) aReq.Old = member.OrganizationMember.Auditable(member.Username) From 310fb756faa7708e0210491719d4a8b97e06df7c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Jun 2024 18:14:22 -0500 Subject: [PATCH 6/6] make gen --- docs/admin/audit-logs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 34c4e8c9a8dc3..ff216b3da73d2 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -13,6 +13,7 @@ We track the following resources: | APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| | AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| | Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idtrue
rolestrue
updated_attrue
user_idtrue
usernametrue
| | CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idtrue
site_permissionstrue
updated_atfalse
user_permissionstrue
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| | HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy