mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: audit organization member add/delete/edit (#13620)
* chore: audit organization member add/removals
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Generated
+2
-1
@@ -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 (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'organization_member';
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-11
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ We track the following resources:
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
|
||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user