asxpio/lib/admin_acl.rb
Sergei Poljanski 4c10221727
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m3s
Build and Deploy to Production / deploy (push) Successful in 40s
admin: publish files to the public bucket
There was no way to put a file up for public use short of running an
S3 client with the uploader key. Add a "files" area to the admin:
upload one or more files, copy the link, edit the note, replace the
contents under the same link, delete. It has its own read/write/delete
permission flags like the other areas, and every action is audited.

The objects go to the storage stack's public bucket and are served by
Garage at s3.asxp.io/public/<key>, not by this app. The app's key can
only put and delete there, so a public_files table is the record of
what was published. Keys get a random prefix: links are not guessable
and names never collide.

That host is same-site with the admin, so a file that can carry script
must not render there. Only a fixed list of types (images, PDF, plain
text, audio, video) is served inline; the rest, HTML and SVG included,
is stored as an attachment. Image metadata is stripped by default, as
for the bingo photos, because of GPS tags.

A per-file limit and a total quota keep the small disk on asxpio-ops
from filling up.

Assisted-by: Claude:fable-5-2
2026-11-12 05:52:10 +04:00

34 lines
1.2 KiB
Ruby

# Which permission an /admin request needs. Every admin path must map to
# an area (or be public/open); unmapped paths are superuser-only, and a
# test fails if any registered route is unmapped.
#
# GET/HEAD → read (except …/new and …/duplicate: write)
# POST …/delete, …/disable → delete
# any other non-GET → write
module AdminACL
RULES = [
[%r{\A/admin/(invoices|gel-rate|crypto-rate)(/|\z)}, 'invoices'],
[%r{\A/admin/bingo(/|\z)}, 'bingo'],
[%r{\A/admin/files(/|\z)}, 'files'],
[%r{\A/admin/(users|audit)(/|\z)}, 'users'],
].freeze
module_function
def area_for(path)
RULES.find { |re, _| path.match?(re) }&.last
end
def perm_for(method, path)
get = %w[GET HEAD].include?(method)
return 'delete' if !get && path.match?(%r{/(delete|disable)\z})
return 'write' if !get || path.match?(%r{/(new|duplicate)\z})
'read'
end
def allowed?(user, method, path)
return true if user.superuser || path.match?(AdminAuth::OPEN)
area = area_for(path) or return false
user.can?(area, perm_for(method, path))
end
end