diff --git a/README.md b/README.md index 1039200..acff556 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,17 @@ Each site node identifies itself via the `SITE_SUBDOMAIN` environment variable, ### Authentication -Upright supports OpenID Connect for authentication (Logto, Keycloak, Duo, Okta, etc.): +#### Static Credentials + +Upright uses static credentials by default with username `admin` and password `upright`. + +> [!WARNING] +> Change the default password before deploying to production by setting the `ADMIN_PASSWORD` environment variable. + + +#### OpenID Connect + +For production environments, Upright supports OpenID Connect (Logto, Keycloak, Duo, Okta, etc.): ```ruby # config/initializers/upright.rb @@ -131,14 +141,6 @@ Upright.configure do |config| end ``` -By default authentication is disabled but this is suitable for internal networks only: - -```ruby -Upright.configure do |config| - config.auth_provider = nil -end -``` - ## Defining Probes ### HTTP Probes @@ -402,7 +404,47 @@ Upright.configure do |config| end ``` -### Testing +## Local Development + +### Setup + +```bash +bin/setup +``` + +This installs dependencies, prepares the database, and starts the dev server. + +### Running Services + +Start supporting Docker services (Playwright server, etc.): + +```bash +bin/services +``` + +### Running the Server + +```bash +bin/dev +``` + +Visit http://app.upright.localhost:3000 and sign in with: +- **Username**: `admin` +- **Password**: `upright` (or value of `ADMIN_PASSWORD` env var) + +### Testing Playwright Probes + +Run probes with a visible browser window: + +```bash +LOCAL_PLAYWRIGHT=1 bin/rails console +``` + +```ruby +Probes::Playwright::MyServiceAuthProbe.check +``` + +### Running Tests ```bash bin/rails test diff --git a/app/controllers/concerns/upright/authentication.rb b/app/controllers/concerns/upright/authentication.rb index 7442040..ee14e54 100644 --- a/app/controllers/concerns/upright/authentication.rb +++ b/app/controllers/concerns/upright/authentication.rb @@ -8,9 +8,7 @@ module Upright::Authentication private def authenticate_user - if Upright.configuration.auth_provider.nil? - Upright::Current.user = Upright::User.new(email: "anonymous", name: "Anonymous") - elsif session[:user_info].present? + if session[:user_info].present? Upright::Current.user = Upright::User.new(session[:user_info]) else redirect_to engine_routes.new_admin_session_url(default_url_options.merge(subdomain: Upright.configuration.admin_subdomain)), allow_other_host: true diff --git a/app/controllers/upright/sessions_controller.rb b/app/controllers/upright/sessions_controller.rb index 8c58b56..9bdaad5 100644 --- a/app/controllers/upright/sessions_controller.rb +++ b/app/controllers/upright/sessions_controller.rb @@ -1,5 +1,6 @@ class Upright::SessionsController < Upright::ApplicationController skip_before_action :authenticate_user, only: [ :new, :create ] + skip_forgery_protection only: :create before_action :ensure_not_signed_in, only: [ :new, :create ] diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..d53625b --- /dev/null +++ b/bin/dev @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -eu +cd "$(dirname "${BASH_SOURCE[0]}")/../test/dummy" +exec bin/dev "$@" diff --git a/bin/services b/bin/services new file mode 100755 index 0000000..a52063f --- /dev/null +++ b/bin/services @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -eu +cd "$(dirname "${BASH_SOURCE[0]}")/../test/dummy" +exec bin/services "$@" diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..63cdef0 --- /dev/null +++ b/bin/setup @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -eu +cd "$(dirname "${BASH_SOURCE[0]}")/../test/dummy" +exec bin/setup "$@" diff --git a/config/routes.rb b/config/routes.rb index 3195269..af920ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,7 @@ root "sites#index", as: :admin_root resource :session, only: [ :new, :create ], as: :admin_session - get "auth/:provider/callback", to: "sessions#create", as: :auth_callback + match "auth/:provider/callback", to: "sessions#create", as: :auth_callback, via: [ :get, :post ] # Dashboards scope :dashboards, as: :dashboard do diff --git a/lib/generators/upright/install/install_generator.rb b/lib/generators/upright/install/install_generator.rb index e0c13b9..e233ae7 100644 --- a/lib/generators/upright/install/install_generator.rb +++ b/lib/generators/upright/install/install_generator.rb @@ -5,8 +5,9 @@ class InstallGenerator < Rails::Generators::Base desc "Install Upright engine into your application" - def copy_initializer + def copy_initializers template "upright.rb", "config/initializers/upright.rb" + template "omniauth.rb", "config/initializers/omniauth.rb" end def copy_sites_config @@ -43,8 +44,8 @@ def show_post_install_message say " 1. Run migrations: bin/rails db:migrate" say " 2. Configure your servers in config/deploy.yml" say " 3. Configure sites in config/sites.yml" - say " 4. Add probes in config/probes/*.yml" - say " 5. Configure authentication in config/initializers/upright.rb" + say " 4. Add probes in probes/*.yml" + say " 5. Set ADMIN_PASSWORD env var (default: upright)" say "" say "For production, review config/initializers/upright.rb and update:" say " config.hostname = \"honcho-upright.com\"" diff --git a/lib/generators/upright/install/templates/omniauth.rb b/lib/generators/upright/install/templates/omniauth.rb new file mode 100644 index 0000000..79f4173 --- /dev/null +++ b/lib/generators/upright/install/templates/omniauth.rb @@ -0,0 +1,8 @@ +# WARNING: Change the default password before deploying to production! +# Set the ADMIN_PASSWORD environment variable or update the credentials below. + +Rails.application.config.middleware.use OmniAuth::Builder do + provider :static_credentials, + title: "Sign In", + credentials: { "admin" => ENV.fetch("ADMIN_PASSWORD", "upright") } +end diff --git a/lib/generators/upright/install/templates/upright.rb b/lib/generators/upright/install/templates/upright.rb index 803b7b4..753e905 100644 --- a/lib/generators/upright/install/templates/upright.rb +++ b/lib/generators/upright/install/templates/upright.rb @@ -18,7 +18,4 @@ # client_id: ENV["OIDC_CLIENT_ID"], # client_secret: ENV["OIDC_CLIENT_SECRET"] # } - # - # No authentication (internal networks only) - config.auth_provider = nil end diff --git a/lib/omniauth/strategies/static_credentials.rb b/lib/omniauth/strategies/static_credentials.rb new file mode 100644 index 0000000..1f690c1 --- /dev/null +++ b/lib/omniauth/strategies/static_credentials.rb @@ -0,0 +1,57 @@ +require "omniauth" + +module OmniAuth + module Strategies + class StaticCredentials + include OmniAuth::Strategy + + option :name, "static_credentials" + option :title, "Sign In" + option :credentials, {} + + def request_phase + OmniAuth::Form.build(title: options.title, url: callback_path) do + text_field "Username", "username" + password_field "Password", "password" + end.to_response + end + + def callback_phase + if valid_credentials? + super + else + fail!(:invalid_credentials) + end + end + + uid { username } + + info do + { name: username, email: "#{username}@localhost" } + end + + protected + + def valid_credentials? + return false if username.blank? || password.blank? + + configured_credentials.any? do |user, pass| + ActiveSupport::SecurityUtils.secure_compare(username, user.to_s) && + ActiveSupport::SecurityUtils.secure_compare(password, pass.to_s) + end + end + + def configured_credentials + options.credentials || {} + end + + def username + request.params["username"] + end + + def password + request.params["password"] + end + end + end +end diff --git a/lib/upright.rb b/lib/upright.rb index 6a825dc..de40908 100644 --- a/lib/upright.rb +++ b/lib/upright.rb @@ -8,6 +8,7 @@ require "omniauth" require "omniauth_openid_connect" require "omniauth/rails_csrf_protection" +require "omniauth/strategies/static_credentials" require "propshaft" require "importmap-rails" require "turbo-rails" diff --git a/lib/upright/configuration.rb b/lib/upright/configuration.rb index e433a61..89b86b3 100644 --- a/lib/upright/configuration.rb +++ b/lib/upright/configuration.rb @@ -44,7 +44,7 @@ def initialize @playwright_server_url = ENV["PLAYWRIGHT_SERVER_URL"] @otel_endpoint = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] - @auth_provider = nil + @auth_provider = :static_credentials @auth_options = {} end @@ -86,25 +86,20 @@ def hostname=(value) end def hostname - if Rails.env.local? - @hostname&.sub(/\.[^.]+\z/, ".localhost") - else - @hostname - end + @hostname end def default_url_options if Rails.env.production? { protocol: "https", host: "#{admin_subdomain}.#{hostname}", domain: hostname } else - { protocol: "http", host: "#{admin_subdomain}.#{hostname}", port: 3040, domain: hostname } + { protocol: "http", host: "#{admin_subdomain}.#{hostname}", port: ENV.fetch("PORT", 3000).to_i, domain: hostname } end end private def configure_allowed_hosts Rails.application.config.hosts = [ /.*\.#{Regexp.escape(hostname)}/, hostname ] - Rails.application.config.action_controller.default_url_options = default_url_options Rails.application.config.action_dispatch.tld_length = 1 end end diff --git a/lib/upright/engine.rb b/lib/upright/engine.rb index 23f4cdc..db8dd31 100644 --- a/lib/upright/engine.rb +++ b/lib/upright/engine.rb @@ -71,7 +71,7 @@ class Upright::Engine < ::Rails::Engine # Start metrics server when Solid Queue runs standalone (not embedded in Puma) initializer "upright.solid_queue_metrics" do SolidQueue.on_start do - unless ENV["SOLID_QUEUE_IN_PUMA"] + unless ENV["SOLID_QUEUE_IN_PUMA"] || Rails.env.local? ENV["PROMETHEUS_EXPORTER_PORT"] ||= "9394" ENV["PROMETHEUS_EXPORTER_LOG_REQUESTS"] = "false" Yabeda::Prometheus::Exporter.start_metrics_server! diff --git a/test/dummy/.foreman b/test/dummy/.foreman new file mode 100644 index 0000000..87c3f5a --- /dev/null +++ b/test/dummy/.foreman @@ -0,0 +1 @@ +port: 3000 diff --git a/test/dummy/Procfile.dev b/test/dummy/Procfile.dev new file mode 100644 index 0000000..da81e28 --- /dev/null +++ b/test/dummy/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server -b '0.0.0.0' -p $PORT +jobs: bin/rails solid_queue:start diff --git a/test/dummy/bin/dev b/test/dummy/bin/dev index 5f91c20..aac2f73 100755 --- a/test/dummy/bin/dev +++ b/test/dummy/bin/dev @@ -1,2 +1,8 @@ -#!/usr/bin/env ruby -exec "./bin/rails", "server", *ARGV +#!/usr/bin/env bash + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +exec foreman start -f Procfile.dev "$@" diff --git a/test/dummy/bin/services b/test/dummy/bin/services new file mode 100755 index 0000000..857f90f --- /dev/null +++ b/test/dummy/bin/services @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -eu +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +docker compose up -d "$@" --remove-orphans +docker compose ps diff --git a/test/dummy/config/initializers/0_url_options.rb b/test/dummy/config/initializers/0_url_options.rb deleted file mode 100644 index 497b506..0000000 --- a/test/dummy/config/initializers/0_url_options.rb +++ /dev/null @@ -1,17 +0,0 @@ -DEFAULT_URL_OPTIONS = { - protocol: "http", - host: "app.upright.localhost", - port: 3040, - domain: "upright.localhost" -}.freeze - -Rails.application.configure do - config.action_controller.default_url_options = DEFAULT_URL_OPTIONS - config.action_dispatch.tld_length = 1 - config.hosts = [ /.*\.upright.localhost/, "upright.localhost" ] -end - -Rails.application.config.after_initialize do - Rails.application.routes.default_url_options = DEFAULT_URL_OPTIONS - Upright::Engine.routes.default_url_options = DEFAULT_URL_OPTIONS -end diff --git a/test/dummy/config/initializers/omniauth.rb b/test/dummy/config/initializers/omniauth.rb index 3135f56..2bdcc45 100644 --- a/test/dummy/config/initializers/omniauth.rb +++ b/test/dummy/config/initializers/omniauth.rb @@ -1,15 +1,8 @@ +# WARNING: Change the default password before deploying to production! +# Set the ADMIN_PASSWORD environment variable or update the credentials below. + Rails.application.config.middleware.use OmniAuth::Builder do - provider :openid_connect, - name: :duo, - issuer: "https://example.auth0.com", - discovery: false, - scope: %i[ openid email profile ], - client_options: { - identifier: "test-client-id", - secret: "test-client-secret", - redirect_uri: "http://app.upright.localhost:3040/auth/duo/callback", - authorization_endpoint: "https://example.auth0.com/authorize", - token_endpoint: "https://example.auth0.com/oauth/token", - userinfo_endpoint: "https://example.auth0.com/userinfo" - } + provider :static_credentials, + title: "Upright Sign In", + credentials: { "admin" => ENV.fetch("ADMIN_PASSWORD", "upright") } end diff --git a/test/dummy/config/initializers/upright.rb b/test/dummy/config/initializers/upright.rb index 61e59ce..70a2e7e 100644 --- a/test/dummy/config/initializers/upright.rb +++ b/test/dummy/config/initializers/upright.rb @@ -1,6 +1,5 @@ Upright.configure do |config| - config.probes_path = Rails.root.join("config/upright/probes") - config.frozen_record_path = Rails.root.join("config") + config.hostname = "upright.localhost" config.user_agent = "Upright-Test/1.0" - config.auth_provider = :openid_connect + config.auth_provider = :static_credentials end diff --git a/test/dummy/config/recurring.yml b/test/dummy/config/recurring.yml index b4207f9..04d267e 100644 --- a/test/dummy/config/recurring.yml +++ b/test/dummy/config/recurring.yml @@ -1,15 +1,33 @@ -# examples: -# periodic_cleanup: -# class: CleanSoftDeletedRecordsJob -# queue: background -# args: [ 1000, { batch_size: 500 } ] -# schedule: every hour -# periodic_cleanup_with_command: -# command: "SoftDeletedRecord.due.delete_all" -# priority: 2 -# schedule: at 5am every day +development: + http_probes: + schedule: every minute + command: "Upright::Probes::HTTPProbe.check_and_record_all_later" + + smtp_probes: + schedule: every minute + command: "Upright::Probes::SMTPProbe.check_and_record_all_later" + + playwright_example_probe: + schedule: every 5 minutes + command: "Probes::Playwright::ExampleProbe.check_and_record_later" production: + http_probes: + schedule: "*/30 * * * * *" + command: "Upright::Probes::HTTPProbe.check_and_record_all_later" + + smtp_probes: + schedule: "*/30 * * * * *" + command: "Upright::Probes::SMTPProbe.check_and_record_all_later" + + playwright_example_probe: + schedule: every 5 minutes + command: "Probes::Playwright::ExampleProbe.check_and_record_later" + clear_solid_queue_finished_jobs: command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" schedule: every hour at minute 12 + + cleanup_stale_probe_results: + command: "Upright::ProbeResult.stale.in_batches.destroy_all" + schedule: every hour at minute 30 diff --git a/test/dummy/config/upright/probes/smtp_probes.yml b/test/dummy/config/upright/probes/smtp_probes.yml deleted file mode 100644 index d29921f..0000000 --- a/test/dummy/config/upright/probes/smtp_probes.yml +++ /dev/null @@ -1,7 +0,0 @@ -- name: "Gmail" - host: "smtp.gmail.com" - port: 587 - -- name: "Outlook" - host: "smtp.office365.com" - port: 587 diff --git a/test/dummy/docker-compose.yml b/test/dummy/docker-compose.yml new file mode 100644 index 0000000..47d2927 --- /dev/null +++ b/test/dummy/docker-compose.yml @@ -0,0 +1,8 @@ +services: + playwright: + image: jacoblincool/playwright:chromium-server-1.56.1 + container_name: playwright + ports: + - "53333:53333" + environment: + - DEBUG=true diff --git a/test/dummy/probes/example_probe.rb b/test/dummy/probes/example_probe.rb new file mode 100644 index 0000000..c2fda1a --- /dev/null +++ b/test/dummy/probes/example_probe.rb @@ -0,0 +1,8 @@ +class Probes::Playwright::ExampleProbe < Upright::Probes::Playwright::Base + def probe_name = "Example: page load" + + def check + page.goto("https://example.com") + page.wait_for_selector("h1") + end +end diff --git a/test/dummy/config/upright/probes/http_probes.yml b/test/dummy/probes/http_probes.yml similarity index 72% rename from test/dummy/config/upright/probes/http_probes.yml rename to test/dummy/probes/http_probes.yml index 3a56133..076013a 100644 --- a/test/dummy/config/upright/probes/http_probes.yml +++ b/test/dummy/probes/http_probes.yml @@ -1,9 +1,6 @@ - name: "Example" url: "https://example.com/" -- name: "Google" - url: "https://www.google.com/" - - name: "Expected301" url: "https://example.com/redirect" expected_status: 301 diff --git a/test/dummy/probes/smtp_probes.yml b/test/dummy/probes/smtp_probes.yml new file mode 100644 index 0000000..214228c --- /dev/null +++ b/test/dummy/probes/smtp_probes.yml @@ -0,0 +1,3 @@ +- name: "Gmail" + host: "smtp.gmail.com" + port: 25 diff --git a/test/dummy/config/upright/probes/traceroute_probes.yml b/test/dummy/probes/traceroute_probes.yml similarity index 100% rename from test/dummy/config/upright/probes/traceroute_probes.yml rename to test/dummy/probes/traceroute_probes.yml diff --git a/test/lib/helpers/authentication_helper.rb b/test/lib/helpers/authentication_helper.rb index fb15e3d..ecf5cd4 100644 --- a/test/lib/helpers/authentication_helper.rb +++ b/test/lib/helpers/authentication_helper.rb @@ -1,13 +1,16 @@ module AuthenticationHelper def sign_in(email: "test@example.com", name: "Test User") + provider = Upright.configuration.auth_provider + OmniAuth.config.test_mode = true - OmniAuth.config.mock_auth[:duo] = OmniAuth::AuthHash.new({ - provider: "duo", + OmniAuth.config.mock_auth[provider] = OmniAuth::AuthHash.new({ + provider: provider.to_s, + uid: email, info: { email: email, name: name } }) on_subdomain :app - get upright.auth_callback_url(:duo) + get upright.auth_callback_url(provider) end def sign_out diff --git a/test/lib/helpers/subdomain_helper.rb b/test/lib/helpers/subdomain_helper.rb index 9aa782c..dcc3c4d 100644 --- a/test/lib/helpers/subdomain_helper.rb +++ b/test/lib/helpers/subdomain_helper.rb @@ -1,9 +1,11 @@ module SubdomainHelper def on_subdomain(subdomain) + hostname = Upright.configuration.hostname + if subdomain.present? - host! "#{subdomain}.#{DEFAULT_URL_OPTIONS[:domain]}" + host! "#{subdomain}.#{hostname}" else - host! DEFAULT_URL_OPTIONS[:domain] + host! hostname end end end diff --git a/test/models/upright/site_test.rb b/test/models/upright/site_test.rb index e5aef70..5b83f6a 100644 --- a/test/models/upright/site_test.rb +++ b/test/models/upright/site_test.rb @@ -27,7 +27,7 @@ class Upright::SiteTest < ActiveSupport::TestCase end test "url builds subdomain url from code" do - assert_equal "http://ams.upright.localhost:3040/", @site.url + assert_equal "http://ams.upright.localhost:3000/", @site.url end test "to_leaflet returns map marker data" do @@ -37,6 +37,6 @@ class Upright::SiteTest < ActiveSupport::TestCase assert_equal "Amsterdam", result[:city] assert_in_delta 52.37, result[:lat], 0.01 assert_in_delta 4.89, result[:lon], 0.01 - assert_equal "http://ams.upright.localhost:3040/", result[:url] + assert_equal "http://ams.upright.localhost:3000/", result[:url] end end