chore: audit organization member add/delete/edit (#13620)

* chore: audit organization member add/removals
This commit is contained in:
Steven Masley
2024-06-24 09:19:32 -10:00
committed by GitHub
parent 94639730f8
commit 65b9f9bfd6
11 changed files with 86 additions and 14 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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 (

View File

@ -0,0 +1 @@
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'organization_member';

View File

@ -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"`

View File

@ -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,
}
}

View File

@ -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 {