Files
clinch/app/models/group.rb
Dan Milne 03dfdbd83a
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
Default-deny access control with group flags and access enumeration
Replaces the implicit "empty allowed_groups means public" rule with
explicit default-deny across both OIDC and ForwardAuth. Adds two boolean
flags on Group — auto_assign (Keycloak-style auto-join on user create)
and admin (members can reach the admin panel) — and drops the
users.admin column entirely. Adds "Users with access" and "Accessible
applications" panels with via-group badges on the application/user show
pages.

BEHAVIOR CHANGE: a ForwardAuth app with no allowed_groups previously
bypassed authentication entirely; it now returns 403 like any other
unauthorized request. The data migration seeds an "everyone" group and
attaches it to all previously group-less apps to preserve behavior on
existing installs. An "admins" group is seeded and backfilled from any
user with the old admin column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 15:53:27 +10:00

47 lines
1.4 KiB
Ruby

class Group < ApplicationRecord
has_many :user_groups, dependent: :destroy
has_many :users, through: :user_groups
has_many :application_groups, dependent: :destroy
has_many :applications, through: :application_groups
# Reserved OIDC claim names that should not be overridden
RESERVED_CLAIMS = %w[
iss sub aud exp iat nbf jti nonce azp
email email_verified preferred_username name
groups
].freeze
validates :name, presence: true, uniqueness: {case_sensitive: false}
normalizes :name, with: ->(name) { name.strip.downcase }
validate :no_reserved_claim_names
scope :auto_assign, -> { where(auto_assign: true) }
scope :admin, -> { where(admin: true) }
before_destroy :ensure_other_admin_group_exists
# Parse custom_claims JSON field
def parsed_custom_claims
return {} if custom_claims.blank?
custom_claims.is_a?(Hash) ? custom_claims : {}
end
private
def ensure_other_admin_group_exists
return unless admin?
return if Group.where(admin: true).where.not(id: id).exists?
errors.add(:base, "cannot delete the last administrators group")
throw :abort
end
def no_reserved_claim_names
return if custom_claims.blank?
reserved_used = parsed_custom_claims.keys.map(&:to_s) & RESERVED_CLAIMS
if reserved_used.any?
errors.add(:custom_claims, "cannot override reserved OIDC claims: #{reserved_used.join(", ")}")
end
end
end