diff --git a/.env.example b/.env.example index b7244d8..390fe6a 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,14 @@ SMTP_DOMAIN=example.com SMTP_AUTH_METHOD=plain SMTP_ENABLE_STARTTLS=auto +# S3_ENABLED=true +# S3_ENDPOINT=https://s3.kosmos.org +# S3_REGION=garage +# S3_BUCKET=akkounts-production +# S3_ALIAS_HOST=accounts.s3.kosmos.org +# S3_ACCESS_KEY=123456abcdefg +# S3_SECRET_KEY=123456789123456789123456789 + LDAP_HOST=localhost LDAP_PORT=389 LDAP_ADMIN_PASSWORD=passthebutter diff --git a/Gemfile b/Gemfile index b169662..df0e515 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,8 @@ gem 'flipper-ui' # HTTP requests gem 'faraday' +gem 'down' +gem 'aws-sdk-s3', require: false # Background/scheduled jobs gem 'sidekiq', '< 7' @@ -59,6 +61,7 @@ gem "sentry-rails" # Services gem 'discourse_api' gem "lnurl" +gem 'manifique', git: 'https://gitea.kosmos.org/5apps/manifique.git', branch: 'master' gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat' group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index b869deb..3e551f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,13 @@ +GIT + remote: https://gitea.kosmos.org/5apps/manifique.git + revision: 8d79113438ee7c3e4288f840a135622519cffd5c + branch: master + specs: + manifique (0.1.0) + faraday (~> 2.7.11) + faraday-follow_redirects (= 0.3.0) + nokogiri (~> 1.15.4) + GIT remote: https://gitea.kosmos.org/kosmos/nostr-gem.git revision: 596529d9eb50d13b3f385245636698fccf37b442 @@ -14,82 +24,100 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + actioncable (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actionmailbox (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.5) - actionpack (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activesupport (= 7.0.5) + actionmailer (7.0.8) + actionpack (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activesupport (= 7.0.8) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.5) - actionview (= 7.0.5) - activesupport (= 7.0.5) + actionpack (7.0.8) + actionview (= 7.0.8) + activesupport (= 7.0.8) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.5) - actionpack (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actiontext (7.0.8) + actionpack (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.5) - activesupport (= 7.0.5) + actionview (7.0.8) + activesupport (= 7.0.8) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.5) - activesupport (= 7.0.5) + activejob (7.0.8) + activesupport (= 7.0.8) globalid (>= 0.3.6) - activemodel (7.0.5) - activesupport (= 7.0.5) - activerecord (7.0.5) - activemodel (= 7.0.5) - activesupport (= 7.0.5) - activestorage (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activesupport (= 7.0.5) + activemodel (7.0.8) + activesupport (= 7.0.8) + activerecord (7.0.8) + activemodel (= 7.0.8) + activesupport (= 7.0.8) + activestorage (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activesupport (= 7.0.8) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.5) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.4) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + aws-eventstream (1.2.0) + aws-partitions (1.839.0) + aws-sdk-core (3.185.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.136.0) + aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) + aws-eventstream (~> 1, >= 1.0.2) backport (1.2.0) - bcrypt (3.1.18) - bech32 (1.3.0) + base64 (0.1.1) + bcrypt (3.1.19) + bech32 (1.4.2) thor (>= 1.1.0) benchmark (0.2.1) bindex (0.8.1) bip-schnorr (0.6.0) ecdsa_ext (~> 0.5.0) + brow (0.4.1) builder (3.2.4) byebug (11.1.3) capybara (3.39.2) @@ -107,7 +135,7 @@ GEM crack (0.4.5) rexml crass (1.0.6) - cssbundling-rails (1.1.2) + cssbundling-rails (1.3.3) railties (>= 6.0.0) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) @@ -116,7 +144,7 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.3) - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -135,6 +163,8 @@ GEM dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) + down (5.4.1) + addressable (~> 2.8) e2mmap (0.1.0) ecdsa (1.2.0) ecdsa_ext (0.5.0) @@ -149,9 +179,10 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (3.2.0) + faker (3.2.1) i18n (>= 1.8.11, < 2) - faraday (2.7.6) + faraday (2.7.11) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-follow_redirects (0.3.0) @@ -159,44 +190,47 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) - faye-websocket (0.11.2) + faye-websocket (0.11.3) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.15.5) - flipper (0.28.0) + ffi (1.16.3) + flipper (1.0.0) + brow (~> 0.4.1) concurrent-ruby (< 2) - flipper-active_record (0.28.0) + flipper-active_record (1.0.0) activerecord (>= 4.2, < 8) - flipper (~> 0.28.0) - flipper-ui (0.28.0) + flipper (~> 1.0.0) + flipper-ui (1.0.0) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 0.28.0) - rack (>= 1.4, < 3) + flipper (~> 1.0.0) + rack (>= 1.4, < 4) rack-protection (>= 1.5.3, <= 4.0.0) sanitize (< 7) fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) hashdiff (1.0.1) i18n (1.14.1) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - importmap-rails (1.1.6) + importmap-rails (1.2.1) actionpack (>= 6.0.0) railties (>= 6.0.0) jaro_winkler (1.5.6) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.6.2) json (2.6.3) kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -209,10 +243,10 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lnurl (1.0.1) + lnurl (1.1.0) bech32 (~> 1.1) - lockbox (1.2.0) - loofah (2.21.3) + lockbox (1.3.0) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -224,10 +258,10 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_magick (4.12.0) - mini_mime (1.1.2) - minitest (5.18.0) + mini_mime (1.1.5) + minitest (5.20.0) multipart-post (2.3.0) - net-imap (0.3.6) + net-imap (0.3.7) date net-protocol net-ldap (0.18.0) @@ -235,50 +269,51 @@ GEM net-protocol net-protocol (0.2.1) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol nio4r (2.5.9) - nokogiri (1.15.2-arm64-darwin) + nokogiri (1.15.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-linux) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - pagy (6.0.4) + pagy (6.1.0) parallel (1.23.0) - parser (3.2.2.3) + parser (3.2.2.4) ast (~> 2.4.1) racc pg (1.2.3) - public_suffix (5.0.1) + public_suffix (5.0.3) puma (4.3.12) nio4r (~> 2.0) raabro (1.4.0) racc (1.7.1) - rack (2.2.7) - rack-protection (3.0.6) - rack + rack (2.2.8) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.5) - actioncable (= 7.0.5) - actionmailbox (= 7.0.5) - actionmailer (= 7.0.5) - actionpack (= 7.0.5) - actiontext (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activemodel (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + rails (7.0.8) + actioncable (= 7.0.8) + actionmailbox (= 7.0.8) + actionmailer (= 7.0.8) + actionpack (= 7.0.8) + actiontext (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activemodel (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) bundler (>= 1.15.0) - railties (= 7.0.5) + railties (= 7.0.8) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) @@ -286,9 +321,9 @@ GEM rails-settings-cached (2.8.3) activerecord (>= 5.0.0) railties (>= 5.0.0) - railties (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + railties (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) method_source rake (>= 12.2) thor (~> 1.0) @@ -300,13 +335,13 @@ GEM ffi (~> 1.0) rbs (2.8.4) redis (4.8.1) - regexp_parser (2.8.1) - responders (3.1.0) + regexp_parser (2.8.2) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) reverse_markdown (2.1.1) nokogiri - rexml (3.2.5) + rexml (3.2.6) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -316,7 +351,7 @@ GEM rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.3) @@ -327,34 +362,36 @@ GEM rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rspec-support (~> 3.12) - rspec-support (3.12.0) - rubocop (1.52.1) + rspec-support (3.12.1) + rubocop (1.57.1) + base64 (~> 0.1.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) - ruby-vips (2.1.4) + ruby-vips (2.2.0) ffi (~> 1.12) ruby2_keywords (0.0.5) rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) - sanitize (6.0.1) + sanitize (6.1.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - sentry-rails (5.9.0) + sentry-rails (5.12.0) railties (>= 5.0) - sentry-ruby (~> 5.9.0) - sentry-ruby (5.9.0) + sentry-ruby (~> 5.12.0) + sentry-ruby (5.12.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (6.5.9) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -378,55 +415,57 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - sprockets (4.2.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sqlite3 (1.6.3-arm64-darwin) - sqlite3 (1.6.3-x86_64-linux) - stimulus-rails (1.2.1) + sqlite3 (1.6.7-arm64-darwin) + sqlite3 (1.6.7-x86_64-linux) + stimulus-rails (1.3.0) railties (>= 6.0.0) - thor (1.2.2) - tilt (2.2.0) - timeout (0.3.2) - turbo-rails (1.4.0) + thor (1.3.0) + tilt (2.3.0) + timeout (0.4.0) + turbo-rails (1.5.0) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) - view_component (3.2.0) + unicode-display_width (2.5.0) + view_component (3.6.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.18.1) + webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.34) - zeitwerk (2.6.8) + zeitwerk (2.6.12) PLATFORMS arm64-darwin-22 + ruby x86_64-linux DEPENDENCIES + aws-sdk-s3 byebug (~> 11.1) capybara cssbundling-rails @@ -435,6 +474,7 @@ DEPENDENCIES devise_ldap_authenticatable discourse_api dotenv-rails + down factory_bot_rails faker faraday @@ -449,6 +489,7 @@ DEPENDENCIES listen (~> 3.2) lnurl lockbox + manifique! net-ldap nostr! pagy (~> 6.0, >= 6.0.2) diff --git a/README.md b/README.md index 9bd72ea..564b833 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,20 @@ The setup task will first delete any existing entries in the directory tree Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over with a fresh installation, delete both that directory as well as the container. +#### Minio / RS + +If you want to run remoteStorage accounts locally, you will have to create the +respective bucket first: + +* `docker compose up web redis minio liquor-cabinet` +* Head to http://localhost:9001 and log in with user `minioadmin`, password + `minioadmin` +* Create a new bucket called `remotestorage` (or whatever you + change the `S3_BUCKET` config to) +* Create a new key with ID "dev-key" and secret "123456789" (or whatever you + 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. + ### Adding npm modules to use with Stimulus controllers The following command downloads the specified npm module to `vendor/javascript` diff --git a/app/components/dropdown_component.html.erb b/app/components/dropdown_component.html.erb new file mode 100644 index 0000000..3ea3bce --- /dev/null +++ b/app/components/dropdown_component.html.erb @@ -0,0 +1,26 @@ +
+
+
+ + + <%= render partial: "icons/kebab-menu", locals: { + custom_class: "inline text-gray-500 h-6 w-6" + } %> + + +
+ +
+
diff --git a/app/components/dropdown_component.rb b/app/components/dropdown_component.rb new file mode 100644 index 0000000..2b76fff --- /dev/null +++ b/app/components/dropdown_component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DropdownComponent < ViewComponent::Base + +end diff --git a/app/components/dropdown_link_component.html.erb b/app/components/dropdown_link_component.html.erb new file mode 100644 index 0000000..eb15ffc --- /dev/null +++ b/app/components/dropdown_link_component.html.erb @@ -0,0 +1,6 @@ +<%= link_to @href, class: @class, data: { + 'dropdown-target': "menuItem", + 'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent" + } do %> + <%= content %> +<% end %> diff --git a/app/components/dropdown_link_component.rb b/app/components/dropdown_link_component.rb new file mode 100644 index 0000000..4eabc8e --- /dev/null +++ b/app/components/dropdown_link_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class DropdownLinkComponent < ViewComponent::Base + def initialize(href:, separator: false, add_class: nil) + @href = href + @class = class_str(separator, add_class) + end + + private + + def class_str(separator, add_class) + str = "no-underline block px-5 py-3 text-sm text-gray-900 bg-white + hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap" + str = "#{str} border-t" if separator + str = "#{str} #{add_class}" if add_class + str + end +end diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb new file mode 100644 index 0000000..8a40970 --- /dev/null +++ b/app/components/rs_auth_component.html.erb @@ -0,0 +1,26 @@ +
+
+ <%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %> +
+
+

+ <%= @web_app.name %> +

+

+ <%= @auth.client_id %> +

+
+ <%= render DropdownComponent.new do %> + <%= render DropdownLinkComponent.new( + href: launch_app_services_storage_rs_auth_url(@auth) + ) do %> + Launch app + <% end %> + <%= render DropdownLinkComponent.new( + href: revoke_services_storage_rs_auth_url(@auth), + separator: true, add_class: "text-red-700" + ) do %> + Revoke access + <% end %> + <% end %> +
diff --git a/app/components/rs_auth_component.rb b/app/components/rs_auth_component.rb new file mode 100644 index 0000000..ed588bf --- /dev/null +++ b/app/components/rs_auth_component.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class RsAuthComponent < ViewComponent::Base + def initialize(auth:) + @auth = auth + @web_app = auth.web_app + end +end diff --git a/app/controllers/admin/app_catalog/web_apps_controller.rb b/app/controllers/admin/app_catalog/web_apps_controller.rb new file mode 100644 index 0000000..052d06a --- /dev/null +++ b/app/controllers/admin/app_catalog/web_apps_controller.rb @@ -0,0 +1,9 @@ +class Admin::AppCatalog::WebAppsController < Admin::AppCatalogController + def index + @pagy, @web_apps = pagy(AppCatalog::WebApp.order('created_at desc')) + + @stats = { + known_apps: AppCatalog::WebApp.count + } + end +end diff --git a/app/controllers/admin/app_catalog_controller.rb b/app/controllers/admin/app_catalog_controller.rb new file mode 100644 index 0000000..507ea04 --- /dev/null +++ b/app/controllers/admin/app_catalog_controller.rb @@ -0,0 +1,9 @@ +class Admin::AppCatalogController < Admin::BaseController + before_action :set_current_section + + private + + def set_current_section + @current_section = :app_catalog + end +end diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb index 67a2beb..2e2933f 100644 --- a/app/controllers/rs/oauth_controller.rb +++ b/app/controllers/rs/oauth_controller.rb @@ -3,8 +3,7 @@ class Rs::OauthController < ApplicationController before_action :authenticate_user!, only: :create def new - username, org = params[:useraddress].split("@") - @user = User.where(cn: username.downcase, ou: org).first + @user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first @scopes = parse_scopes params[:scope] @redirect_uri = params[:redirect_uri] @client_id = params[:client_id] @@ -22,7 +21,7 @@ class Rs::OauthController < ApplicationController unless current_user == @user sign_out :user - redirect_to new_rs_oauth_url(@user.address, + redirect_to new_rs_oauth_url(@user.cn, scope: params[:scope], redirect_uri: params[:redirect_uri], client_id: params[:client_id], @@ -88,7 +87,7 @@ class Rs::OauthController < ApplicationController permissions: permissions, client_id: client_id, redirect_uri: redirect_uri, - app_name: client_id, #TODO use user-defined name + app_name: client_id, expire_at: expire_at ) @@ -96,29 +95,15 @@ class Rs::OauthController < ApplicationController allow_other_host: true end - # GET /rs/oauth/token/:id/launch_app - def launch_app - auth = current_user.remote_storage_authorizations.find(params[:id]) - - redirect_to app_auth_url(auth), allow_other_host: true - end - private def require_signed_in_with_username unless user_signed_in? - username, org = params[:useraddress].split("@") session[:user_return_to] = request.url - redirect_to new_user_session_path(cn: username, ou: org) + redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain) end end - def app_auth_url(auth) - url = "#{auth.url}#remotestorage=#{current_user.address}" - url += "&access_token=#{auth.token}" - url - end - def hostname_of(uri) uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0] end diff --git a/app/controllers/services/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb index 5d455ba..67c7e76 100644 --- a/app/controllers/services/remotestorage_controller.rb +++ b/app/controllers/services/remotestorage_controller.rb @@ -3,10 +3,13 @@ class Services::RemotestorageController < Services::BaseController before_action :require_feature_enabled before_action :require_service_available - def dashboard + # Dashboard + def show # unless current_user.services_enabled.include?(:remotestorage) # redirect_to service_remotestorage_info_path # end + @rs_auths = current_user.remote_storage_authorizations + # TODO sort by app name end private diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb new file mode 100644 index 0000000..e31f046 --- /dev/null +++ b/app/controllers/services/rs_auths_controller.rb @@ -0,0 +1,42 @@ +class Services::RsAuthsController < Services::BaseController + before_action :authenticate_user! + before_action :require_feature_enabled + before_action :require_service_available + # before_action :require_service_enabled + before_action :find_rs_auth + + def destroy + @auth.destroy! + + respond_to do |format| + format.html do redirect_to services_storage_url, flash: { + success: 'App authorization revoked' + } + end + format.json { head :no_content } + end + end + + def launch_app + launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}" + + redirect_to launch_url, allow_other_host: true + end + + private + + def require_feature_enabled + unless Flipper.enabled?(:remotestorage, current_user) + http_status :forbidden + end + end + + def require_service_available + http_status :not_found unless Setting.remotestorage_enabled? + end + + def find_rs_auth + @auth = current_user.remote_storage_authorizations.find(params[:id]) + http_status :not_found unless @auth.present? + end +end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index d4efea5..8f30419 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -110,7 +110,9 @@ class SettingsController < ApplicationController def set_settings_section @settings_section = params[:section] - allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments] + allowed_sections = [ + :profile, :account, :lightning, :remotestorage, :xmpp, :experiments + ] unless allowed_sections.include?(@settings_section.to_sym) redirect_to setting_path(:profile) @@ -124,6 +126,7 @@ class SettingsController < ApplicationController def user_params params.require(:user).permit(:display_name, :avatar, preferences: [ :lightning_notify_sats_received, + :remotestorage_notify_auth_created, :xmpp_exchange_contacts_with_invitees ]) end diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb index 5cf4012..be50ebd 100644 --- a/app/controllers/webfinger_controller.rb +++ b/app/controllers/webfinger_controller.rb @@ -6,15 +6,19 @@ class WebfingerController < ApplicationController def show resource = params[:resource] - if resource && resource.match(/acct:\w+/) - useraddress = resource.split(":").last - username, org = useraddress.split("@") - username.downcase! - unless User.where(cn: username, ou: org).any? + if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1) + @username, @org = @useraddress.split("@") + + unless Rails.env.development? + # Allow different domains (e.g. localhost:3000) in development only + head 404 and return unless @org == Setting.primary_domain + end + + unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any? head 404 and return end - render json: webfinger(useraddress).to_json, + render json: webfinger.to_json, content_type: "application/jrd+json" else head 422 and return @@ -23,19 +27,18 @@ class WebfingerController < ApplicationController private - def webfinger(useraddress) + def webfinger links = []; - links << remotestorage_link(useraddress) if Setting.remotestorage_enabled + # TODO check if storage service is enabled for user, not just globally + links << remotestorage_link if Setting.remotestorage_enabled { "links" => links } end - def remotestorage_link(useraddress) - # TODO use when OAuth routes are available - # auth_url = new_rs_oauth_url(useraddress) - auth_url = "https://example.com/rs/oauth" - storage_url = "#{Setting.rs_storage_url}/#{useraddress}" + def remotestorage_link + auth_url = new_rs_oauth_url(@username) + storage_url = "#{Setting.rs_storage_url}/#{@username}" { "rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage", diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js index f93adf1..6958c14 100644 --- a/app/javascript/controllers/application.js +++ b/app/javascript/controllers/application.js @@ -1,8 +1,9 @@ import { Application } from "@hotwired/stimulus" -import { Modal, Tabs } from "tailwindcss-stimulus-components" +import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components" const application = Application.start() +application.register('dropdown', Dropdown) application.register('modal', Modal) application.register('tabs', Tabs) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 84f7dd5..81132b9 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -5,4 +5,16 @@ class NotificationMailer < ApplicationMailer @subject = "Sats received" mail to: @user.email, subject: @subject end + + def remotestorage_auth_created + @user = params[:user] + @auth = params[:auth] + @permissions = @auth.permissions.map do |p| + access = p.split(":")[1] == 'r' ? 'read' : 'read/write' + directory = p.split(':')[0] == '' ? 'all folders and files' : p.split(':')[0] + "#{access} #{directory}" + end + @subject = "New app connected to your storage" + mail to: @user.email, subject: @subject + end end diff --git a/app/models/app_catalog.rb b/app/models/app_catalog.rb new file mode 100644 index 0000000..e6cd4c5 --- /dev/null +++ b/app/models/app_catalog.rb @@ -0,0 +1,5 @@ +module AppCatalog + def self.table_name_prefix + "app_catalog_" + end +end diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb new file mode 100644 index 0000000..b42bea7 --- /dev/null +++ b/app/models/app_catalog/web_app.rb @@ -0,0 +1,16 @@ +class AppCatalog::WebApp < ApplicationRecord + store :metadata, coder: JSON + + has_many :remote_storage_authorizations + + has_one_attached :icon + has_one_attached :apple_touch_icon + + validates :url, presence: true, uniqueness: true + validates :url, format: { with: URI.regexp }, + if: Proc.new { |a| a.url.present? } + + def update_metadata + AppCatalogManager::UpdateMetadata.call(self) + end +end diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 82ac744..2e574d5 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -1,5 +1,6 @@ class RemoteStorageAuthorization < ApplicationRecord belongs_to :user + belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true serialize :permissions @@ -15,22 +16,36 @@ class RemoteStorageAuthorization < ApplicationRecord before_create :generate_token before_create :store_token_in_redis + before_create :find_or_create_web_app after_create :schedule_token_expiry + after_create :notify_user before_destroy :delete_token_from_redis after_destroy :remove_token_expiry_job def url - if self.redirect_uri - uri = URI.parse self.redirect_uri - "#{uri.scheme}://#{client_id}" + uri = URI.parse self.redirect_uri + "#{uri.scheme}://#{client_id}" + end + + def launch_url + return url unless web_app && web_app.metadata[:start_url].present? + + start_url = web_app.metadata[:start_url] + + if start_url.match("^https?:\/\/") + return start_url.start_with?(url) ? start_url : url else - "http://#{client_id}" + path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "") + "#{url}/#{path}" end end def delete_token_from_redis - key = "rs:authorizations:#{user.address}:#{token}" + key = "authorizations:#{user.cn}:#{token}" redis.srem? key, redis.smembers(key) + rescue => e + Rails.logger.error e + Sentry.capture_exception(e) if Setting.sentry_enabled? end private @@ -44,7 +59,7 @@ class RemoteStorageAuthorization < ApplicationRecord end def store_token_in_redis - redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions + redis.sadd "authorizations:#{user.cn}:#{token}", permissions end def schedule_token_expiry @@ -60,4 +75,40 @@ class RemoteStorageAuthorization < ApplicationRecord job.delete if job.display_args == [id] end end + + def find_or_create_web_app + if looks_like_hosted_origin? + web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url) + web_app.update_metadata unless web_app.name.present? + self.web_app = web_app + self.app_name = web_app.name.presence || client_id + else + self.app_name = client_id + end + end + + def looks_like_hosted_origin? + uri = URI.parse self.redirect_uri + !!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/) + rescue URI::InvalidURIError + false + end + + def notify_user + notify = user.preferences[:remotestorage_notify_auth_created] + + case notify + when "xmpp" + router = Router.new + payload = { + type: "normal", to: user.address, + from: Setting.xmpp_notifications_from_address, + body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}" + } + XmppSendMessageJob.perform_later(payload) + when "email" + NotificationMailer.with(user: user, auth: self) + .remotestorage_auth_created.deliver_later + end + end end diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb new file mode 100644 index 0000000..4530ae3 --- /dev/null +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -0,0 +1,52 @@ +require "manifique" +require "down" + +module AppCatalogManager + class UpdateMetadata < AppCatalogManagerService + def initialize(app) + @app = app + end + + def call + agent = Manifique::Agent.new(url: @app.url) + metadata = agent.fetch_metadata + + @app.name = metadata.name + + [:name, :short_name, :description, :theme_color, :background_color, + :display, :start_url, :scope, :share_target, :icons].each do |prop| + @app.metadata[prop] = metadata.send(prop) if prop + end + + if icon = metadata.select_icon(sizes: "256x256") || + icon = metadata.select_icon(sizes: "192x192") + attach_remote_image(:icon, icon) + # TODO elsif get whatever is available + end + + if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") + attach_remote_image(:apple_touch_icon, apple_touch_icon) + end + + @app.save! + rescue Manifique::Error => e + msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" + Rails.logger.warn(msg) + Sentry.capture_message(msg) if Setting.sentry_enabled? + false + end + + def attach_remote_image(attachment_name, icon) + if icon['src'].start_with?("http") + download_url = icon['src'] + else + download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" + end + filename = "#{attachment_name}.png" + key = "web_apps/#{@app.id}/icons/#{attachment_name}.png" + + tempfile = Down.download(download_url) + @app.send(attachment_name).attach(key: key, io: tempfile, filename: filename) + end + end +end diff --git a/app/services/app_catalog_manager_service.rb b/app/services/app_catalog_manager_service.rb new file mode 100644 index 0000000..f513907 --- /dev/null +++ b/app/services/app_catalog_manager_service.rb @@ -0,0 +1,2 @@ +class AppCatalogManagerService < ApplicationService +end diff --git a/app/services/router.rb b/app/services/router.rb new file mode 100644 index 0000000..a7536ff --- /dev/null +++ b/app/services/router.rb @@ -0,0 +1,7 @@ +class Router + include Rails.application.routes.url_helpers + + def self.default_url_options + ActionMailer::Base.default_url_options + end +end diff --git a/app/views/admin/app_catalog/web_apps/index.html.erb b/app/views/admin/app_catalog/web_apps/index.html.erb new file mode 100644 index 0000000..ebfa289 --- /dev/null +++ b/app/views/admin/app_catalog/web_apps/index.html.erb @@ -0,0 +1,56 @@ +<%= render HeaderComponent.new(title: "App Catalog") %> + +<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %> +
+ <%= render QuickstatsContainerComponent.new do %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Known Web Apps', + value: @stats[:known_apps], + ) %> + <%# <%= render QuickstatsItemComponent.new( + <%# type: :number, + <%# title: 'Accepted', + <%# value: @stats[:accepted], + <%# ) %> + <%# <%= render QuickstatsItemComponent.new( + <%# type: :number, + <%# title: 'Users with referrals', + <%# value: @stats[:users_with_referrals], + <%# meta: "/ #{User.count}" + <%# ) %> + <% end %> +
+ <% if @web_apps.any? %> +
+

Web Apps

+ + + + + + + + + + + <% @web_apps.each do |web_app| %> + + + + + + + <% end %> + +
NameURL
<%= web_app.name %><%= link_to web_app.url, web_app.url, + target: "_blank", rel: "nofollow noopener", + class: "ks-text-link" %>
+ <%== pagy_nav @pagy %> +
+ <% end %> +<% end %> diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 7f9ba87..05f8f11 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -63,10 +63,12 @@
+ <% if @avatar.present? %>

LDAP

+ <% end %>

diff --git a/app/views/icons/_globe.html.erb b/app/views/icons/_globe.html.erb index 0a0586d..62237bf 100644 --- a/app/views/icons/_globe.html.erb +++ b/app/views/icons/_globe.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/icons/_kebab-menu.html.erb b/app/views/icons/_kebab-menu.html.erb new file mode 100644 index 0000000..ac1ee3a --- /dev/null +++ b/app/views/icons/_kebab-menu.html.erb @@ -0,0 +1,10 @@ + + + Menu + + + + + + + diff --git a/app/views/icons/_remotestorage.html.erb b/app/views/icons/_remotestorage.html.erb new file mode 100644 index 0000000..2daafc4 --- /dev/null +++ b/app/views/icons/_remotestorage.html.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/icons/_star.html.erb b/app/views/icons/_star.html.erb index bcdc31a..5734060 100644 --- a/app/views/icons/_star.html.erb +++ b/app/views/icons/_star.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/notification_mailer/remotestorage_auth_created.text.erb b/app/views/notification_mailer/remotestorage_auth_created.text.erb new file mode 100644 index 0000000..61338f3 --- /dev/null +++ b/app/views/notification_mailer/remotestorage_auth_created.text.erb @@ -0,0 +1,23 @@ +Hi <%= @user.display_name.presence || @user.cn %>, + +You have just granted '<%= @auth.client_id %>' access to your Kosmos Storage, with the following permissions: + +<% @permissions.each do |p| %> +* <%= p %> +<% end %> + +Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: + +<%= services_storage_url %> + +Have fun! + +--- + +You can disable email notifications for new app authorizations in your account settings: +<%= setting_url(:remotestorage) %> +<% if Setting.discourse_enabled %> + +If you have any questions, please visit our community forums: +<%= Setting.discourse_public_url %> +<% end %> diff --git a/app/views/services/chat/show.html.erb b/app/views/services/chat/show.html.erb index 5f65971..7e749b1 100644 --- a/app/views/services/chat/show.html.erb +++ b/app/views/services/chat/show.html.erb @@ -38,7 +38,7 @@

Chat Apps

Use your account with many different apps, and on any devices you wish! - When opening an app for the first time, just enter your user address and + When opening an app for the first time, just enter your address and password to log in.

diff --git a/app/views/services/remotestorage/dashboard.html.erb b/app/views/services/remotestorage/dashboard.html.erb deleted file mode 100644 index f6b0932..0000000 --- a/app/views/services/remotestorage/dashboard.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%= render HeaderComponent.new(title: "Storage") %> - -<%= render MainSimpleComponent.new do %> -
-

Feature enabled

-
-<% end %> diff --git a/app/views/services/remotestorage/show.html.erb b/app/views/services/remotestorage/show.html.erb new file mode 100644 index 0000000..58b7ed7 --- /dev/null +++ b/app/views/services/remotestorage/show.html.erb @@ -0,0 +1,16 @@ +<%= render HeaderComponent.new(title: "Storage") %> + +<%= render MainSimpleComponent.new do %> +
+

Connected Apps

+ <% if @rs_auths.any? %> +
+ <% @rs_auths.each do |auth| %> + <%= render RsAuthComponent.new(auth: auth) %> + <% end %> +
+ <% else %> +

No apps connected yet.

+ <% end %> +
+<% end %> diff --git a/app/views/settings/_remotestorage.html.erb b/app/views/settings/_remotestorage.html.erb new file mode 100644 index 0000000..dc93b8e --- /dev/null +++ b/app/views/settings/_remotestorage.html.erb @@ -0,0 +1,25 @@ +<%= form_for @user, url: setting_path(:remotestorage), html: { :method => :put } do |f| %> +
+

Notifications

+ +
+
+

+ <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> +

+
+<% end %> diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb index 7762431..1ba5eb3 100644 --- a/app/views/shared/_admin_nav.html.erb +++ b/app/views/shared/_admin_nav.html.erb @@ -10,5 +10,9 @@ <%= link_to "Lightning", admin_lightning_path, class: main_nav_class(@current_section, :lightning) %> <% end %> +<% if Setting.remotestorage_enabled? %> + <%= link_to "Apps", admin_app_catalog_web_apps_path, + class: main_nav_class(@current_section, :app_catalog) %> +<% end %> <%= link_to "Settings", admin_settings_registrations_path, class: main_nav_class(@current_section, :settings) %> diff --git a/app/views/shared/_admin_sidenav_app_catalog.html.erb b/app/views/shared/_admin_sidenav_app_catalog.html.erb new file mode 100644 index 0000000..6fa14bc --- /dev/null +++ b/app/views/shared/_admin_sidenav_app_catalog.html.erb @@ -0,0 +1,10 @@ +<%= render SidenavLinkComponent.new( + name: "Web Apps", path: admin_app_catalog_web_apps_path, icon: "globe", + active: current_page?(admin_app_catalog_web_apps_path) +) %> +<%= render SidenavLinkComponent.new( + name: "Recommended Apps", path: "#", icon: "star", disabled: true +) %> +<%= render SidenavLinkComponent.new( + name: "OAuth Apps", path: "#", icon: "key", disabled: true +) %> diff --git a/app/views/shared/_sidenav_settings.html.erb b/app/views/shared/_sidenav_settings.html.erb index aa30f60..359f3e1 100644 --- a/app/views/shared/_sidenav_settings.html.erb +++ b/app/views/shared/_sidenav_settings.html.erb @@ -18,6 +18,12 @@ active: @settings_section.to_s == "lightning" ) %> <% end %> +<% if Setting.remotestorage_enabled %> +<%= render SidenavLinkComponent.new( + name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage", + active: @settings_section.to_s == "remotestorage" +) %> +<% end %> <% if Setting.nostr_enabled %> <%= render SidenavLinkComponent.new( name: "Experiments", path: setting_path(:experiments), icon: "science", diff --git a/config/default_preferences.yml b/config/default_preferences.yml index ff7f051..c65d658 100644 --- a/config/default_preferences.yml +++ b/config/default_preferences.yml @@ -1,2 +1,3 @@ lightning_notify_sats_received: disabled # or xmpp, email +remotestorage_notify_auth_created: email # or xmpp, email xmpp_exchange_contacts_with_invitees: true diff --git a/config/environments/development.rb b/config/environments/development.rb index eecc942..1f96c8f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -71,6 +71,9 @@ Rails.application.configure do # Allow requests from any IP config.web_console.whiny_requests = false - # Store attachments on the local disk (in ./storage) - config.active_storage.service = :local + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + config.active_storage.service = :local + end end diff --git a/config/environments/production.rb b/config/environments/production.rb index bfcfecb..5adf41e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -110,9 +110,11 @@ Rails.application.configure do # Set this to true and configure the email server for immediate delivery to raise delivery errors. config.action_mailer.raise_delivery_errors = true - # TODO make configurable - # Store attachments in S3-compatible back-end - config.active_storage.service = :local + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + config.active_storage.service = :local + end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). diff --git a/config/environments/test.rb b/config/environments/test.rb index 739a1ac..7d731e9 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -52,6 +52,10 @@ Rails.application.configure do config.active_job.queue_adapter = :test - # Store attachments on the local disk (in ./tmp) - config.active_storage.service = :test + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + # Store attachments on the local disk (in ./tmp) + config.active_storage.service = :test + end end diff --git a/config/routes.rb b/config/routes.rb index e7fef10..c9f4281 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,10 +19,10 @@ Rails.application.routes.draw do resources :invitations, only: ['index', 'show', 'create', 'destroy'] namespace :services do - get 'storage', to: 'remotestorage#dashboard' - resource :chat, only: [:show], controller: 'chat' + resource :mastodon, only: [:show], controller: 'mastodon' + resources :lightning, only: [:index] do collection do get 'transactions' @@ -30,7 +30,14 @@ Rails.application.routes.draw do end end - resource :mastodon, only: [:show], controller: 'mastodon' + resource :storage, controller: 'remotestorage', only: [:show] do + resources :rs_auths, only: [:destroy] do + member do + get :revoke, to: 'rs_auths#destroy' + get :launch_app + end + end + end end resources :settings, param: 'section', only: ['index', 'show', 'update'] do @@ -60,11 +67,16 @@ Rails.application.routes.draw do namespace :admin do root to: 'dashboard#index' + resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ } get 'invitations', to: 'invitations#index' resources :donations get 'lightning', to: 'lightning#index' + namespace :app_catalog do + resources 'web_apps', only: ['index'] + end + namespace :settings do resources 'registrations', only: ['index', 'create'] resources 'services', only: ['index', 'create'] @@ -73,9 +85,8 @@ Rails.application.routes.draw do namespace :rs do resource :oauth, only: [:new, :create], path_names: { - new: ':useraddress', create: ':useraddress' - }, controller: 'oauth', constraints: { useraddress: /[^\/]+/} - get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app + new: ':username', create: ':username' + }, controller: 'oauth' end get '.well-known/webfinger', to: 'webfinger#show' @@ -95,4 +106,8 @@ Rails.application.routes.draw do end root to: 'dashboard#index' + + direct :s3_image do |blob| + File.join(ENV['S3_ALIAS_HOST'], blob.key) + end end diff --git a/config/storage.yml b/config/storage.yml index 1f93f73..3ddfbb8 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -5,3 +5,13 @@ local: test: service: Disk root: <%= Rails.root.join("tmp/storage") %> + +<% if ENV["S3_ENABLED"] %> +s3: + service: S3 + endpoint: <%= ENV["S3_ENDPOINT"] %> + region: <%= ENV["S3_REGION"] %> + bucket: <%= ENV["S3_BUCKET"] %> + access_key_id: <%= ENV["S3_ACCESS_KEY"] %> + secret_access_key: <%= ENV["S3_SECRET_KEY"] %> +<% end %> diff --git a/db/migrate/20231019125135_create_app_catalog_web_apps.rb b/db/migrate/20231019125135_create_app_catalog_web_apps.rb new file mode 100644 index 0000000..451a54b --- /dev/null +++ b/db/migrate/20231019125135_create_app_catalog_web_apps.rb @@ -0,0 +1,11 @@ +class CreateAppCatalogWebApps < ActiveRecord::Migration[7.0] + def change + create_table :app_catalog_web_apps do |t| + t.string :url + t.string :name + t.text :metadata + + t.timestamps + end + end +end diff --git a/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb b/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb new file mode 100644 index 0000000..49457b5 --- /dev/null +++ b/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb @@ -0,0 +1,7 @@ +class AddWebAppIdToRemoteStorageAuthorizations < ActiveRecord::Migration[7.0] + def change + add_reference :remote_storage_authorizations, :web_app, foreign_key: { + to_table: :app_catalog_web_apps + } + end +end diff --git a/db/schema.rb b/db/schema.rb index bff7e78..f830293 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do +ActiveRecord::Schema[7.0].define(version: 2023_10_24_104909) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -39,6 +39,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "app_catalog_web_apps", force: :cascade do |t| + t.string "url" + t.string "name" + t.text "metadata" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "donations", force: :cascade do |t| t.integer "user_id" t.integer "amount_sats" @@ -88,8 +96,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do t.datetime "expire_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "web_app_id" t.index ["permissions"], name: "index_remote_storage_authorizations_on_permissions" t.index ["user_id"], name: "index_remote_storage_authorizations_on_user_id" + t.index ["web_app_id"], name: "index_remote_storage_authorizations_on_web_app_id" end create_table "settings", force: :cascade do |t| @@ -124,5 +134,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "remote_storage_authorizations", "app_catalog_web_apps", column: "web_app_id" add_foreign_key "remote_storage_authorizations", "users" end diff --git a/docker-compose.yml b/docker-compose.yml index 3690616..5106dfd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,12 +37,13 @@ services: environment: RAILS_ENV: development PRIMARY_DOMAIN: kosmos.org - REDIS_URL: redis://redis:6379/0 - RS_REDIS_URL: redis://redis:6379/1 LDAP_HOST: ldap LDAP_PORT: 3389 LDAP_ADMIN_PASSWORD: passthebutter LDAP_USE_TLS: "false" + REDIS_URL: redis://redis:6379/0 + RS_REDIS_URL: redis://redis:6379/1 + RS_STORAGE_URL: "http://localhost:4567" depends_on: - ldap - redis @@ -57,18 +58,51 @@ services: environment: RAILS_ENV: development PRIMARY_DOMAIN: kosmos.org - REDIS_URL: redis://redis:6379/0 - RS_REDIS_URL: redis://redis:6379/1 LDAP_HOST: ldap LDAP_PORT: 3389 LDAP_ADMIN_PASSWORD: passthebutter LDAP_USE_TLS: "false" LAUNCHY_DRY_RUN: true BROWSER: /dev/null + REDIS_URL: redis://redis:6379/0 + RS_REDIS_URL: redis://redis:6379/1 + RS_STORAGE_URL: "http://localhost:4567" depends_on: - ldap - redis + minio: + image: quay.io/minio/minio:latest + command: "server /data --console-address ':9001'" + networks: + - external_network + - internal_network + ports: + - "9000:9000" + - "9001:9001" + volumes: + - ./tmp/minio:/data + + liquor-cabinet: + image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2 + networks: + - external_network + - internal_network + ports: + - "4567:4567" + environment: + RACK_ENV: staging + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_DB: 1 + S3_ENDPOINT: http://minio:9000 + S3_ACCESS_KEY: dev-key + S3_SECRET_KEY: 123456789 + S3_BUCKET: remotestorage + depends_on: + - minio + - redis + # phpldapadmin: # image: osixia/phpldapadmin:0.9.0 # ports: diff --git a/spec/controllers/rs/oauth_controller_spec.rb b/spec/controllers/rs/oauth_controller_spec.rb index 48ca14b..09b0750 100644 --- a/spec/controllers/rs/oauth_controller_spec.rb +++ b/spec/controllers/rs/oauth_controller_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe Rs::OauthController, type: :controller do let(:user) { create :user } - describe "GET /rs/oauth/:useraddress" do + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + end + + describe "GET /rs/oauth/:username" do context "when user is signed in" do before do sign_in user @@ -14,7 +18,7 @@ RSpec.describe Rs::OauthController, type: :controller do before do get :new, params: { - useraddress: other_user.address, + username: other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" @@ -22,7 +26,7 @@ RSpec.describe Rs::OauthController, type: :controller do end it "logs out the users and repeats the request" do - url = new_rs_oauth_url other_user.address, + url = new_rs_oauth_url other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" @@ -34,7 +38,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "when no valid token exists" do before do get :new, params: { - useraddress: user.address, + username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", @@ -61,7 +65,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", client_id: "https://example.com" } @@ -75,7 +79,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no client_id" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com" } @@ -89,7 +93,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "different host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com/foobar", client_id: "https://google.com" @@ -116,7 +120,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with same host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "https://example.com" @@ -131,7 +135,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with different host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://app.example.com", client_id: "https://example.com" @@ -146,7 +150,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with different redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com/a_new_route", client_id: "https://example.com" @@ -161,7 +165,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with state param given" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "https://example.com", @@ -178,7 +182,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no scope" do before do get :new, params: { - useraddress: user.address, + username: user.cn, redirect_uri: "https://example.com", client_id: "https://example.com", state: "foobar123" @@ -193,7 +197,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "empty scope" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "", redirect_uri: "https://example.com", client_id: "https://example.com", @@ -210,7 +214,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "when user is not signed in" do it "redirects to the signin page with username pre-filled" do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,photos", redirect_uri: "https://example.com" } @@ -227,7 +231,7 @@ RSpec.describe Rs::OauthController, type: :controller do describe "full" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "*:rw", redirect_uri: "https://example.com", client_id: "example.com" @@ -243,7 +247,7 @@ RSpec.describe Rs::OauthController, type: :controller do describe "read-only" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "*:r", redirect_uri: "https://example.com", client_id: "example.com" @@ -258,7 +262,7 @@ RSpec.describe Rs::OauthController, type: :controller do end end - describe "POST /rs/oauth/:useraddress" do + describe "POST /rs/oauth/:username" do context "when user is signed in" do before do sign_in user @@ -433,33 +437,4 @@ RSpec.describe Rs::OauthController, type: :controller do end end end - - describe "GET /rs/oauth/token/:id/launch_app" do - context "when user is signed in" do - before do - sign_in user - end - - context "token exists" do - before do - @auth = user.remote_storage_authorizations.create!( - permissions: %w(documents), client_id: "app.example.com", - redirect_uri: "https://app.example.com", - expire_at: 2.days.from_now - ) - - get :launch_app, params: { id: @auth.id } - end - - after do - @auth.destroy - end - - it "redirects to the given URL with the correct RS URL fragment params" do - launch_url = "https://app.example.com#remotestorage=#{user.address}&access_token=#{@auth.token}" - expect(response).to redirect_to(launch_url) - end - end - end - end end diff --git a/spec/controllers/services/rs_auths_controller_spec.rb b/spec/controllers/services/rs_auths_controller_spec.rb new file mode 100644 index 0000000..44bcdc0 --- /dev/null +++ b/spec/controllers/services/rs_auths_controller_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Services::RsAuthsController, type: :controller do + let(:user) { create :user } + + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + allow_any_instance_of(Flipper).to receive(:enabled?).and_return(true) + end + + describe "GET /services/storage/rs_auths/:id/launch_app" do + context "when user is signed in" do + before do + sign_in user + end + + context "token exists" do + before do + @auth = user.remote_storage_authorizations.create!( + permissions: %w(documents), client_id: "app.example.com", + redirect_uri: "https://app.example.com", + expire_at: 2.days.from_now + ) + + get :launch_app, params: { id: @auth.id } + end + + after do + @auth.destroy + end + + it "redirects to the given URL with the correct RS URL fragment params" do + launch_url = "https://app.example.com#remotestorage=#{user.address}&access_token=#{@auth.token}" + expect(response).to redirect_to(launch_url) + end + end + end + end +end diff --git a/spec/factories/app_catalog/web_apps.rb b/spec/factories/app_catalog/web_apps.rb new file mode 100644 index 0000000..c07de2e --- /dev/null +++ b/spec/factories/app_catalog/web_apps.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :web_app, class: 'AppCatalog::WebApp' do + url { "https://myfavoritedrinks.remotestorage.io/" } + name { "My Favorite Drinks" } + end +end diff --git a/spec/factories/remote_storage_authorizations.rb b/spec/factories/remote_storage_authorizations.rb index be7c810..0cb47b1 100644 --- a/spec/factories/remote_storage_authorizations.rb +++ b/spec/factories/remote_storage_authorizations.rb @@ -1,9 +1,10 @@ FactoryBot.define do factory :remote_storage_authorization do permissions { ["documents:rw"] } - client_id { "some-fancy-app" } - redirect_uri { "https://example.com/some-fancy-app" } + client_id { "app.example.com" } + redirect_uri { "https://app.example.com" } app_name { "Fancy App" } - expire_at { nil } + expire_at { 1.month.from_now } + web_app end end diff --git a/spec/features/rs/oauth_spec.rb b/spec/features/rs/oauth_spec.rb index a68556f..9e499b8 100644 --- a/spec/features/rs/oauth_spec.rb +++ b/spec/features/rs/oauth_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do context "with normal permissions" do before do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") @@ -36,7 +36,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do context "root access" do context "full" do before do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: ":rw") @@ -60,7 +60,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do end it "prefills the username field in the signin form" do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") @@ -69,7 +69,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do end it "redirects to the OAuth dialog after sign-in" do - auth_url = new_rs_oauth_url(useraddress: user.address, + auth_url = new_rs_oauth_url(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") diff --git a/spec/jobs/remote_storage_expire_authorization_job_spec.rb b/spec/jobs/remote_storage_expire_authorization_job_spec.rb index e65662e..8e6cbb7 100644 --- a/spec/jobs/remote_storage_expire_authorization_job_spec.rb +++ b/spec/jobs/remote_storage_expire_authorization_job_spec.rb @@ -2,8 +2,13 @@ require 'rails_helper' RSpec.describe RemoteStorageExpireAuthorizationJob, type: :job do before do + allow_any_instance_of(AppCatalog::WebApp).to( + receive(:update_metadata).and_return(true) + ) + @user = create :user, cn: "ronald", ou: "kosmos.org" - @rs_authorization = create :remote_storage_authorization, user: @user, expire_at: 1.day.ago + @rs_authorization = create :remote_storage_authorization, + user: @user, expire_at: 1.day.ago end after do @@ -20,7 +25,7 @@ RSpec.describe RemoteStorageExpireAuthorizationJob, type: :job do } it "removes the RS authorization from redis" do - redis_key = "rs:authorizations:#{@user.address}:#{@rs_authorization.token}" + redis_key = "authorizations:#{@user.cn}:#{@rs_authorization.token}" expect(redis.keys(redis_key)).to_not be_empty perform_enqueued_jobs { job } diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index 3d046cf..3673bde 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -5,9 +5,13 @@ RSpec.describe RemoteStorageAuthorization, type: :model do let(:user) { create :user } + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + end + describe "#create" do after(:each) { clear_enqueued_jobs } - after(:all) { redis_rs_delete_keys("rs:authorizations:*") } + after(:all) { redis_rs_delete_keys("authorizations:*") } let(:auth) do user.remote_storage_authorizations.create!( @@ -22,7 +26,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end it "stores a token in redis" do - user_auth_keys = redis_rs.keys("rs:authorizations:#{user.address}:*") + user_auth_keys = redis_rs.keys("authorizations:#{user.cn}:*") expect(user_auth_keys.length).to eq(1) authorizations = redis_rs.smembers(user_auth_keys.first) @@ -44,7 +48,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do describe "#destroy" do after(:each) { clear_enqueued_jobs } - after(:all) { redis_rs_delete_keys("rs:authorizations:*") } + after(:all) { redis_rs_delete_keys("authorizations:*") } it "removes the token from redis" do auth = user.remote_storage_authorizations.create!( @@ -54,7 +58,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do ) auth.destroy! - expect(redis_rs.keys("rs:authorizations:#{user.address}:*")).to be_empty + expect(redis_rs.keys("authorizations:#{user.address}:*")).to be_empty end context "with expiry set" do @@ -72,141 +76,238 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end end - # describe "#find_or_create_web_app" do - # context "with origin that looks hosted" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # end - # - # it "generates a web_app" do - # expect(auth.web_app).to be_a(AppCatalog::WebApp) - # end - # - # it "uses the Web App's name as app name" do - # expect(auth.app_name).to eq("Example Domain") - # end - # end - # - # context "when creating two authorizations for the same app" do - # before do - # user_2 = create :user - # ResqueSpec.reset! - # auth_1 = user.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # auth_2 = user_2.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # end - # - # after do - # auth_1.destroy - # auth_2.destroy - # user_2.destroy - # end - # - # it "uses the same web app instance for both authorizations" do - # expect(auth_1.web_app).to be_a(AppCatalog::WebApp) - # expect(auth_1.web_app).to eq(auth_2.web_app) - # end - # end - # - # describe "non-production app origins" do - # context "when host is not an FQDN" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(recipes), - # client_id: "localhost:4200", - # redirect_uri: "http://localhost:4200" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("localhost:4200") - # end - # end - # - # context "when host is an IP address" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(recipes), - # client_id: "192.168.0.23:3000", - # redirect_uri: "http://192.168.0.23:3000" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("192.168.0.23:3000") - # end - # end - # - # context "when host is an extension URL" do # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(bookmarks), - # client_id: "123.addons.allizom.org", - # redirect_uri: "123.addons.allizom.org/foo" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("123.addons.allizom.org") - # end - # end - # end - # end + describe "#find_or_create_web_app" do + context "with origin that looks hosted" do + after(:all) { redis_rs_delete_keys("authorizations:*") } - # describe "auth notifications" do - # context "with auth notifications enabled" do - # before do - # ResqueSpec.reset! - # user.push(mailing_lists: "rs-auth-notifications-#{Rails.env}") - # auth = user.remote_storage_authorizations.create!( - # :permissions => %w(documents photos contacts:rw videos:r tasks/work:r), - # :client_id => "example.com", - # :redirect_uri => "https://example.com" - # ) - # end - # - # it "notifies the user via email" do - # expect(enqueued_jobs.size).to eq(1) - # job = enqueued_jobs.first - # expect(job).to eq( - # job: ActionMailer::DeliveryJob, - # args: ['StorageAuthorizationMailer', 'authorized_rs_app', 'deliver_now', - # auth.id.to_s], - # queue: 'mailers' - # ) - # end - # end - # - # context "with auth notifications disabled" do - # before do - # ResqueSpec.reset! - # user.pull(mailing_lists: "rs-auth-notifications-#{Rails.env}") - # auth = user.remote_storage_authorizations.create!( - # :permissions => %w(documents photos contacts:rw videos:r tasks/work:r), - # :client_id => "example.com", - # :redirect_uri => "https://example.com" - # ) - # end - # - # it "does not notify the user via email about new RS app" do - # expect(enqueued_jobs.size).to eq(0) - # end - # end - # end + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(documents:rw), + client_id: "example.com", + redirect_uri: "https://example.com" + ) + end + + it "generates a web_app" do + expect(auth.web_app).to be_a(AppCatalog::WebApp) + end + end + + context "when creating two authorizations for the same app" do + let(:user_2) { create :user, id: 23, cn: "michiel", email: "michiel@example.com" } + + let(:auth_1) do + user.remote_storage_authorizations.create!( + permissions: %w(documents photos contacts:rw videos:r tasks/work:r), + client_id: "example.com", + redirect_uri: "https://example.com" + ) + end + + let(:auth_2) do + user_2.remote_storage_authorizations.create!( + permissions: %w(documents photos contacts:rw videos:r tasks/work:r), + client_id: "example.com", + redirect_uri: "https://example.com" + ) + end + + after do + auth_1.destroy + auth_2.destroy + user_2.destroy + end + + it "uses the same web app for both authorizations" do + expect(auth_1.web_app).to eq(auth_2.web_app) + end + end + + describe "non-production app origins" do + context "when host is not an FQDN" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(recipes), + client_id: "localhost:4200", + redirect_uri: "http://localhost:4200" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("localhost:4200") + end + end + + context "when host is an IP address" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(recipes), + client_id: "192.168.0.23:3000", + redirect_uri: "http://192.168.0.23:3000" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("192.168.0.23:3000") + end + end + + context "when host is an extension URL" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(bookmarks), + client_id: "123.addons.allizom.org", + redirect_uri: "123.addons.allizom.org/foo" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("123.addons.allizom.org") + end + end + end + end + + describe "#launch_url" do + after(:all) { redis_rs_delete_keys("authorizations:*") } + + context "without start URL" do + before do + AppCatalog::WebApp.create!( + url: "https://webmarks.5apps.com", name: "Webmarks", + metadata: { name: "Webmarks", start_url: nil, scope: nil } + ) + end + + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(bookmarks:rw), client_id: "webmarks.5apps.com", + redirect_uri: "https://webmarks.5apps.com/connect" + ) + end + + it "uses the base URL (from client ID)" do + expect(auth.launch_url).to eq("https://webmarks.5apps.com") + end + end + + context "with start URL" do + before do + AppCatalog::WebApp.create!( + url: "https://hyperdraft.rosano.ca", name: "Hyperdraft", + metadata: { + name: "Hyperdraft", scope: nil, + start_url: "https://hyperdraft.rosano.ca/start" + } + ) + end + + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(notes:rw), client_id: "hyperdraft.rosano.ca", + redirect_uri: "https://hyperdraft.rosano.ca/write/foo" + ) + end + + describe "full URL" do + it "respects the start URL" do + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start") + end + + it "does not respect URLs outside of the client ID scope" do + auth.web_app.metadata[:start_url] = "https://uberdraft.rosano.ca/write" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca") + end + end + + describe "relative paths" do + it "includes the path relative from the base URL" do + auth.web_app.metadata[:start_url] = "start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + + auth.web_app.metadata[:start_url] = "./start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + + auth.web_app.metadata[:start_url] = "../start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + end + end + + describe "absolute path" do + it "includes the path relative from the base URL" do + auth.web_app.metadata[:start_url] = "/write" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/write") + end + end + end + end + + describe "notifications" do + include ActiveJob::TestHelper + + after(:each) { clear_enqueued_jobs } + after(:all) { redis_rs_delete_keys("authorizations:*") } + + before { allow(user).to receive(:display_name).and_return("Jimmy") } + + context "with notifications disabled" do + before do + user.preferences.merge!({ remotestorage_notify_auth_created: "off" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "does not notify the user via email about new RS app" do + expect(enqueued_jobs.size).to eq(0) + end + end + + context "with email notifications enabled" do + before do + user.preferences.merge!({ remotestorage_notify_auth_created: "email" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "notifies the user via email" do + expect(enqueued_jobs.size).to eq(1) + job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first + expect(job['arguments'][0]).to eq('NotificationMailer') + expect(job['arguments'][1]).to eq('remotestorage_auth_created') + expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1') + expect(job['arguments'][3]['params']['auth']['_aj_globalid']).to eq('gid://akkounts/RemoteStorageAuthorization/1') + end + end + + context "with XMPP notifications enabled" do + before do + Setting.xmpp_notifications_from_address = "botka@kosmos.org" + user.preferences.merge!({ remotestorage_notify_auth_created: "xmpp" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "sends an XMPP message to the account owner's JID" do + expect(enqueued_jobs.size).to eq(1) + expect(enqueued_jobs.first["job_class"]).to eq("XmppSendMessageJob") + msg = enqueued_jobs.first["arguments"].first + expect(msg["type"]).to eq("normal") + expect(msg["from"]).to eq("botka@kosmos.org") + expect(msg["to"]).to eq(user.address) + expect(msg["body"]).to match(/granted 'app\.example\.com' access to your Kosmos Storage/) + end + end + end end diff --git a/spec/requests/webfinger_spec.rb b/spec/requests/webfinger_spec.rb index f944a7a..1dcdfa3 100644 --- a/spec/requests/webfinger_spec.rb +++ b/spec/requests/webfinger_spec.rb @@ -15,10 +15,10 @@ RSpec.describe "WebFinger", type: :request do res = JSON.parse(response.body) rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"} - expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony@kosmos.org") + expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony") oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"] - expect(oauth_url).to eql("https://example.com/rs/oauth") + expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony") end end