Compare commits
4 Commits
8f578ed3f4
...
v0.16.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782e197d91 | ||
|
|
020759bfb3 | ||
|
|
85f50bfc96 | ||
|
|
b55139eb1c |
56
.github/workflows/build.yml
vendored
Normal file
56
.github/workflows/build.yml
vendored
Normal 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
|
||||||
@@ -2,17 +2,12 @@ module Admin
|
|||||||
class AccessChecksController < BaseController
|
class AccessChecksController < BaseController
|
||||||
def new
|
def new
|
||||||
load_options
|
load_options
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
load_options
|
|
||||||
@user = User.find_by(id: params[:user_id])
|
@user = User.find_by(id: params[:user_id])
|
||||||
@application = Application.find_by(id: params[:application_id])
|
@application = Application.find_by(id: params[:application_id])
|
||||||
return render :new unless @user && @application
|
return unless @user && @application
|
||||||
|
|
||||||
@allowed = @application.user_allowed?(@user)
|
@allowed = @application.user_allowed?(@user)
|
||||||
@via = @user.groups & @application.allowed_groups
|
@via = @user.groups & @application.allowed_groups
|
||||||
render :new
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<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 class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :user_id, "User", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
<%= form.label :user_id, "User", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||||
|
|||||||
@@ -157,17 +157,5 @@ Rails.application.configure do
|
|||||||
# Skip DNS rebinding protection for the default health check endpoint.
|
# Skip DNS rebinding protection for the default health check endpoint.
|
||||||
config.host_authorization = {exclude: ->(request) { request.path == "/up" }}
|
config.host_authorization = {exclude: ->(request) { request.path == "/up" }}
|
||||||
|
|
||||||
# Sentry configuration for production
|
# Sentry is configured in config/initializers/sentry.rb, gated on SENTRY_DSN.
|
||||||
# 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
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,9 +53,10 @@ Rails.application.configure do
|
|||||||
# Child sources: Allow self for any future iframes
|
# Child sources: Allow self for any future iframes
|
||||||
policy.child_src :self
|
policy.child_src :self
|
||||||
|
|
||||||
# Additional security headers for WebAuthn
|
# Do not enforce Trusted Types. The only valid value for
|
||||||
# Required for WebAuthn to work properly
|
# require-trusted-types-for is 'script'; there is no 'none' token, so
|
||||||
policy.require_trusted_types_for :none
|
# 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)
|
# CSP reporting using report_uri (supported method)
|
||||||
policy.report_uri "/api/csp-violation-report"
|
policy.report_uri "/api/csp-violation-report"
|
||||||
|
|||||||
@@ -1,62 +1,44 @@
|
|||||||
# Sentry configuration for error tracking and performance monitoring
|
# Sentry configuration for error tracking and performance monitoring.
|
||||||
# Only initializes if SENTRY_DSN environment variable is set
|
# Only initializes if the SENTRY_DSN environment variable is set.
|
||||||
|
|
||||||
return unless ENV["SENTRY_DSN"].present?
|
return unless ENV["SENTRY_DSN"].present?
|
||||||
|
|
||||||
Rails.application.configure do
|
Sentry.init do |config|
|
||||||
config.sentry.dsn = ENV["SENTRY_DSN"]
|
config.dsn = ENV["SENTRY_DSN"]
|
||||||
|
|
||||||
# Set environment (defaults to Rails.env)
|
# Environment label (defaults to Rails.env)
|
||||||
config.sentry.environment = ENV["SENTRY_ENVIRONMENT"] || Rails.env
|
config.environment = ENV["SENTRY_ENVIRONMENT"] || Rails.env
|
||||||
|
|
||||||
# Set release version from Git or environment variable
|
# Release version from an env var or the current Git SHA
|
||||||
config.sentry.release = ENV["SENTRY_RELEASE"] || `git rev-parse HEAD 2>/dev/null`.strip.presence || nil
|
config.release = ENV["SENTRY_RELEASE"] || `git rev-parse HEAD 2>/dev/null`.strip.presence
|
||||||
|
|
||||||
# Sample rate for performance monitoring (0.0 to 1.0)
|
# Only report from production unless explicitly enabled elsewhere.
|
||||||
config.sentry.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", 0.1).to_f
|
config.enabled_environments =
|
||||||
|
if ENV["SENTRY_ENABLED_IN_DEVELOPMENT"] == "true"
|
||||||
# Enable profiling in development/staging, disable in production unless explicitly enabled
|
%w[production development]
|
||||||
config.sentry.profiles_sample_rate = if Rails.env.production?
|
else
|
||||||
ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", 0.0).to_f
|
%w[production]
|
||||||
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)
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter sensitive parameters
|
# Don't send cookies, request bodies, or user IPs by default.
|
||||||
if event.context[:request]
|
config.send_default_pii = false
|
||||||
event.context[:request].reject! { |key, value|
|
|
||||||
key.to_s.match?(/password|secret|token|key|authorization/i)
|
# 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
|
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
|
# Ignore common non-critical exceptions
|
||||||
config.sentry.excluded_exceptions += [
|
config.excluded_exceptions += [
|
||||||
"ActionController::RoutingError",
|
"ActionController::RoutingError",
|
||||||
"ActionController::InvalidAuthenticityToken",
|
"ActionController::InvalidAuthenticityToken",
|
||||||
"ActionController::UnknownFormat",
|
"ActionController::UnknownFormat",
|
||||||
@@ -66,75 +48,38 @@ Rails.application.configure do
|
|||||||
"ActiveRecord::RecordNotFound"
|
"ActiveRecord::RecordNotFound"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add CSP-specific tags for security events
|
# Attach application/user context and scrub anything sensitive before sending.
|
||||||
config.sentry.tags = lambda do
|
config.before_send = lambda do |event, _hint|
|
||||||
{
|
event.tags = (event.tags || {}).merge(
|
||||||
# Add application context
|
|
||||||
app_name: "clinch",
|
app_name: "clinch",
|
||||||
app_environment: Rails.env,
|
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
|
|
||||||
|
|
||||||
# Enhance before_send to handle CSP events properly
|
if defined?(Current) && Current.respond_to?(:user) && Current.user
|
||||||
config.sentry.before_send = lambda do |event, hint|
|
event.user = (event.user || {}).merge(
|
||||||
# Filter out sensitive information
|
id: Current.user.id,
|
||||||
if event.context[:extra]
|
email: Current.user.email_address,
|
||||||
event.context[:extra].reject! { |key, value|
|
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)
|
key.to_s.match?(/password|secret|token|key/i) || value.to_s.match?(/password|secret/i)
|
||||||
}
|
end
|
||||||
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
|
event
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add CSP-specific breadcrumbs for security events
|
# Scrub sensitive data out of breadcrumbs.
|
||||||
config.sentry.before_breadcrumb = lambda do |breadcrumb, hint|
|
config.before_breadcrumb = lambda do |breadcrumb, _hint|
|
||||||
# Filter out sensitive breadcrumb data
|
if breadcrumb.data.is_a?(Hash)
|
||||||
if breadcrumb[:data]
|
breadcrumb.data.reject! do |key, value|
|
||||||
breadcrumb[:data].reject! { |key, value|
|
key.to_s.match?(/password|secret|token|key|authorization/i) || value.to_s.match?(/password|secret/i)
|
||||||
key.to_s.match?(/password|secret|token|key|authorization/i) ||
|
end
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
breadcrumb
|
breadcrumb
|
||||||
end
|
end
|
||||||
|
|
||||||
# Only send errors in production unless explicitly enabled
|
|
||||||
config.sentry.enabled = Rails.env.production? || ENV["SENTRY_ENABLED_IN_DEVELOPMENT"] == "true"
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Clinch
|
module Clinch
|
||||||
VERSION = "0.16.0"
|
VERSION = "0.16.2"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :groups
|
resources :groups
|
||||||
get "access", to: "access_checks#new"
|
get "access", to: "access_checks#new"
|
||||||
post "access", to: "access_checks#create"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
|
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ module Admin
|
|||||||
assert_match "alice@example.com", response.body
|
assert_match "alice@example.com", response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create returns 'can access' with via group when user is in an allowed group" do
|
test "returns 'can access' with via group when user is in an allowed group" do
|
||||||
post admin_access_path, params: {
|
get admin_access_path, params: {
|
||||||
user_id: users(:alice).id,
|
user_id: users(:alice).id,
|
||||||
application_id: @kavita.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
|
assert_match "Administrators", response.body # alice is in admin_group; kavita has admin_group
|
||||||
end
|
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)
|
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,
|
user_id: lonely.id,
|
||||||
application_id: @kavita.id
|
application_id: @kavita.id
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ module Admin
|
|||||||
assert_match "shares no group", response.body
|
assert_match "shares no group", response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create renders form unchanged when ids are missing" do
|
test "renders form unchanged when ids are missing" do
|
||||||
post admin_access_path, params: {user_id: "", application_id: ""}
|
get admin_access_path, params: {user_id: "", application_id: ""}
|
||||||
assert_response :success
|
assert_response :success
|
||||||
# No result panel should render. The panel-only phrases:
|
# No result panel should render. The panel-only phrases:
|
||||||
refute_match "Granted via", response.body
|
refute_match "Granted via", response.body
|
||||||
|
|||||||
Reference in New Issue
Block a user