6 Commits

Author SHA1 Message Date
Dan Milne
782e197d91 Fix access check form: use GET so results render
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
Build and publish image / build (push) Has been cancelled
The access check form POSTed and re-rendered :new with a 200 HTML
response, which Turbo rejects ("Form responses must redirect to
another location"), so the result panel never appeared. Since the
check is a read-only query, switch to a GET form and fold the lookup
into the new action. Results are now bookmarkable via the URL.

Bump version to 0.16.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:42:57 +10:00
Dan Milne
020759bfb3 Fix invalid require-trusted-types-for CSP directive
require-trusted-types-for only accepts 'script'; emitting 'none'
produced an invalid directive that browsers rejected. Omit the
directive entirely to leave Trusted Types unenforced (needed for
WebAuthn). Bump version to 0.16.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:39:35 +10:00
Dan Milne
85f50bfc96 Add GitHub Actions workflow to build and publish image to GHCR
Builds the production Docker image and pushes it to
ghcr.io/dkam/clinch on pushes to main (edge + sha tags) and on v*
release tags (vX.Y.Z, vX.Y, latest). amd64 only, with GHA layer caching.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:02:29 +10:00
Dan Milne
b55139eb1c Fix Sentry config to use Sentry.init API
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
The Sentry setup used a config.sentry.* accessor that sentry-rails has
never provided, so booting with SENTRY_DSN set raised NoMethodError
during environment load (e.g. db:prepare). The code only ran once a DSN
was configured, which is why it surfaced in production now.

Rewrites config/initializers/sentry.rb to call Sentry.init, the actual
sentry-ruby API, and removes the duplicate broken block from
production.rb. Verified production boots with SENTRY_DSN set
(Sentry.initialized? == true) and that the no-DSN path still early-returns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:57:26 +10:00
Dan Milne
8f578ed3f4 Upgrade Ruby to 4.0.5
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
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:51:28 +10:00
Dan Milne
aa5736ddab Update gems and fix lint to clear CI failures
Bumps dependencies (jwt 3.2.0, puma 8.0.2, net-imap 0.6.4.1 and others
via bundle update) to resolve bundler-audit advisories, and applies
standardrb autofixes so the lint job passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:51:23 +10:00
21 changed files with 195 additions and 207 deletions

56
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Build and publish image
on:
push:
branches: [ main ]
tags: [ 'v*' ]
# Only one build per ref at a time; cancel superseded main builds.
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required to push to GHCR
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract image metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=edge,branch=main
type=sha,prefix=sha-,format=short,enable={{is_default_branch}}
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
flavor: |
latest=auto
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1 +1 @@
4.0.3
4.0.5

View File

@@ -8,7 +8,7 @@
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=4.0.3
ARG RUBY_VERSION=4.0.5
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
LABEL org.opencontainers.image.source=https://github.com/dkam/clinch

View File

@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.18)
action_text-trix (2.1.19)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
@@ -85,9 +85,9 @@ GEM
bigdecimal (4.1.2)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.24.1)
bootsnap (1.24.6)
msgpack (~> 1.2)
brakeman (8.0.4)
brakeman (8.0.5)
racc
builder (3.3.0)
bundler-audit (0.9.3)
@@ -102,11 +102,11 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
cbor (0.5.10.2)
cbor (0.5.10.3)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
concurrent-ruby (1.3.6)
concurrent-ruby (1.3.7)
connection_pool (3.0.2)
cose (1.3.1)
cbor (~> 0.5.9)
@@ -131,12 +131,12 @@ GEM
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
fugit (1.12.1)
fugit (1.12.2)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.8)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
@@ -151,13 +151,13 @@ GEM
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
jbuilder (2.15.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
json (2.19.4)
jwt (3.1.2)
json (2.19.9)
jwt (3.2.0)
base64
kamal (2.11.0)
kamal (2.12.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@@ -186,14 +186,14 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.1.0)
marcel (1.2.1)
matrix (0.4.3)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (5.27.0)
msgpack (1.8.0)
net-imap (0.6.4)
msgpack (1.8.3)
net-imap (0.6.4.1)
date
net-protocol
net-pop (0.1.2)
@@ -208,25 +208,25 @@ GEM
net-protocol
net-ssh (7.3.2)
nio4r (2.7.5)
nokogiri (1.19.3-aarch64-linux-gnu)
nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.3-aarch64-linux-musl)
nokogiri (1.19.4-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.19.3-arm-linux-gnu)
nokogiri (1.19.4-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.3-arm-linux-musl)
nokogiri (1.19.4-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.19.3-arm64-darwin)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-gnu)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-musl)
nokogiri (1.19.4-x86_64-linux-musl)
racc (~> 1.4)
openssl (4.0.1)
openssl (4.0.2)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
ostruct (0.6.3)
parallel (1.28.0)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
@@ -238,11 +238,11 @@ GEM
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
psych (5.4.0)
date
stringio
public_suffix (7.0.5)
puma (8.0.1)
puma (8.0.2)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
@@ -299,11 +299,11 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.1.0)
rubocop (1.84.2)
rubocop (1.87.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
@@ -321,20 +321,20 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
rubyzip (3.2.2)
rubyzip (3.4.0)
safety_net_attestation (0.5.0)
jwt (>= 2.0, < 4.0)
securerandom (0.4.1)
selenium-webdriver (4.43.0)
selenium-webdriver (4.45.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sentry-rails (6.5.0)
sentry-rails (6.6.2)
railties (>= 5.2.0)
sentry-ruby (~> 6.5.0)
sentry-ruby (6.5.0)
sentry-ruby (~> 6.6.2)
sentry-ruby (6.6.2)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
logger
@@ -344,7 +344,7 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
solid_cable (3.0.12)
solid_cable (4.0.0)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
@@ -360,13 +360,13 @@ GEM
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
sqlite3 (2.9.3-aarch64-linux-gnu)
sqlite3 (2.9.3-aarch64-linux-musl)
sqlite3 (2.9.3-arm-linux-gnu)
sqlite3 (2.9.3-arm-linux-musl)
sqlite3 (2.9.3-arm64-darwin)
sqlite3 (2.9.3-x86_64-linux-gnu)
sqlite3 (2.9.3-x86_64-linux-musl)
sqlite3 (2.9.5-aarch64-linux-gnu)
sqlite3 (2.9.5-aarch64-linux-musl)
sqlite3 (2.9.5-arm-linux-gnu)
sqlite3 (2.9.5-arm-linux-musl)
sqlite3 (2.9.5-arm64-darwin)
sqlite3 (2.9.5-x86_64-linux-gnu)
sqlite3 (2.9.5-x86_64-linux-musl)
sshkit (1.25.0)
base64
logger
@@ -374,10 +374,10 @@ GEM
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
standard (1.54.0)
standard (1.55.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.84.0)
rubocop (~> 1.87.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
@@ -389,20 +389,20 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
tailwindcss-rails (4.4.0)
tailwindcss-rails (4.6.0)
railties (>= 7.0.0)
tailwindcss-ruby (~> 4.0)
tailwindcss-ruby (4.2.4)
tailwindcss-ruby (4.2.4-aarch64-linux-gnu)
tailwindcss-ruby (4.2.4-aarch64-linux-musl)
tailwindcss-ruby (4.2.4-arm64-darwin)
tailwindcss-ruby (4.2.4-x86_64-linux-gnu)
tailwindcss-ruby (4.2.4-x86_64-linux-musl)
tailwindcss-ruby (4.3.1)
tailwindcss-ruby (4.3.1-aarch64-linux-gnu)
tailwindcss-ruby (4.3.1-aarch64-linux-musl)
tailwindcss-ruby (4.3.1-arm64-darwin)
tailwindcss-ruby (4.3.1-x86_64-linux-gnu)
tailwindcss-ruby (4.3.1-x86_64-linux-musl)
thor (1.5.0)
thruster (0.1.20)
thruster (0.1.20-aarch64-linux)
thruster (0.1.20-arm64-darwin)
thruster (0.1.20-x86_64-linux)
thruster (0.1.21)
thruster (0.1.21-aarch64-linux)
thruster (0.1.21-arm64-darwin)
thruster (0.1.21-x86_64-linux)
timeout (0.6.1)
tpm-key_attestation (0.14.1)
bindata (~> 2.4)
@@ -432,13 +432,13 @@ GEM
safety_net_attestation (~> 0.5.0)
tpm-key_attestation (~> 0.14.0)
websocket (1.2.11)
websocket-driver (0.8.0)
websocket-driver (0.8.1)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.5)
zeitwerk (2.8.2)
PLATFORMS
aarch64-linux

View File

@@ -2,17 +2,12 @@ module Admin
class AccessChecksController < BaseController
def new
load_options
end
def create
load_options
@user = User.find_by(id: params[:user_id])
@application = Application.find_by(id: params[:application_id])
return render :new unless @user && @application
return unless @user && @application
@allowed = @application.user_allowed?(@user)
@via = @user.groups & @application.allowed_groups
render :new
end
private

View File

@@ -156,7 +156,7 @@ module Api
end
def render_bearer_error(message)
render json: { error: message }, status: :unauthorized
render json: {error: message}, status: :unauthorized
end
def check_forward_auth_token
@@ -207,7 +207,7 @@ module Api
session[:return_to_after_authenticating] = original_url
login_params = { rd: original_url, rm: request.method }
login_params = {rd: original_url, rm: request.method}
login_url = "#{base_url}/signin?#{login_params.to_query}"
redirect_to login_url, allow_other_host: true, status: :found

View File

@@ -191,7 +191,7 @@ module Authentication
token = SecureRandom.urlsafe_base64(32)
Rails.cache.write(
"forward_auth_token:#{token}",
{ session_id: session_obj.id, host: bound_host },
{session_id: session_obj.id, host: bound_host},
expires_in: 60.seconds
)

View File

@@ -31,7 +31,7 @@ module ApplicationHelper
end
lines << "OIDC_DISCOVERY_URL=#{OidcJwtService.issuer_url}"
lines << "OIDC_PROVIDER_NAME='Clinch'"
lines << "OIDC_REQUIRE_PKCE=#{application.requires_pkce? ? 'true' : 'false'}"
lines << "OIDC_REQUIRE_PKCE=#{application.requires_pkce? ? "true" : "false"}"
lines
end

View File

@@ -35,7 +35,7 @@ module PrivateAddressCheck
return [ip] if ip
Resolv.getaddresses(host.to_s).filter_map { |a| parse_ip(a) }
rescue StandardError
rescue
# Resolution failure: surface no addresses. Callers treat "can't resolve" as
# not-provably-internal; the dial itself will then fail safely.
[]

View File

@@ -2,6 +2,6 @@ class TotpMailer < ApplicationMailer
def enabled(user)
@user = user
mail subject: "Two-factor authentication enabled on your account",
to: user.email_address
to: user.email_address
end
end

View File

@@ -5,7 +5,7 @@
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<%= form_with url: admin_access_path, method: :post, class: "space-y-4" do |form| %>
<%= form_with url: admin_access_path, method: :get, class: "space-y-4" do |form| %>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<%= form.label :user_id, "User", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>

View File

@@ -157,17 +157,5 @@ Rails.application.configure do
# Skip DNS rebinding protection for the default health check endpoint.
config.host_authorization = {exclude: ->(request) { request.path == "/up" }}
# Sentry configuration for production
# Only enabled if SENTRY_DSN environment variable is set
if ENV["SENTRY_DSN"].present?
config.sentry.enabled = true
# Performance monitoring: sample 20% of transactions for traces
# Adjust based on your traffic volume and Sentry plan limits
config.sentry.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", 0.2).to_f
# Continuous profiling: disabled by default in production due to cost
# Enable temporarily for performance investigations if needed
config.sentry.profiles_sample_rate = ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.0).to_f
end
# Sentry is configured in config/initializers/sentry.rb, gated on SENTRY_DSN.
end

View File

@@ -53,9 +53,10 @@ Rails.application.configure do
# Child sources: Allow self for any future iframes
policy.child_src :self
# Additional security headers for WebAuthn
# Required for WebAuthn to work properly
policy.require_trusted_types_for :none
# Do not enforce Trusted Types. The only valid value for
# require-trusted-types-for is 'script'; there is no 'none' token, so
# emitting it produces an invalid directive that browsers reject. To leave
# Trusted Types unenforced (needed for WebAuthn), omit the directive entirely.
# CSP reporting using report_uri (supported method)
policy.report_uri "/api/csp-violation-report"

View File

@@ -1,62 +1,44 @@
# Sentry configuration for error tracking and performance monitoring
# Only initializes if SENTRY_DSN environment variable is set
# Sentry configuration for error tracking and performance monitoring.
# Only initializes if the SENTRY_DSN environment variable is set.
return unless ENV["SENTRY_DSN"].present?
Rails.application.configure do
config.sentry.dsn = ENV["SENTRY_DSN"]
Sentry.init do |config|
config.dsn = ENV["SENTRY_DSN"]
# Set environment (defaults to Rails.env)
config.sentry.environment = ENV["SENTRY_ENVIRONMENT"] || Rails.env
# Environment label (defaults to Rails.env)
config.environment = ENV["SENTRY_ENVIRONMENT"] || Rails.env
# Set release version from Git or environment variable
config.sentry.release = ENV["SENTRY_RELEASE"] || `git rev-parse HEAD 2>/dev/null`.strip.presence || nil
# Release version from an env var or the current Git SHA
config.release = ENV["SENTRY_RELEASE"] || `git rev-parse HEAD 2>/dev/null`.strip.presence
# Sample rate for performance monitoring (0.0 to 1.0)
config.sentry.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", 0.1).to_f
# Enable profiling in development/staging, disable in production unless explicitly enabled
config.sentry.profiles_sample_rate = if Rails.env.production?
ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.0).to_f
else
ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.5).to_f
end
# Include additional context
config.sentry.before_send = lambda do |event, hint|
# Filter out sensitive information
if event.context[:extra]
event.context[:extra].reject! { |key, value|
key.to_s.match?(/password|secret|token|key/i) || value.to_s.match?(/password|secret/i)
}
# Only report from production unless explicitly enabled elsewhere.
config.enabled_environments =
if ENV["SENTRY_ENABLED_IN_DEVELOPMENT"] == "true"
%w[production development]
else
%w[production]
end
# Filter sensitive parameters
if event.context[:request]
event.context[:request].reject! { |key, value|
key.to_s.match?(/password|secret|token|key|authorization/i)
}
# Don't send cookies, request bodies, or user IPs by default.
config.send_default_pii = false
# Breadcrumbs for debugging
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
# Performance monitoring sample rate (0.0 to 1.0)
config.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", 0.1).to_f
# Profiling: disabled in production by default due to cost.
config.profiles_sample_rate =
if Rails.env.production?
ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.0).to_f
else
ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.5).to_f
end
event
end
# Include breadcrumbs for debugging
config.sentry.breadcrumbs_logger = [:active_support_logger, :http_logger]
# Send session data for user context
config.sentry.user_context = lambda do
if Current.user.present?
{
id: Current.user.id,
email: Current.user.email_address,
admin: Current.user.admin?
}
end
end
# Ignore common non-critical exceptions
config.sentry.excluded_exceptions += [
config.excluded_exceptions += [
"ActionController::RoutingError",
"ActionController::InvalidAuthenticityToken",
"ActionController::UnknownFormat",
@@ -66,75 +48,38 @@ Rails.application.configure do
"ActiveRecord::RecordNotFound"
]
# Add CSP-specific tags for security events
config.sentry.tags = lambda do
{
# Add application context
# Attach application/user context and scrub anything sensitive before sending.
config.before_send = lambda do |event, _hint|
event.tags = (event.tags || {}).merge(
app_name: "clinch",
app_environment: Rails.env,
# Add CSP policy status
csp_enabled: defined?(Rails.application.config.content_security_policy) &&
Rails.application.config.content_security_policy.present?
}
end
app_environment: Rails.env
)
# Enhance before_send to handle CSP events properly
config.sentry.before_send = lambda do |event, hint|
# Filter out sensitive information
if event.context[:extra]
event.context[:extra].reject! { |key, value|
if defined?(Current) && Current.respond_to?(:user) && Current.user
event.user = (event.user || {}).merge(
id: Current.user.id,
email: Current.user.email_address,
admin: Current.user.admin?
)
end
if event.extra.is_a?(Hash)
event.extra.reject! do |key, value|
key.to_s.match?(/password|secret|token|key/i) || value.to_s.match?(/password|secret/i)
}
end
# Filter sensitive parameters
if event.context[:request]
event.context[:request].reject! { |key, value|
key.to_s.match?(/password|secret|token|key|authorization/i)
}
end
# Special handling for CSP violations
if event.tags&.dig(:csp_violation)
# Ensure CSP violations have proper security context
event.context[:server] = event.context[:server] || {}
event.context[:server][:name] = "clinch-auth-service"
event.context[:server][:environment] = Rails.env
# Add additional security context
event.context[:extra] ||= {}
event.context[:extra][:security_context] = {
csp_reporting: true,
user_authenticated: event.context[:user].present?,
request_origin: event.context[:request]&.dig(:headers, "Origin"),
request_referer: event.context[:request]&.dig(:headers, "Referer")
}
end
end
event
end
# Add CSP-specific breadcrumbs for security events
config.sentry.before_breadcrumb = lambda do |breadcrumb, hint|
# Filter out sensitive breadcrumb data
if breadcrumb[:data]
breadcrumb[:data].reject! { |key, value|
key.to_s.match?(/password|secret|token|key|authorization/i) ||
value.to_s.match?(/password|secret/i)
}
end
# Mark CSP-related events
if breadcrumb[:message]&.include?("CSP Violation") ||
breadcrumb[:category]&.include?("csp")
breadcrumb[:data] ||= {}
breadcrumb[:data][:security_event] = true
breadcrumb[:data][:csp_violation] = true
# Scrub sensitive data out of breadcrumbs.
config.before_breadcrumb = lambda do |breadcrumb, _hint|
if breadcrumb.data.is_a?(Hash)
breadcrumb.data.reject! do |key, value|
key.to_s.match?(/password|secret|token|key|authorization/i) || value.to_s.match?(/password|secret/i)
end
end
breadcrumb
end
# Only send errors in production unless explicitly enabled
config.sentry.enabled = Rails.env.production? || ENV["SENTRY_ENABLED_IN_DEVELOPMENT"] == "true"
end

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Clinch
VERSION = "0.16.0"
VERSION = "0.16.2"
end

View File

@@ -96,7 +96,6 @@ Rails.application.routes.draw do
end
resources :groups
get "access", to: "access_checks#new"
post "access", to: "access_checks#create"
end
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)

View File

@@ -1,8 +1,8 @@
class AddOidcAuthorizationCodeIdToTokens < ActiveRecord::Migration[8.1]
def change
add_reference :oidc_access_tokens, :oidc_authorization_code,
null: true, foreign_key: true, index: true
null: true, foreign_key: true, index: true
add_reference :oidc_refresh_tokens, :oidc_authorization_code,
null: true, foreign_key: true, index: true
null: true, foreign_key: true, index: true
end
end

View File

@@ -15,8 +15,8 @@ module Admin
assert_match "alice@example.com", response.body
end
test "create returns 'can access' with via group when user is in an allowed group" do
post admin_access_path, params: {
test "returns 'can access' with via group when user is in an allowed group" do
get admin_access_path, params: {
user_id: users(:alice).id,
application_id: @kavita.id
}
@@ -25,9 +25,9 @@ module Admin
assert_match "Administrators", response.body # alice is in admin_group; kavita has admin_group
end
test "create returns 'cannot access' with reason when user shares no group with the app" do
test "returns 'cannot access' with reason when user shares no group with the app" do
lonely = User.create!(email_address: "lonely@example.com", password: "password123", skip_auto_assign: true)
post admin_access_path, params: {
get admin_access_path, params: {
user_id: lonely.id,
application_id: @kavita.id
}
@@ -36,8 +36,8 @@ module Admin
assert_match "shares no group", response.body
end
test "create renders form unchanged when ids are missing" do
post admin_access_path, params: {user_id: "", application_id: ""}
test "renders form unchanged when ids are missing" do
get admin_access_path, params: {user_id: "", application_id: ""}
assert_response :success
# No result panel should render. The panel-only phrases:
refute_match "Granted via", response.body

View File

@@ -27,7 +27,7 @@ module Admin
@group.applications = [applications(:kavita_app)]
patch admin_group_path(@group), params: {
group: { name: @group.name }
group: {name: @group.name}
}
assert_redirected_to admin_group_path(@group)

View File

@@ -186,7 +186,7 @@ module Api
# Under default-deny the user must be in at least one group to access the app.
# This rewritten test verifies that when an app's headers_config disables the
# groups header, no x-remote-groups is sent regardless of memberships.
app = grant_everyone_access Application.create!(
grant_everyone_access Application.create!(
name: "Headers Hidden", slug: "headers-hidden", app_type: "forward_auth",
domain_pattern: "hidden.example.com",
active: true,
@@ -559,7 +559,7 @@ module Api
end
test "should track failed attempts and eventually rate limit" do
cache = Rails.application.config.forward_auth_cache
Rails.application.config.forward_auth_cache
# Make 50 failed requests (no session = unauthorized)
50.times do

View File

@@ -17,7 +17,11 @@ module SessionTestHelper
# written under the old "empty allowed_groups = public" rule keep working.
# New tests should attach groups explicitly to model real access intent.
def grant_everyone_access(app)
everyone = (groups(:everyone) rescue Group.find_by(auto_assign: true))
everyone = begin
groups(:everyone)
rescue
Group.find_by(auto_assign: true)
end
app.allowed_groups << everyone unless app.allowed_groups.include?(everyone)
app
end