Skip to content

Community Room Authorization

Overview

A team sub-community ("room") is a plain Community row scoped to a Team via team_id — same model, same feed/goals/members machinery as a public community (see Features → Communities § Team Sub-Community Fields). For a team_id-nil (public/free) community, none of the rules on this page apply — access is the existing public-community model, unaffected.

For a team_id-present community, two independent rules gate everything: who can read/write the room (this page), and who can manage the room's lifecycle (a separate, broader permission). They are enforced by different policy objects and do not imply each other — this is the trap a future contributor is most likely to fall into, and the reason this page exists.

Rule 1 — room access requires actual membership

CommunityAccessPolicy#can_access_room? (rails_api/app/services/community_access_policy.rb, shipped OBJ-2302, PR #2199):

ruby
def can_access_room?
  return true if @community.team_id.blank?
  return true if @user&.admin

  @user.present? && CommunityMember.exists?(user_id: @user.id, community_id: @community.id)
end
  • team_id blank (public/free community) — always accessible. This policy is a no-op outside team rooms.
  • @user&.admin — a global site admin (User#admin, the app-wide admin flag), not a team-role admin/owner. Bypasses the membership check entirely.
  • Everyone else — must have an actual CommunityMember row for that specific room. Being a seated member of the team is not enough; team-query data (isMember on teamHome.rooms[] / teamSettings.subCommunities[]) reads the same underlying membership, so it stays consistent with what this policy enforces.

Gated call sites — reads (rails_api/app/graphql/resolvers/community_queries.rb, via authorize_room_access!):

QueryBehavior on failure
community(id:)FORBIDDEN GraphQL error
communityFeed(communityId:)FORBIDDEN GraphQL error
communityMembers(communityId:)FORBIDDEN GraphQL error
communityBadges(communityId:)FORBIDDEN GraphQL error

Gated call sites — writes:

Mutation / InteractionGuardBehavior on failure
Social::CreateCommunityPostrequire_room_access (calls CommunityAccessPolicy)fail_with(code: :forbidden)
Social::AddPostCommentrequire_room_access (calls CommunityAccessPolicy)fail_with(code: :forbidden)

Do not loosen this policy to work around a 403. A member who isn't seated on the room shouldn't read or post to it by direct API call any more than through the UI — that's the exact gap OBJ-2302 closed. If a legitimate new surface needs different access, it needs its own reasoned rule, not a widened can_access_room?.

Social::AddPostComment carries the same require_room_access guard as Social::CreateCommunityPost (OBJ-2347) — comment creation is covered by this page's rules, not just post creation.

Rule 2 — archived rooms are read-only, server-enforced

archived_at is a plain column, not a soft-delete marker (acts_as_paranoid's deleted_at is separate) — an archived room's record and feed stay live for a 30-day retention window (Teams::ArchiveSubCommunity::ARCHIVE_WINDOW), readable by existing members, not writable by anyone.

Social::CreateCommunityPost and Social::AddPostComment both carry a require_not_archived guard — the post path added v4.35 Phase 1 (OBJ-1825), mirroring the pre-existing not_archived? guard in Teams::JoinSubCommunity; the comment path added later (OBJ-2347), mirroring CreateCommunityPost:

ruby
def require_not_archived
  return unless community

  fail_with(error: 'This room is archived', code: :conflict) if community.archived_at.present?
end

Ordered after require_room_access in the guard chain — membership is checked first, then room state. Failure returns code: :conflict, which Mutations::CreateCommunityPost#resolve and Mutations::AddPostComment#resolve both surface as a normal { post: nil, errors: [...] } / { post_comment: nil, errors: [...] } GraphQL response, not a hard error — this is a validation-style failure, not an authorization failure. A :forbidden from Rule 1's guard is different: both resolvers raise it as a hard GraphQL::ExecutionError with extensions: { code: 'FORBIDDEN' } instead.

Client-side suppression alone is not a control. ionic_frontend/src/views/Community.vue's team-room mode suppresses the composer and post actions when a room is archived (canPostToRoom/readOnly wired down to CommunityFeed.vue/CommunityFeedCard.vue), but that's UI honesty, not enforcement — the require_not_archived guard above is what actually stops a member from posting to an archived room via dev tools, a replay, or any non-honest client. Any new mutation that writes to a CommunityPost, comment, or reaction should carry the same two guards; AddPostComment now does (OBJ-2347). There is no PostReaction write path today — rails_api/app/models/post_reaction.rb and the read-only aggregation in community_queries.rb are the only call sites — but whoever builds one must add require_room_access and require_not_archived from the start, not retrofit them later.

What Rule 1 does not cover — managing a room is not being in it

Room lifecycle mutations — archive, un-archive, set-default — are gated by a separate policy: TeamAccessPolicy#can?(:manage_team), a team-role check (team owner/admin), not CommunityAccessPolicy. Teams::ArchiveSubCommunity, Teams::UnarchiveSubCommunity, and Teams::SetDefaultSubCommunity all guard on policy.can?(:manage_team) and never check CommunityMember membership at all.

Accepted, deliberate consequence: a team owner/admin can archive or set a default for a room they are not a member of, but cannot open its feed (Rule 1 still applies to them, since TeamAccessPolicy's manage_team permission doesn't imply a CommunityMember row, and they're a team-role admin, not User#admin). This is correct, not a bug — managing a room's lifecycle and reading its contents are different concerns. Recorded here so nobody "fixes" it into a bypass later.


Last updated: 2026-08-20 — Social::AddPostComment closed the comment-path room-access and archived-room gaps (OBJ-2347); this page no longer documents the comment path as an open hole. Originally written for v4.35 Phase 1 (OBJ-1825) once the archived-room write guard (Rule 2) landed server-side, per Roy's review note that this contract needed to state both rules in one place.

Loading…