diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index dd5205c0afb42..8f6059b0dceff 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.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 2171366f4c66f..d8d6ce094b4ea 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.AuditableOrganizationMember: + return typed.Username default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } @@ -144,6 +146,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.CustomRole: return typed.ID + case database.AuditableOrganizationMember: + return typed.UserID default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) } @@ -181,6 +185,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeOauth2ProviderAppSecret case database.CustomRole: return database.ResourceTypeCustomRole + case database.AuditableOrganizationMember: + return database.ResourceTypeOrganizationMember default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) } @@ -219,6 +225,8 @@ func ResourceRequiresOrgID[T Auditable]() bool { return false case database.CustomRole: return true + case database.AuditableOrganizationMember: + 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/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/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..e15aa9d4821f9 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.AuditableOrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + }) ) + aReq.Old = database.AuditableOrganizationMember{} + 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.Auditable(user.Username) 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.AuditableOrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionDelete, + }) ) + aReq.Old = member.OrganizationMember.Auditable(member.Username) + 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.AuditableOrganizationMember{} httpapi.Write(ctx, rw, http.StatusOK, "organization member removed") } @@ -149,13 +170,22 @@ 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.AuditableOrganizationMember](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) ) + 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.", }) @@ -182,6 +212,10 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { }) return } + aReq.New = database.AuditableOrganizationMember{ + OrganizationMember: updatedUser, + Username: member.Username, + } resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{updatedUser}) if err != nil { 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" } 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
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index e2788959e3275..d5f7dfed70fb5 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -50,6 +50,14 @@ type Table map[string]map[string]Action var AuditableResources = auditMap(auditableResourcesTypes) var auditableResourcesTypes = map[any]map[string]Action{ + &database.AuditableOrganizationMember{}: { + "username": ActionTrack, + "user_id": ActionTrack, + "organization_id": ActionTrack, + "created_at": ActionTrack, + "updated_at": ActionTrack, + "roles": ActionTrack, + }, &database.CustomRole{}: { "name": ActionTrack, "display_name": ActionTrack, 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