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 |
Field | Tracked |
---|
created_at | true |
expires_at | true |
hashed_secret | false |
id | false |
ip_address | false |
last_used | true |
lifetime_seconds | false |
login_type | false |
scope | false |
token_name | false |
updated_at | false |
user_id | true |
|
| AuditOAuthConvertState
| Field | Tracked |
---|
created_at | true |
expires_at | true |
from_login_type | true |
to_login_type | true |
user_id | true |
|
| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
display_name | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
source | false |
|
+| AuditableOrganizationMember
| Field | Tracked |
---|
created_at | true |
organization_id | true |
roles | true |
updated_at | true |
user_id | true |
username | true |
|
| CustomRole
| Field | Tracked |
---|
created_at | false |
display_name | true |
id | false |
name | true |
org_permissions | true |
organization_id | true |
site_permissions | true |
updated_at | false |
user_permissions | true |
|
| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
| HealthSettings
| Field | Tracked |
---|
dismissed_healthchecks | true |
id | false |
|
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