Deferred from PR LeanerCloud/cloud-commitments-cli#1737, where CodeRabbit proposed it. Filing so the idea is tracked rather than dismissed.
The suggestion
Route group mutations through recorded domain events instead of writing the group row directly: introduce an event store, define group events (created / permissions-changed / accounts-changed / deleted), append on each mutation, and rebuild group state from a projection.
That PR closes LeanerCloud/cloud-commitments-cli#1550, a p1 privilege escalation. Its whole change is a grant ceiling plus two guards on an existing write path. Event sourcing is an architectural programme, not a fix: it introduces an event store, an append path, a projection, replay and versioning semantics, a migration for existing groups, and a new consistency model for every reader of groups. Landing it inside a security fix would have made the security change unreviewable, and the escalation would have stayed open for the duration.
What it would actually buy
Worth stating honestly, because the case is not zero:
What it would cost
- Every reader of the
groups table becomes a projection consumer, or the projection must be kept synchronously consistent — which removes most of the benefit.
- Replay/versioning semantics have to be designed and maintained.
- A migration has to synthesise an initial event per existing group.
- The seeded,
system_managed groups are owned by SQL migrations; those writes bypass the API entirely and would need their own story.
Suggested smaller step first
If the goal is auditability rather than event sourcing as such, a group_permission_audit table written in the same transaction as UpdateGroup — actor, group, before, after, timestamp — delivers most of the value at a fraction of the cost and no change to how groups are read. Worth evaluating that before committing to a projection-based model.
Related: LeanerCloud/cloud-commitments-cli#1629 (the silent-drop failure this would have made recoverable), LeanerCloud/cloud-commitments-cli#1737.
Findings from the 2026-09-02 codebase audit
Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.
A03-024 (low)
One premise here needs correcting before the design work starts. The note says groups.created_by exists but lacks a changed_by for later edits; in fact created_by is never populated either. CreateGroupAPI receives the real actorUserID from the handler (handler_groups.go:53) and then passes a literal "" to CreateGroup for every caller (internal/auth/service_api.go:311-313), with a comment explaining that the column is a UUID FK and actorUserID may be the non-UUID admin-API-key sentinel. Only the sentinel case (group_ceiling.go:40) actually cannot be stored, so human admins leave created_by NULL for no reason. The one-line fix is to pass actorUserID when it is not AdminAPIKeyActorID and "" only for the sentinel, which gives the audit trail a real creation record to build on rather than a column that is uniformly empty. Audit finding A03-024.
Deferred from PR LeanerCloud/cloud-commitments-cli#1737, where CodeRabbit proposed it. Filing so the idea is tracked rather than dismissed.
The suggestion
Route group mutations through recorded domain events instead of writing the group row directly: introduce an event store, define group events (created / permissions-changed / accounts-changed / deleted), append on each mutation, and rebuild group state from a projection.
Why it was not done in LeanerCloud/cloud-commitments-cli#1737
That PR closes LeanerCloud/cloud-commitments-cli#1550, a p1 privilege escalation. Its whole change is a grant ceiling plus two guards on an existing write path. Event sourcing is an architectural programme, not a fix: it introduces an event store, an append path, a projection, replay and versioning semantics, a migration for existing groups, and a new consistency model for every reader of
groups. Landing it inside a security fix would have made the security change unreviewable, and the escalation would have stayed open for the duration.What it would actually buy
Worth stating honestly, because the case is not zero:
UpdateGroupoverwritespermissionsin place. sec(frontend): group edit drops unrepresentable permissions and widens their resource to * cloud-commitments-cli#1629 is a good illustration: an admin edit silently dropped verbs and nothing recorded what they had been. An append-only log would make that recoverable and attributable.groups.created_byexists but there is nochanged_byfor later edits.What it would cost
groupstable becomes a projection consumer, or the projection must be kept synchronously consistent — which removes most of the benefit.system_managedgroups are owned by SQL migrations; those writes bypass the API entirely and would need their own story.Suggested smaller step first
If the goal is auditability rather than event sourcing as such, a
group_permission_audittable written in the same transaction asUpdateGroup— actor, group, before, after, timestamp — delivers most of the value at a fraction of the cost and no change to how groups are read. Worth evaluating that before committing to a projection-based model.Related: LeanerCloud/cloud-commitments-cli#1629 (the silent-drop failure this would have made recoverable), LeanerCloud/cloud-commitments-cli#1737.
Findings from the 2026-09-02 codebase audit
Added by an automated audit of
3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd(tip oforigin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report:docs/audits/codebase-audit-2026-09-02.md.A03-024 (low)
One premise here needs correcting before the design work starts. The note says
groups.created_byexists but lacks achanged_byfor later edits; in factcreated_byis never populated either.CreateGroupAPIreceives the realactorUserIDfrom the handler (handler_groups.go:53) and then passes a literal""toCreateGroupfor every caller (internal/auth/service_api.go:311-313), with a comment explaining that the column is a UUID FK andactorUserIDmay be the non-UUID admin-API-key sentinel. Only the sentinel case (group_ceiling.go:40) actually cannot be stored, so human admins leavecreated_byNULL for no reason. The one-line fix is to passactorUserIDwhen it is notAdminAPIKeyActorIDand""only for the sentinel, which gives the audit trail a real creation record to build on rather than a column that is uniformly empty. Audit finding A03-024.