Compare commits

...
Author SHA1 Message Date
raucao 2093f67a64 Merge pull request 'Allow remote access to dev services' (#253) from dev/remote_access into chore/docker-compose-setup
CI / Test (pull_request) Successful in 45s
Reviewed-on: #253
2026-10-06 17:37:49 +00:00
raucao 695977172f Fix Liquor Cabinet performance in dev
CI / Test (pull_request) Failing after 46s
Can't handle enough requests without ThreadPool, so once you hit the
limit, requests take 5s+ (having to wait for the keepalive timeout)
2026-10-06 19:36:26 +02:00
raucao bc37df52a0 Allow remote access to dev services
CI / Test (pull_request) Successful in 46s
Release Drafter / Update release notes draft (pull_request) Successful in 19s
2026-10-06 19:34:24 +02:00
raucao 6279699c11 Switch dev object storage from MinIO to Garage
CI / Test (pull_request) Successful in 43s
MinIO no longer publishes prebuilt community images (quay.io returns
401, Docker Hub 404), so the dev setup cannot pull the image.

Use a single-node Garage instance instead, which auto-creates the
remotestorage bucket and a dev-key1 access key on first start, and
point liquor-cabinet at it.
2026-10-06 13:52:38 +00:00
raucao 0d9c580a0a Automate first-run LDAP and database setup
The documented flow started web (with Solid Queue in Puma) before the
databases existed, and required manually creating the 389ds back-end
and seeding the LDAP directory and databases. Automate it:

- add an ldap-init one-shot service that creates the 389ds back-end if
  it does not exist
- add a web entrypoint that seeds LDAP and the databases on first start,
  and runs db:prepare on later boots
- update README and AGENTS accordingly, including the reset steps
2026-10-06 13:52:38 +00:00
raucao 49c2c9feea Merge pull request 'Gate remoteStorage by serviceEnabled instead of Flipper' (#251) from bugfix/remotestorage-service-enabled into master
CI / Test (push) Failing after 41s
Reviewed-on: #251
2026-10-06 13:52:17 +00:00
raucao 465563d6a6 Merge pull request 'Fix db seeds for Solid Queue' (#249) from fix/seeds-active-job into master
CI / Test (push) Successful in 43s
Reviewed-on: #249
2026-10-06 13:38:20 +00:00
raucao 911d2a5855 Fix db seeds for Solid Queue
CI / Test (pull_request) Successful in 45s
Release Drafter / Update release notes draft (pull_request) Successful in 3s
db/seeds.rb still required "sidekiq/testing" and wrapped seeding in
Sidekiq::Testing.inline!, but Sidekiq is no longer in the bundle after
the Solid Queue migration, so `rails db:setup` failed with a LoadError.

Run the jobs triggered while seeding inline via Active Job instead,
restoring the previous behavior with the adapter the app actually uses.
2026-10-06 13:36:33 +00:00
raucao d8bba02636 Merge pull request 'Stabilize flaky remoteStorage Redis spec' (#252) from bugfix/rs-redis-spec-stabilization into master
CI / Test (push) Failing after 44s
Reviewed-on: #252
2026-10-06 13:36:20 +00:00
raucao 2419982ef5 Stabilize flaky remoteStorage Redis spec
CI / Test (pull_request) Successful in 42s
Release Drafter / Update release notes draft (pull_request) Successful in 2s
"stores a token in redis" asserted an exact count of Redis keys for the
user, but database cleanup only covers SQL and Redis keys accumulate
across examples and spec files. It also relied on a previously-run
example having created the authorization, so it failed depending on test
order and leftover state.

Clear the authorizations:* namespace before each example in the #create
group, create the authorization under test, and assert on its specific
token key instead of a global count.
2026-10-06 15:33:04 +02:00
raucao 8fae099c12 Gate remoteStorage by serviceEnabled instead of Flipper
CI / Test (pull_request) Successful in 2m4s
Release Drafter / Update release notes draft (pull_request) Successful in 2s
The admin "Default services" setting writes the LDAP serviceEnabled
attribute, but remoteStorage access was also gated behind an unused
per-user Flipper flag that nothing ever enabled, so remoteStorage was
inaccessible for everyone.

Gate remoteStorage on service_enabled? like the other services, and make
the admin toggle reflect the LDAP attribute (this also fixes it reading
current_user instead of the user being viewed). E-Mail keeps its Flipper
gate for now.
2026-10-06 15:10:00 +02:00
16 changed files with 195 additions and 66 deletions

No files matched your search

+7 -4
View File
@@ -7,13 +7,16 @@ ejabberd, Discourse, Mastodon, remoteStorage, Nostr, LNDHub, and BTCPay.
## Development environment ## Development environment
Development runs in Docker Compose — run all commands against the `web` container. Development runs in Docker Compose — run all commands against the `web` container.
Start services: `docker compose up` (web, ldap, redis, minio, liquor-cabinet, strfry). Start services: `docker compose up` (web, ldap, redis, garage, liquor-cabinet, strfry).
The `web` service runs `bin/dev` (foreman: Puma + Tailwind CSS watcher) and embeds The `web` service runs `bin/dev` (foreman: Puma + Tailwind CSS watcher) and embeds
Solid Queue workers (`SOLID_QUEUE_IN_PUMA=true`). Solid Queue workers (`SOLID_QUEUE_IN_PUMA=true`).
First-time LDAP setup (after creating the 389ds backend once): First-time LDAP and database setup is automated: the `ldap-init` service
`docker compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be_name="dev"` creates the 389ds back-end, then the `web` entrypoint seeds LDAP and the
then `docker compose run web bin/rails ldap:setup`. databases on first start and runs `db:prepare` on every boot. Manual
equivalents: `docker compose exec ldap dsconf localhost backend create
--suffix="dc=kosmos,dc=org" --be-name="dev"` and `docker compose run --rm web
bin/rails ldap:setup`.
## Common commands (prefix with `docker compose exec web`) ## Common commands (prefix with `docker compose exec web`)
+44 -24
View File
@@ -14,13 +14,14 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
3. Run `docker compose up --build` and wait until all services have started 2. Run `docker compose up --build` and wait until all services have started
(389ds might take an extra minute to be ready). This will take a while when (389ds might take an extra minute to be ready). This will take a while when
running for the first time, so you might want to do something else in the running for the first time, so you might want to do something else in the
meantime. meantime.
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` On the first start, the `ldap-init` service creates the 389ds back-end, and the
6. `docker compose run web rails db:setup` `web` container then seeds the LDAP directory and the databases automatically.
On every start, `web` also applies any pending database migrations.
After these steps, you should have a working Rails app with a handful of test After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000). users running on [http://localhost:3000](http://localhost:3000).
@@ -71,15 +72,12 @@ containers you want to run to the `up` command, like so:
#### LDAP server #### LDAP server
After creating the Docker container for the first time (or after deleting it), On first start, the `ldap-init` service creates the dirsrv back-end
you need to run the following command once, in order to create the dirsrv automatically, and the `web` container then seeds it with development entries.
back-end: To do either step manually (for example, after changing the setup), run:
docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev" docker compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"
docker compose run --rm web bin/rails ldap:setup
Now you can seed the back-end with data using this Rails task:
bundle exec rails ldap:setup
The setup task will first delete any existing entries in the directory tree The setup task will first delete any existing entries in the directory tree
("dc=kosmos,dc=org"), and then create our development entries. ("dc=kosmos,dc=org"), and then create our development entries.
@@ -88,20 +86,42 @@ Note that all 389ds data is stored in the `389ds-data` volume. So if you want
to start over with a fresh installation, delete both that volume as well as the to start over with a fresh installation, delete both that volume as well as the
container. container.
#### Minio / remoteStorage To reset the development environment completely, remove all volumes plus the
generated database files and the first-run marker, then start over:
If you want to run remoteStorage accounts locally, you will have to create the docker compose down -v
respective bucket first. With the `minio` container running (run by default rm -f db/*.sqlite3 tmp/.setup-complete
when using Docker Compose), follow these steps: docker compose up --build
* `docker compose up web redis minio liquor-cabinet` #### Garage / remoteStorage
* Head to http://localhost:9001 and log in with user `minioadmin`, password
`minioadmin` remoteStorage accounts use the `garage` S3-compatible object store. On first
* Create a new bucket called `remotestorage` (or whatever you start, Garage automatically configures a single-node cluster and creates the
change the `S3_BUCKET` config to) `remotestorage` bucket together with a `dev-key1` access key (secret
* Create a new key with ID "dev-key" and secret "123456789" (or whatever you `1234567890123456`), so no manual setup is required.
change `S3_ACCESS_KEY` and `S3_SECRET_KEY` to). Leave the policy field empty,
as it will automatically allow access to the bucket you created. If you want to run remoteStorage accounts locally, the `garage` container is
started by default when using Docker Compose. To run just the remoteStorage
stack:
* `docker compose up web redis garage liquor-cabinet`
The S3 API is available at http://localhost:3900 (region `garage`). If you want
to start over with a fresh storage, delete the `garage-data` volume as well as
the container.
#### Accessing remoteStorage from another machine
remoteStorage clients force HTTPS for any host except `localhost`, and browsers
block plain-HTTP requests to a LAN IP as mixed content. To connect to the dev
remoteStorage from a browser on another machine (including production apps such
as Inspektor), forward the ports over SSH and connect as `localhost`:
ssh -N -L 3000:localhost:3000 -L 4567:localhost:4567 <user>@<dev-host>
Then use `<user>@localhost:3000` as the remoteStorage address in the client.
WeFinger discovery and storage requests are served through the forwarded ports,
so no TLS setup is needed.
### Adding npm modules to use with Stimulus controllers ### Adding npm modules to use with Stimulus controllers
@@ -1,7 +1,7 @@
class Services::RemotestorageController < Services::BaseController class Services::RemotestorageController < Services::BaseController
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_service_available before_action :require_service_available
before_action :require_feature_enabled before_action :require_service_enabled
# Dashboard # Dashboard
def show def show
@@ -17,8 +17,8 @@ class Services::RemotestorageController < Services::BaseController
http_status :not_found unless Setting.remotestorage_enabled? http_status :not_found unless Setting.remotestorage_enabled?
end end
def require_feature_enabled def require_service_enabled
unless Flipper.enabled?(:remotestorage, current_user) unless current_user.service_enabled?(:remotestorage)
http_status :forbidden http_status :forbidden
end end
end end
@@ -1,8 +1,7 @@
class Services::RsAuthsController < Services::BaseController class Services::RsAuthsController < Services::BaseController
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_feature_enabled before_action :require_service_enabled
before_action :require_service_available before_action :require_service_available
# before_action :require_service_enabled
before_action :find_rs_auth, only: [:destroy, :launch_app] before_action :find_rs_auth, only: [:destroy, :launch_app]
def index def index
@@ -23,9 +22,12 @@ class Services::RsAuthsController < Services::BaseController
end end
def launch_app def launch_app
user_address = Rails.env.development? ? user_address =
"#{current_user.cn}@localhost:3000" : if Rails.env.development?
current_user.address "#{current_user.cn}@#{request.host_with_port}"
else
current_user.address
end
launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}" launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}"
@@ -34,8 +36,8 @@ class Services::RsAuthsController < Services::BaseController
private private
def require_feature_enabled def require_service_enabled
unless Flipper.enabled?(:remotestorage, current_user) unless current_user.service_enabled?(:remotestorage)
http_status :forbidden http_status :forbidden
end end
end end
+16 -2
View File
@@ -88,8 +88,14 @@ class WebfingerController < WellKnownController
end end
def remotestorage_link def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain) auth_url =
storage_url = "#{Setting.rs_storage_url}/#{@username}" if Rails.env.development?
new_rs_oauth_url(@username)
else
new_rs_oauth_url(@username, host: Setting.rs_accounts_domain)
end
storage_url = "#{remotestorage_storage_base_url}/#{@username}"
{ {
rel: "http://tools.ietf.org/id/draft-dejong-remotestorage", rel: "http://tools.ietf.org/id/draft-dejong-remotestorage",
@@ -103,4 +109,12 @@ class WebfingerController < WellKnownController
} }
} }
end end
def remotestorage_storage_base_url
return Setting.rs_storage_url unless Rails.env.development?
uri = URI.parse(Setting.rs_storage_url)
uri.host = request.host
uri.to_s
end
end end
+1 -1
View File
@@ -305,7 +305,7 @@
<td>remoteStorage</td> <td>remoteStorage</td>
<td> <td>
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(
enabled: Flipper.enabled?(:remotestorage, current_user) && @services_enabled.include?("remotestorage"), enabled: @services_enabled.include?("remotestorage"),
input_enabled: false input_enabled: false
) %> ) %>
</td> </td>
+1 -2
View File
@@ -43,8 +43,7 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.remotestorage_enabled? && <% if Setting.remotestorage_enabled? %>
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat bg-[length:80%] bg-[center_top_-156px] bg-no-repeat
bg-[url(/img/logos/icon_remotestorage.svg)]"> bg-[url(/img/logos/icon_remotestorage.svg)]">
+1 -2
View File
@@ -25,8 +25,7 @@
active: @settings_section.to_s == "lightning" active: @settings_section.to_s == "lightning"
) %> ) %>
<% end %> <% end %>
<% if Setting.remotestorage_enabled? && <% if Setting.remotestorage_enabled? %>
Flipper.enabled?(:remotestorage, current_user) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage", name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage",
active: @settings_section.to_s == "remotestorage" active: @settings_section.to_s == "remotestorage"
+6 -3
View File
@@ -1,8 +1,9 @@
require 'sidekiq/testing'
ldap = LdapService.new ldap = LdapService.new
Sidekiq::Testing.inline! do original_queue_adapter = ActiveJob::Base.queue_adapter
ActiveJob::Base.queue_adapter = :inline
begin
ldap.delete_all_users! ldap.delete_all_users!
puts "Create user: admin" puts "Create user: admin"
@@ -25,4 +26,6 @@ Sidekiq::Testing.inline! do
password: "user is user", confirmed: true password: "user is user", confirmed: true
}) })
end end
ensure
ActiveJob::Base.queue_adapter = original_queue_adapter
end end
+40 -14
View File
@@ -12,6 +12,20 @@ services:
DS_DM_PASSWORD: passthebutter DS_DM_PASSWORD: passthebutter
SUFFIX_NAME: "dc=kosmos,dc=org" SUFFIX_NAME: "dc=kosmos,dc=org"
ldap-init:
image: 4teamwork/389ds:latest
networks:
- internal_network
volumes:
- ./docker/ldap-init.sh:/ldap-init.sh:ro
environment:
LDAP_ADMIN_PASSWORD: passthebutter
LDAP_SUFFIX: "dc=kosmos,dc=org"
depends_on:
ldap:
condition: service_healthy
command: ["/bin/sh", "/ldap-init.sh"]
redis: redis:
restart: always restart: always
image: redis:7-alpine image: redis:7-alpine
@@ -26,7 +40,7 @@ services:
web: web:
build: . build: .
tty: true tty: true
command: bash -c "rm -f /akkounts/tmp/pids/server.pid; bin/dev" command: ["bash", "docker/web-entrypoint.sh"]
volumes: volumes:
- .:/akkounts - .:/akkounts
- /akkounts/node_modules - /akkounts/node_modules
@@ -57,20 +71,28 @@ services:
NOSTR_PRIVATE_KEY: 7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea NOSTR_PRIVATE_KEY: 7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea
NOSTR_RELAY_URL: ws://strfry:7777 NOSTR_RELAY_URL: ws://strfry:7777
depends_on: depends_on:
- ldap ldap:
- redis condition: service_started
ldap-init:
condition: service_completed_successfully
redis:
condition: service_started
minio: garage:
image: quay.io/minio/minio:latest image: dxflrs/garage:v2.4.1
command: "server /data --console-address ':9001'" command: ["/garage", "server", "--single-node", "--default-bucket"]
networks: networks:
- external_network - external_network
- internal_network - internal_network
ports: ports:
- "9000:9000" - "3900:3900"
- "9001:9001"
volumes: volumes:
- minio-data:/data - ./docker/garage/garage.toml:/etc/garage.toml:ro
- garage-data:/var/lib/garage
environment:
GARAGE_DEFAULT_ACCESS_KEY: dev-key1
GARAGE_DEFAULT_SECRET_KEY: "1234567890123456"
GARAGE_DEFAULT_BUCKET: remotestorage
liquor-cabinet: liquor-cabinet:
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-rc.1 image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-rc.1
@@ -79,17 +101,21 @@ services:
- internal_network - internal_network
ports: ports:
- "4567:4567" - "4567:4567"
volumes:
- ./docker/liquor-cabinet/rainbows.conf.rb:/etc/liquor-cabinet/rainbows.conf.rb:ro
command: ["bundle", "exec", "rainbows", "-c", "/etc/liquor-cabinet/rainbows.conf.rb", "--listen", "0.0.0.0:4567"]
environment: environment:
RACK_ENV: staging RACK_ENV: staging
REDIS_HOST: redis REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_DB: 1 REDIS_DB: 1
S3_ENDPOINT: http://minio:9000 S3_ENDPOINT: http://garage:3900
S3_ACCESS_KEY: dev-key S3_REGION: garage
S3_SECRET_KEY: 123456789 S3_ACCESS_KEY: dev-key1
S3_SECRET_KEY: "1234567890123456"
S3_BUCKET: remotestorage S3_BUCKET: remotestorage
depends_on: depends_on:
- minio - garage
- redis - redis
strfry: strfry:
@@ -127,7 +153,7 @@ networks:
volumes: volumes:
389ds-data: 389ds-data:
driver: local driver: local
minio-data: garage-data:
driver: local driver: local
redis-data: redis-data:
driver: local driver: local
+14
View File
@@ -0,0 +1,14 @@
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -e
SUFFIX="${LDAP_SUFFIX:-dc=kosmos,dc=org}"
URI="ldap://ldap:3389"
if dsconf -D "cn=Directory Manager" -w "$LDAP_ADMIN_PASSWORD" "$URI" \
backend suffix list --suffix 2>/dev/null | grep -Fqx "$SUFFIX"; then
echo "LDAP backend for $SUFFIX already exists, skipping."
else
echo "Creating LDAP backend for $SUFFIX..."
dsconf -D "cn=Directory Manager" -w "$LDAP_ADMIN_PASSWORD" "$URI" \
backend create --suffix "$SUFFIX" --be-name dev
fi
+5
View File
@@ -0,0 +1,5 @@
Rainbows! do
use :ThreadPool
worker_connections 16
client_max_body_size 100 * 1024 * 1024
end
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
if [ ! -f tmp/.setup-complete ]; then
echo "First start: setting up LDAP entries and databases..."
bin/rails ldap:setup
bin/rails db:setup
touch tmp/.setup-complete
else
bin/rails db:prepare
fi
rm -f tmp/pids/server.pid
exec bin/dev
@@ -6,7 +6,9 @@ RSpec.describe Services::RsAuthsController, type: :controller do
before do before do
allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true)
allow_any_instance_of(RemoteStorageAuthorization).to receive(:remove_token_expiry_job).and_return(nil) allow_any_instance_of(RemoteStorageAuthorization).to receive(:remove_token_expiry_job).and_return(nil)
allow_any_instance_of(Flipper).to receive(:enabled?).and_return(true) allow_any_instance_of(LdapService).to receive(:fetch_users).and_return([
{ services_enabled: ["remotestorage"] }
])
end end
describe "GET /services/storage/rs_auths/:id/launch_app" do describe "GET /services/storage/rs_auths/:id/launch_app" do
@@ -35,6 +37,20 @@ RSpec.describe Services::RsAuthsController, type: :controller do
expect(response).to redirect_to(launch_url) expect(response).to redirect_to(launch_url)
end end
end end
context "when remoteStorage is not enabled for the user" do
before do
allow_any_instance_of(LdapService).to receive(:fetch_users).and_return([
{ services_enabled: [] }
])
get :launch_app, params: { id: 1 }
end
it "responds with forbidden" do
expect(response).to have_http_status(:forbidden)
end
end
end end
end end
end end
@@ -11,6 +11,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do
end end
describe "#create" do describe "#create" do
before(:each) { redis_rs_delete_keys("authorizations:*") }
after(:each) { clear_enqueued_jobs } after(:each) { clear_enqueued_jobs }
after(:all) { redis_rs_delete_keys("authorizations:*") } after(:all) { redis_rs_delete_keys("authorizations:*") }
@@ -27,10 +28,9 @@ RSpec.describe RemoteStorageAuthorization, type: :model do
end end
it "stores a token in redis" do it "stores a token in redis" do
user_auth_keys = redis_rs.keys("authorizations:#{user.cn}:*") auth
expect(user_auth_keys.length).to eq(1)
authorizations = redis_rs.smembers(user_auth_keys.first) authorizations = redis_rs.smembers("authorizations:#{user.cn}:#{auth.token}")
expect(authorizations.sort).to eq(%w(documents photos contacts:rw videos:r tasks/work:r).sort) expect(authorizations.sort).to eq(%w(documents photos contacts:rw videos:r tasks/work:r).sort)
end end