3 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
7 changed files with 69 additions and 18 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

@@ -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

View File

@@ -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" %>

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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