mirror of https://github.com/coder/coder.git
chore: Ensure all audit types in ResourceTable match APGL (#6563)
* chore: Ensure all audit types in ResourceTable match APGL * Implement more checks to ensure all tracked fields are present * Add unit test to ensure all types are represented in audit table * Trade compile time safety for syntax
This commit is contained in:
parent
a65a16122d
commit
37c859ec4c
|
@ -9,17 +9,17 @@ We track the following resources:
|
|||
|
||||
<!-- Code generated by 'make docs/admin/audit-logs.md'. DO NOT EDIT -->
|
||||
|
||||
| <b>Resource<b> | |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| APIKey<br><i>write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>expires_at</td><td>false</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>false</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>false</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>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></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> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_private</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>min_autostart_interval</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
| <b>Resource<b> | |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| APIKey<br><i>write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>expires_at</td><td>false</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>false</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>false</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>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></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> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>git_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
|
||||
<!-- End generated by 'make docs/admin/audit-logs.md'. -->
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package audit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
|
@ -42,8 +43,11 @@ const (
|
|||
type Table map[string]map[string]Action
|
||||
|
||||
// AuditableResources contains a definitive list of all auditable resources and
|
||||
// which fields are auditable.
|
||||
var AuditableResources = auditMap(map[any]map[string]Action{
|
||||
// which fields are auditable. All resource types must be valid audit.Auditable
|
||||
// types.
|
||||
var AuditableResources = auditMap(auditableResourcesTypes)
|
||||
|
||||
var auditableResourcesTypes = map[any]map[string]Action{
|
||||
&database.GitSSHKey{}: {
|
||||
"user_id": ActionTrack,
|
||||
"created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff.
|
||||
|
@ -64,9 +68,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
|
|||
"description": ActionTrack,
|
||||
"icon": ActionTrack,
|
||||
"default_ttl": ActionTrack,
|
||||
"min_autostart_interval": ActionTrack,
|
||||
"created_by": ActionTrack,
|
||||
"is_private": ActionTrack,
|
||||
"group_acl": ActionTrack,
|
||||
"user_acl": ActionTrack,
|
||||
"allow_user_cancel_workspace_jobs": ActionTrack,
|
||||
|
@ -159,7 +161,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
|
|||
"exp": ActionTrack,
|
||||
"uuid": ActionTrack,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// auditMap converts a map of struct pointers to a map of struct names as
|
||||
// strings. It's a convenience wrapper so that structs can be passed in by value
|
||||
|
@ -168,12 +170,61 @@ func auditMap(m map[any]map[string]Action) Table {
|
|||
out := make(Table, len(m))
|
||||
|
||||
for k, v := range m {
|
||||
out[structName(reflect.TypeOf(k).Elem())] = v
|
||||
tableKey, tableValue := entry(k, v)
|
||||
out[tableKey] = tableValue
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// entry is a helper function that checks the json tags to make sure all fields
|
||||
// are tracked. And no excess fields are tracked.
|
||||
func entry(v any, f map[string]Action) (string, map[string]Action) {
|
||||
vt := reflect.TypeOf(v)
|
||||
for vt.Kind() == reflect.Ptr {
|
||||
vt = vt.Elem()
|
||||
}
|
||||
|
||||
// This should never happen because audit.Audible only allows structs in
|
||||
// its union.
|
||||
if vt.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("audit table entry value must be a struct, got %T", v))
|
||||
}
|
||||
|
||||
name := structName(vt)
|
||||
|
||||
// Use the flattenStructFields to recurse anonymously embedded structs
|
||||
vv := reflect.ValueOf(v)
|
||||
diffs, err := flattenStructFields(vv, vv)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("audit table entry type %T failed to flatten", v))
|
||||
}
|
||||
|
||||
fcpy := make(map[string]Action, len(f))
|
||||
for k, v := range f {
|
||||
fcpy[k] = v
|
||||
}
|
||||
for _, d := range diffs {
|
||||
jsonTag := d.FieldType.Tag.Get("json")
|
||||
if jsonTag == "-" {
|
||||
// This field is explicitly ignored.
|
||||
continue
|
||||
}
|
||||
if _, ok := fcpy[jsonTag]; !ok {
|
||||
panic(fmt.Sprintf("audit table entry missing action for field %q in type %q", d.FieldType.Name, name))
|
||||
}
|
||||
delete(fcpy, jsonTag)
|
||||
}
|
||||
|
||||
// If there are any fields left in fcpy, they are extra fields that don't
|
||||
// exist in the struct. Don't track them.
|
||||
if len(fcpy) > 0 {
|
||||
panic(fmt.Sprintf("audit table entry has extra actions for type %q: %v", name, fcpy))
|
||||
}
|
||||
|
||||
return structName(vt), f
|
||||
}
|
||||
|
||||
func (t Action) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package audit
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// TestAuditableResources ensures that all auditable resources are included in
|
||||
// the Auditable interface and vice versa.
|
||||
func TestAuditableResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pkgs, err := packages.Load(&packages.Config{
|
||||
Mode: packages.NeedTypes,
|
||||
}, "../../coderd/audit")
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatal("expected one package")
|
||||
}
|
||||
auditPkg := pkgs[0]
|
||||
auditableType := auditPkg.Types.Scope().Lookup("Auditable")
|
||||
|
||||
// If any of these type cast fails, our Auditable interface is not what we
|
||||
// expect it to be.
|
||||
named, ok := auditableType.(*types.TypeName)
|
||||
require.True(t, ok, "expected Auditable to be a type name")
|
||||
|
||||
interfaceType, ok := named.Type().(*types.Named).Underlying().(*types.Interface)
|
||||
require.True(t, ok, "expected Auditable to be an interface")
|
||||
|
||||
unionType, ok := interfaceType.EmbeddedType(0).(*types.Union)
|
||||
require.True(t, ok, "expected Auditable to be a union")
|
||||
|
||||
found := make(map[string]bool)
|
||||
// Now we check we have all the resources in the AuditableResources
|
||||
for i := 0; i < unionType.Len(); i++ {
|
||||
// All types come across like 'github.com/coder/coder/coderd/database.<type>'
|
||||
typeName := unionType.Term(i).Type().String()
|
||||
_, ok := AuditableResources[typeName]
|
||||
assert.True(t, ok, "missing resource %q from AuditableResources", typeName)
|
||||
found[typeName] = true
|
||||
}
|
||||
|
||||
// Also check that all resources in the table are in the union. We could
|
||||
// have extra resources here.
|
||||
for name := range AuditableResources {
|
||||
_, ok := found[name]
|
||||
assert.True(t, ok, "extra resource %q found in AuditableResources", name)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue