- Notifications
You must be signed in to change notification settings - Fork928
chore: enforce orgid in audit logs where required#12283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -4,6 +4,7 @@ import ( | ||
"context" | ||
"database/sql" | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
@@ -25,6 +26,9 @@ type RequestParams struct { | ||
Audit Auditor | ||
Log slog.Logger | ||
// OrganizationID is only provided when possible. If an audit resource extends | ||
// beyond the org scope, leave this as the nil uuid. | ||
OrganizationID uuid.UUID | ||
Request *http.Request | ||
Action database.AuditAction | ||
AdditionalFields json.RawMessage | ||
@@ -96,7 +100,7 @@ func ResourceTarget[T Auditable](tgt T) string { | ||
case database.HealthSettings: | ||
return "" // no target? | ||
default: | ||
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) | ||
} | ||
} | ||
@@ -129,7 +133,7 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { | ||
// Artificial ID for auditing purposes | ||
return typed.ID | ||
default: | ||
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) | ||
} | ||
} | ||
@@ -160,8 +164,57 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { | ||
case database.HealthSettings: | ||
return database.ResourceTypeHealthSettings | ||
default: | ||
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) | ||
} | ||
} | ||
// ResourceRequiresOrgID will ensure given resources are always audited with an | ||
// organization ID. | ||
func ResourceRequiresOrgID[T Auditable]() bool { | ||
var tgt T | ||
switch any(tgt).(type) { | ||
case database.Template, database.TemplateVersion: | ||
return true | ||
case database.Workspace, database.WorkspaceBuild: | ||
return true | ||
case database.AuditableGroup: | ||
return true | ||
case database.User: | ||
return false | ||
case database.GitSSHKey: | ||
return false | ||
case database.APIKey: | ||
return false | ||
case database.License: | ||
return false | ||
case database.WorkspaceProxy: | ||
return false | ||
case database.AuditOAuthConvertState: | ||
// The merge state is for the given user | ||
return false | ||
case database.HealthSettings: | ||
// Artificial ID for auditing purposes | ||
return false | ||
default: | ||
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) | ||
} | ||
} | ||
// requireOrgID will either panic (in unit tests) or log an error (in production) | ||
// if the given resource requires an organization ID and the provided ID is nil. | ||
func requireOrgID[T Auditable](ctx context.Context, id uuid.UUID, log slog.Logger) uuid.UUID { | ||
if ResourceRequiresOrgID[T]() && id == uuid.Nil { | ||
var tgt T | ||
resourceName := fmt.Sprintf("%T", tgt) | ||
if flag.Lookup("test.v") != nil { | ||
// In unit tests we panic to fail the tests | ||
panic(fmt.Sprintf("missing required organization ID for resource %q", resourceName)) | ||
} | ||
log.Error(ctx, "missing required organization ID for resource in audit log", | ||
slog.F("resource", resourceName), | ||
Comment on lines +210 to +214 Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. maybe no harm to also include resource type? Edit: %T does that, the varname maybe we can do MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I think %+v is too much and we'd have to be careful with any secrets. | ||
) | ||
} | ||
return id | ||
} | ||
// InitRequest initializes an audit log for a request. It returns a function | ||
@@ -243,6 +296,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request | ||
StatusCode: int32(sw.Status), | ||
RequestID: httpmw.RequestID(p.Request), | ||
AdditionalFields: p.AdditionalFields, | ||
OrganizationID: requireOrgID[T](logCtx, p.OrganizationID, p.Log), | ||
} | ||
err := p.Audit.Export(ctx, auditLog) | ||
if err != nil { | ||
@@ -276,7 +330,7 @@ func BackgroundAudit[T Auditable](ctx context.Context, p *BackgroundAuditParams[ | ||
ID: uuid.New(), | ||
Time: dbtime.Now(), | ||
UserID: p.UserID, | ||
OrganizationID:requireOrgID[T](ctx,p.OrganizationID, p.Log), | ||
Ip: ip, | ||
UserAgent: sql.NullString{}, | ||
ResourceType: either(p.Old, p.New, ResourceType[T], p.Action), | ||
Uh oh!
There was an error while loading.Please reload this page.