mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 03:16:43 +00:00
3f69b76b06
Session IP/User-Agent binding was log-only by default, so a stolen session token granted durable any-origin access. IP-change enforcement already existed behind PAD_IP_CHANGE_ENFORCE=strict; this extends the same single toggle to also enforce the User-Agent-hash binding. When strict enforce is ON, a request whose client IP OR User-Agent hash no longer matches the session's stored binding now revokes the session (DeleteSessionIfExists) and rejects the request (401 for API, revoked-passthrough for public/browser paths), killing the stolen token. When enforce is OFF (default), behavior is unchanged: UA mismatch is logged (slog only, no new audit row) and the request proceeds, so existing self-host users see no behavior change and routine client churn (browser/WebView updates, DevTools emulation, mobile-app rebuilds) is tolerated. The UA hash is stable within a real session, so UA-mismatch enforce carries fewer false positives than IP enforce (mobile roaming, VPN toggles, carrier NAT) — documented in the handler comment. Adds the ActionSessionUAChanged audit action, emitted only in strict mode. No DB migration: reuses the existing IPChangeEnforce config flag and the existing session store primitives. Claude-Session: https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF
142 lines
6.1 KiB
Go
142 lines
6.1 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// Item-level actions (existing)
|
|
var ValidActions = []string{
|
|
"created", "updated", "archived", "restored", "moved", "read", "searched",
|
|
}
|
|
|
|
// Audit action constants for auth/admin events
|
|
const (
|
|
ActionLogin = "login"
|
|
ActionLoginFailed = "login_failed"
|
|
ActionLogout = "logout"
|
|
ActionBootstrap = "bootstrap"
|
|
ActionRegister = "register"
|
|
ActionPasswordChanged = "password_changed"
|
|
ActionPasswordReset = "password_reset"
|
|
ActionTokenCreated = "token_created"
|
|
ActionTokenRevoked = "token_revoked"
|
|
ActionTokenRotated = "token_rotated"
|
|
ActionTOTPEnabled = "totp_enabled"
|
|
ActionTOTPDisabled = "totp_disabled"
|
|
ActionMemberInvited = "member_invited"
|
|
ActionMemberRemoved = "member_removed"
|
|
ActionRoleChanged = "role_changed"
|
|
ActionSettingsChanged = "settings_changed"
|
|
ActionOAuthLogin = "oauth_login"
|
|
ActionOAuthLoginFailed = "oauth_login_failed"
|
|
ActionPlanChanged = "plan_changed"
|
|
// ActionPlanOverridesChanged is logged when an admin updates a
|
|
// user's plan_overrides JSON via the admin user-detail page.
|
|
// Surfaces per-user storage / workspace / API-token quota
|
|
// overrides in the audit feed so operators can correlate a
|
|
// mysteriously-allowed upload with the override that enabled it.
|
|
ActionPlanOverridesChanged = "plan_overrides_changed"
|
|
ActionPasswordResetByAdmin = "password_reset_by_admin"
|
|
ActionUserDisabled = "user_disabled"
|
|
ActionUserEnabled = "user_enabled"
|
|
ActionAccountDeleted = "account_deleted"
|
|
// ActionEmailVerified is logged when a user confirms their email address
|
|
// via a verification link (POST /auth/verify-email). PLAN-1933 / TASK-1936.
|
|
ActionEmailVerified = "email_verified"
|
|
// ActionEmailVerifiedByAdmin is logged when an admin force-verifies a
|
|
// user's email from the admin console (DR-7). PLAN-1933 / TASK-1936.
|
|
ActionEmailVerifiedByAdmin = "email_verified_by_admin"
|
|
// ActionSessionIPChanged is logged when a session presents a different
|
|
// client IP than the one recorded at creation. We don't strict-check IP
|
|
// by default (that breaks legitimate geo shifts — VPN toggle, mobile
|
|
// roaming) but surface the change to the audit log for detection. In
|
|
// deployments configured with PAD_IP_CHANGE_ENFORCE=strict the middleware
|
|
// additionally rejects the request.
|
|
ActionSessionIPChanged = "session_ip_changed"
|
|
// ActionSessionUAChanged is logged when a session presents a different
|
|
// User-Agent hash than the one recorded at creation. Like the IP signal
|
|
// this is surfaced to the audit log for detection; in deployments
|
|
// configured with PAD_IP_CHANGE_ENFORCE=strict the middleware additionally
|
|
// revokes the session and rejects the request. The UA hash is stable for
|
|
// the life of a real session (a browser doesn't rewrite its own UA mid-
|
|
// session), so a mismatch is a stronger theft signal than an IP change —
|
|
// which is precisely why UA enforcement carries fewer false positives than
|
|
// IP enforcement. This audit row is only emitted in strict mode; log-only
|
|
// mode keeps the historical slog-only behavior to avoid changing the audit
|
|
// feed for existing self-host users.
|
|
ActionSessionUAChanged = "session_ua_changed"
|
|
// ActionStripeEventUnmarked is logged when /admin/stripe-event-unmark
|
|
// rolls back a row from stripe_processed_events (TASK-736). The
|
|
// endpoint intentionally reopens Stripe retry windows, so a persisted
|
|
// audit trail is required — a compromised cloud_secret could otherwise
|
|
// spam unmarks invisible to the admin /audit-log UI.
|
|
ActionStripeEventUnmarked = "stripe_event_unmarked"
|
|
// ActionPaymentFailedEmailSent is logged when the sidecar triggers the
|
|
// /admin/payment-failed endpoint and pad dispatches a failed-payment
|
|
// notification to the user. Audit trail exists so operators can prove
|
|
// a customer was notified before a dunning-related plan change.
|
|
ActionPaymentFailedEmailSent = "payment_failed_email_sent"
|
|
)
|
|
|
|
type Activity struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id,omitempty"`
|
|
DocumentID string `json:"document_id,omitempty"`
|
|
Action string `json:"action"`
|
|
Actor string `json:"actor"`
|
|
Source string `json:"source"`
|
|
Metadata string `json:"metadata,omitempty"` // JSON
|
|
UserID string `json:"user_id,omitempty"`
|
|
IPAddress string `json:"ip_address,omitempty"`
|
|
UserAgent string `json:"user_agent,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Enrichment fields — populated by handlers, not stored in DB
|
|
ItemTitle string `json:"item_title,omitempty"`
|
|
ItemSlug string `json:"item_slug,omitempty"`
|
|
ItemRef string `json:"item_ref,omitempty"` // e.g. "BUG-1748" — computed from the referenced item
|
|
CollectionSlug string `json:"collection_slug,omitempty"`
|
|
ActorName string `json:"actor_name,omitempty"`
|
|
}
|
|
|
|
type ActivityListParams struct {
|
|
Action string
|
|
Actor string
|
|
Source string
|
|
// Since, when non-zero, restricts results to activity created on or
|
|
// after this instant (a.created_at >= Since). Applied in the SQL query
|
|
// so LIMIT counts post-filter rows. Used by `pad project activity
|
|
// --since` and the pad_project.activity MCP action.
|
|
Since time.Time
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
// AuditLogParams are query parameters for the audit log endpoint.
|
|
type AuditLogParams struct {
|
|
Action string
|
|
Actor string
|
|
WorkspaceID string
|
|
Days int
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
// TimelineEntry represents a single entry in the unified item timeline.
|
|
// It wraps one of: a comment, an activity, or a version.
|
|
type TimelineEntry struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"` // "comment", "activity", "version"
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Actor string `json:"actor"`
|
|
ActorName string `json:"actor_name,omitempty"`
|
|
Source string `json:"source"`
|
|
Comment *Comment `json:"comment,omitempty"`
|
|
Activity *Activity `json:"activity,omitempty"`
|
|
Version *Version `json:"version,omitempty"`
|
|
}
|
|
|
|
// TimelineResponse is the paginated response from the timeline endpoint.
|
|
type TimelineResponse struct {
|
|
Entries []TimelineEntry `json:"entries"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|