diff --git a/.rubocop.yml b/.rubocop.yml index 283a0b025..a94e31595 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,6 +24,21 @@ Lint/EmptyFile: Exclude: - 'packages/forest_admin_rails/app/models/forest_admin_rails/application_record.rb' +Metrics/AbcSize: + Exclude: + - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/auth_manager.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb' + - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb' + +Metrics/CyclomaticComplexity: + Exclude: + - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb' + +Metrics/MethodLength: + Max: 20 + Exclude: + - 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb' + Style/BlockComments: Exclude: - 'packages/forest_admin_agent/spec/spec_helper.rb' @@ -99,3 +114,12 @@ Style/RedundantConstantBase: Layout/LineLength: Max: 120 + +RSpec/ExampleLength: + Max: 20 + +RSpec/MultipleExpectations: + Max: 5 + +RSpec/MultipleMemoizedHelpers: + Max: 10 diff --git a/README.md b/README.md index 5e82dac79..784685372 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Forest Admin agent PHP +# Forest Admin agent Ruby Forest Admin provides an off-the-shelf administration panel based on a highly-extensible API plugged into your application. diff --git a/bin/run_rspec b/bin/run_rspec index 8ab6eaad7..5eeb66f4e 100755 --- a/bin/run_rspec +++ b/bin/run_rspec @@ -4,19 +4,19 @@ require 'rspec' require 'rspec/core/rake_task' # Liste des dossiers à parcourir pour les tests -folders_to_test = %w[./packages/forestadmin_agent ./packages/forestadmin_rails] +folders_to_test = %w[./packages/forest_admin_agent ./packages/forest_admin_rails] # Boucle à travers les dossiers et exécute les tests RSpec avec la configuration spécifique folders_to_test.each do |folder| if File.directory?(folder) if File.exist?(folder) - puts "Exécution des tests RSpec dans le dossier : #{folder}" + puts "Running RSpec tests in the folder : #{folder}" RSpec::Core::Runner.run(%W[--require #{folder}/spec/spec_helper.rb #{folder}]) else - puts "Fichier de configuration RSpec non trouvé dans le dossier : #{folder}/spec" + puts "RSpec configuration file not found in folder : #{folder}/spec" end else - puts "Dossier non trouvé : #{folder}" + puts "Folder not found : #{folder}" end end diff --git a/packages/forest_admin_agent/forest_admin_agent.gemspec b/packages/forest_admin_agent/forest_admin_agent.gemspec index 259df194c..c5ad5b4b2 100644 --- a/packages/forest_admin_agent/forest_admin_agent.gemspec +++ b/packages/forest_admin_agent/forest_admin_agent.gemspec @@ -35,7 +35,10 @@ admin work on any Ruby application." spec.add_dependency "dry-container", "~> 0.11" spec.add_dependency "lightly", "~> 0.4.0" + spec.add_dependency "jwt", "~> 2.7" spec.add_dependency "mono_logger", "~> 1.1" + spec.add_dependency "openid_connect", "~> 2.2" spec.add_dependency "rake", "~> 13.0" + spec.add_dependency "rack-cors", "~> 2.0" spec.add_dependency "zeitwerk", "~> 2.3" end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent.rb b/packages/forest_admin_agent/lib/forest_admin_agent.rb index 762297b2a..40ca59727 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent.rb @@ -2,6 +2,7 @@ require 'zeitwerk' loader = Zeitwerk::Loader.for_gem +loader.inflector.inflect('oauth2' => 'OAuth2') loader.setup module ForestAdminAgent diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/auth_manager.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/auth_manager.rb new file mode 100644 index 000000000..992604040 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/auth_manager.rb @@ -0,0 +1,50 @@ +require 'json' + +module ForestAdminAgent + module Auth + class AuthManager + def initialize + @oidc = ForestAdminAgent::Auth::OidcClientManager.new + end + + def start(rendering_id) + client = @oidc.make_forest_provider rendering_id + client.authorization_uri({ state: JSON.generate({ renderingId: rendering_id }) }) + end + + def verify_code_and_generate_token(params) + raise Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_MISSING unless params['state'] + + if Facades::Container.cache(:debug) + OpenIDConnect.http_config do |options| + options.ssl.verify = false + end + end + + rendering_id = get_rendering_id_from_state(params['state']) + + forest_provider = @oidc.make_forest_provider rendering_id + forest_provider.authorization_code = params['code'] + access_token = forest_provider.access_token! 'none' + resource_owner = forest_provider.get_resource_owner access_token + + resource_owner.make_jwt + end + + private + + def get_rendering_id_from_state(state) + state = JSON.parse(state.tr("'", '"').gsub('=>', ':')) + raise Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_RENDERING_ID unless state.key? 'renderingId' + + begin + Integer(state['renderingId']) + rescue ArgumentError + raise Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID + end + + state['renderingId'].to_i + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb new file mode 100644 index 000000000..0488c4990 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb @@ -0,0 +1,62 @@ +require 'openid_connect' +require_relative 'forest_resource_owner' + +module ForestAdminAgent + module Auth + module OAuth2 + class ForestProvider < OpenIDConnect::Client + attr_reader :rendering_id + + def initialize(rendering_id, attributes = {}) + super attributes + @rendering_id = rendering_id + @authorization_endpoint = '/oidc/auth' + @token_endpoint = '/oidc/token' + self.userinfo_endpoint = "/liana/v2/renderings/#{rendering_id}/authorization" + end + + def get_resource_owner(access_token) + headers = { 'forest-token': access_token.access_token, 'forest-secret-key': secret } + hash = check_response do + OpenIDConnect.http_client.get access_token.client.userinfo_uri, {}, headers + end + + response = OpenIDConnect::ResponseObject::UserInfo.new hash + + create_resource_owner response.raw_attributes[:data] + end + + private + + def create_resource_owner(data) + ForestResourceOwner.new data, rendering_id + end + + def check_response + response = yield + case response.status + when 200 + server_error = response.body.key?('errors') ? response.body['errors'][0] : nil + if server_error && + server_error['name'] == Utils::ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED + raise Error, Utils::ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED + end + + response.body.with_indifferent_access + when 400 + raise OpenIDConnect::BadRequest.new('API Access Failed', response) + when 401 + raise OpenIDConnect::Unauthorized.new(Utils::ErrorMessages::AUTHORIZATION_FAILED, response) + when 404 + raise OpenIDConnect::HttpError.new(response.status, Utils::ErrorMessages::SECRET_NOT_FOUND, response) + when 422 + raise OpenIDConnect::HttpError.new(response.status, + Utils::ErrorMessages::SECRET_AND_RENDERINGID_INCONSISTENT, response) + else + raise OpenIDConnect::HttpError.new(response.status, 'Unknown HttpError', response) + end + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb new file mode 100644 index 000000000..4616e6b75 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_resource_owner.rb @@ -0,0 +1,42 @@ +require 'date' +require 'jwt' + +module ForestAdminAgent + module Auth + module OAuth2 + class ForestResourceOwner + def initialize(data, rendering_id) + @data = data + @rendering_id = rendering_id + end + + def id + @data['id'] + end + + def expiration_in_seconds + (DateTime.now + (1 / 24.0)).to_time.to_i + end + + def make_jwt + attributes = @data['attributes'] + user = { + id: id, + email: attributes['email'], + first_name: attributes['first_name'], + last_name: attributes['last_name'], + team: attributes['teams'][0], + tags: attributes['tags'], + rendering_id: @rendering_id, + exp: expiration_in_seconds, + permission_level: attributes['permission_level'] + } + + JWT.encode user, + Facades::Container.cache(:auth_secret), + 'HS256' + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/oidc_config.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/oidc_config.rb new file mode 100644 index 000000000..0cf6f1a3c --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/oidc_config.rb @@ -0,0 +1,29 @@ +require 'openid_connect' + +module ForestAdminAgent + module Auth + module OAuth2 + class OidcConfig + def self.discover!(identifier, cache_options = {}) + uri = URI.parse(identifier) + Resource.new(uri).discover!(cache_options).tap do |response| + response.expected_issuer = identifier + response.validate! + end + rescue SWD::Exception, OpenIDConnect::ValidationFailed => e + raise OpenIDConnect::Discovery::DiscoveryFailed, e.message + end + + class Resource < OpenIDConnect::Discovery::Provider::Config::Resource + def initialize(uri) + super + @host = uri.host + @port = uri.port unless [80, 443].include?(uri.port) + @path = File.join uri.path, 'oidc/.well-known/openid-configuration' + attr_missing! + end + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb new file mode 100644 index 000000000..71070f634 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/auth/oidc_client_manager.rb @@ -0,0 +1,71 @@ +require 'openid_connect' +require_relative 'oauth2/oidc_config' +require_relative 'oauth2/forest_provider' + +module ForestAdminAgent + module Auth + class OidcClientManager + TTL = 60 * 60 * 24 + + def make_forest_provider(rendering_id) + config_agent = Facades::Container.config_from_cache + cache_key = "#{config_agent[:env_secret]}-client-data" + cache = setup_cache(cache_key, config_agent) + + render_provider(cache, rendering_id, config_agent[:env_secret]) + end + + private + + def setup_cache(env_secret, config_agent) + lightly = Lightly.new(life: TTL, dir: "#{config_agent[:cache_dir]}/issuer") + lightly.get env_secret do + oidc_config = retrieve_config(config_agent[:forest_server_url]) + credentials = register( + config_agent[:env_secret], + oidc_config.raw['registration_endpoint'], + { + token_endpoint_auth_method: 'none', + registration_endpoint: oidc_config.raw['registration_endpoint'], + application_type: 'web' + } + ) + + { + client_id: credentials['client_id'], + issuer: oidc_config.raw['issuer'], + redirect_uri: credentials['redirect_uris'].first + } + end + end + + def register(env_secret, registration_endpoint, data) + response = OpenIDConnect.http_client.post( + registration_endpoint, + data, + { 'Authorization' => "Bearer #{env_secret}" } + ) + + response.body + end + + def render_provider(cache, rendering_id, secret) + OAuth2::ForestProvider.new( + rendering_id, + { + identifier: cache[:client_id], + redirect_uri: cache[:redirect_uri], + host: cache[:issuer].to_s.sub(%r{^https?://(www.)?}, ''), + secret: secret + } + ) + end + + def retrieve_config(uri) + OAuth2::OidcConfig.discover! uri + rescue OpenIDConnect::Discovery::DiscoveryFailed + raise Error, ForestAdminAgent::Utils::ErrorMessages::SERVER_DOWN + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/facades/container.rb b/packages/forest_admin_agent/lib/forest_admin_agent/facades/container.rb new file mode 100644 index 000000000..bf35a7756 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/facades/container.rb @@ -0,0 +1,19 @@ +module ForestAdminAgent + module Facades + class Container + def self.instance + ForestAdminAgent::Builder::AgentFactory.instance.container + end + + def self.config_from_cache + instance.resolve(:cache).get('config') + end + + def self.cache(key) + raise "Key #{key} not found in container" unless config_from_cache.key?(key) + + config_from_cache[key] + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb index e0e291329..1b190f190 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/http/router.rb @@ -1,11 +1,15 @@ module ForestAdminAgent module Http class Router + include ForestAdminAgent::Routes + def self.routes [ # actions_routes, # api_charts_routes, - ForestAdminAgent::Routes::System::HealthCheck.new.routes + System::HealthCheck.new.routes, + Security::Authentication.new.routes, + Resources::List.new.routes ].inject(&:merge) end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_route.rb index e61ddc759..981c0f4fe 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_route.rb @@ -1,10 +1,7 @@ module ForestAdminAgent module Routes class AbstractRoute - attr_reader :request - def initialize - @request = ActionDispatch::Request.new({}) @routes = {} setup_routes end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb new file mode 100644 index 000000000..008b66dcb --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -0,0 +1,18 @@ +module ForestAdminAgent + module Routes + module Resources + class List < AbstractRoute + include ForestAdminAgent::Builder + def setup_routes + add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) }) + + self + end + + def handle_request(args = {}) + { name: args['collection_name'], content: args['collection_name'] } + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/security/authentication.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/security/authentication.rb new file mode 100644 index 000000000..f2efbb2be --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/security/authentication.rb @@ -0,0 +1,82 @@ +require 'jwt' + +module ForestAdminAgent + module Routes + module Security + class Authentication < AbstractRoute + include ForestAdminAgent::Builder + def setup_routes + add_route( + 'forest_authentication', + 'POST', + '/authentication', ->(args) { handle_authentication(args) } + ) + add_route( + 'forest_authentication-callback', + 'GET', + '/authentication/callback', ->(args) { handle_authentication_callback(args) } + ) + add_route( + 'forest_logout', + 'POST', + '/authentication/logout', ->(args) { handle_authentication_logout(args) } + ) + + self + end + + def handle_authentication(args = {}) + rendering_id = get_and_check_rendering_id args + + { + content: { + authorizationUrl: auth.start(rendering_id) + } + } + end + + def handle_authentication_callback(args = {}) + token = auth.verify_code_and_generate_token(args) + token_data = JWT.decode( + token, + Facades::Container.cache(:auth_secret), + true, + { algorithm: 'HS256' } + )[0] + + { + content: { + token: token, + tokenData: token_data + } + } + end + + def handle_authentication_logout(_args = {}) + { + content: nil, + status: 204 + } + end + + def auth + ForestAdminAgent::Auth::AuthManager.new + end + + protected + + def get_and_check_rendering_id(params) + raise Error, ForestAdminAgent::Utils::ErrorMessages::MISSING_RENDERING_ID unless params['renderingId'] + + begin + Integer(params['renderingId']) + rescue ArgumentError + raise Error, ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID + end + + params['renderingId'].to_i + end + end + end + end +end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/system/health_check.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/system/health_check.rb index eb833f914..aadbe2d0f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/system/health_check.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/system/health_check.rb @@ -4,7 +4,7 @@ module System class HealthCheck < AbstractRoute include ForestAdminAgent::Builder def setup_routes - add_route('forest', 'GET', '/', handle_request) + add_route('forest', 'GET', '/', ->(args) { handle_request(args) }) self end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/error_messages.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/error_messages.rb new file mode 100644 index 000000000..cb4a6f680 --- /dev/null +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/error_messages.rb @@ -0,0 +1,38 @@ +module ForestAdminAgent + module Utils + class ErrorMessages + AUTH_SECRET_MISSING = 'Your Forest authSecret seems to be missing. Can you check that you properly set a Forest +authSecret in the Forest initializer?'.freeze + + SECRET_AND_RENDERINGID_INCONSISTENT = 'Cannot retrieve the project you\'re trying to unlock. The envSecret and +renderingId seems to be missing or inconsistent.'.freeze + + SERVER_DOWN = 'Cannot retrieve the data from the Forest server. Forest API seems to be down right now.'.freeze + + SECRET_NOT_FOUND = 'Cannot retrieve the data from the Forest server. Can you check that you properly copied the +Forest envSecret in the Liana initializer?'.freeze + + UNEXPECTED = 'Cannot retrieve the data from the Forest server. An error occured in Forest API'.freeze + + INVALID_STATE_MISSING = 'Invalid response from the authentication server: the state parameter is missing'.freeze + + INVALID_STATE_FORMAT = 'Invalid response from the authentication server: the state parameter is not at the right +format'.freeze + + INVALID_STATE_RENDERING_ID = 'Invalid response from the authentication server: the state does not contain a +renderingId'.freeze + + MISSING_RENDERING_ID = 'Authentication request must contain a renderingId'.freeze + + INVALID_RENDERING_ID = 'The parameter renderingId is not valid'.freeze + + REGISTRATION_FAILED = 'The registration to the authentication API failed, response: '.freeze + + OIDC_CONFIGURATION_RETRIEVAL_FAILED = 'Failed to retrieve the provider\'s configuration.'.freeze + + TWO_FACTOR_AUTHENTICATION_REQUIRED = 'TwoFactorAuthenticationRequiredForbiddenError'.freeze + + AUTHORIZATION_FAILED = 'Error while authorizing the user on Forest Admin'.freeze + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/auth/auth_manager.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/auth/auth_manager.rbs new file mode 100644 index 000000000..9e3877592 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/auth/auth_manager.rbs @@ -0,0 +1,14 @@ +module ForestAdminAgent + module Auth + class AuthManager + @oidc: ForestAdminAgent::Auth::OidcClientManager + + def initialize: -> void + def start: (untyped rendering_id) -> untyped + def verify_code_and_generate_token: (untyped params) -> untyped + + private + def get_rendering_id_from_state: (untyped state) -> untyped + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs new file mode 100644 index 000000000..a07f05f97 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_provider.rbs @@ -0,0 +1,16 @@ +module ForestAdminAgent + module Auth + module OAuth2 + class ForestProvider + attr_reader rendering_id: Integer + + def initialize: (Integer , Array[String]) -> void + def get_resource_owner: -> ForestResourceOwner + + private + def create_resource_owner: -> ForestResourceOwner + def check_response: -> Array[String] + end + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs new file mode 100644 index 000000000..5130a39b8 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oauth2/forest_resource_owner.rbs @@ -0,0 +1,15 @@ +module ForestAdminAgent + module Auth + module OAuth2 + class ForestResourceOwner + @data: Array[String] + @rendering_id: Integer + + def initialize: (Array[String] data, Integer rendering_id) -> void + def id: -> Integer + def expiration_in_seconds: -> Integer + def make_jwt: -> String + end + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/auth/oidc_client_manager.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oidc_client_manager.rbs new file mode 100644 index 000000000..d0bc260f0 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/auth/oidc_client_manager.rbs @@ -0,0 +1,15 @@ +module ForestAdminAgent + module Auth + class OidcClientManager + TTL_CONFIG: Integer + + def make_forest_provider: -> untyped + + private + + def setup_cache: -> { client_id: String, issuer: String, redirect_uri: String } + def register: -> Array[String] + def render_provider: -> untyped + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/builder/agent_factory.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/builder/agent_factory.rbs new file mode 100644 index 000000000..400d5cd9d --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/builder/agent_factory.rbs @@ -0,0 +1,21 @@ +module ForestAdminAgent + module Builder + class AgentFactory + TTL_CONFIG: Integer + TTL_SCHEMA: Integer + @options: untyped + + attr_reader customizer: untyped + attr_reader container: untyped + attr_reader has_env_secret: untyped + def setup: (Array[string] options) -> untyped + def build: -> nil + + private + def send_schema: (?force: false) -> nil + def build_container: -> untyped + def build_cache: -> nil + def build_logger: -> untyped + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/facades/container.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/facades/container.rbs new file mode 100644 index 000000000..26c9fc4e8 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/facades/container.rbs @@ -0,0 +1,9 @@ +module ForestAdminAgent + module Facades + class Container + def self.instance: -> untyped + def self.config_from_cache: -> untyped + def self.get: (untyped key) -> untyped + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/http/router.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/http/router.rbs new file mode 100644 index 000000000..31d82c6f9 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/http/router.rbs @@ -0,0 +1,9 @@ +module ForestAdminAgent + module Http + class Router + def self.routes: -> Array[String] + def self.actions_routes: -> Array[String] + def self.api_charts_routes: -> Array[String] + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/routes/abstract_route.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/routes/abstract_route.rbs new file mode 100644 index 000000000..114e949e6 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/routes/abstract_route.rbs @@ -0,0 +1,12 @@ +module ForestAdminAgent + module Routes + class AbstractRoute + def initialize : -> void + def routes: -> {} + def add_route: (String, String,String, String) -> void + def setup: -> AbstractRoute + + def setup_routes: -> void + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/routes/security/authentication.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/routes/security/authentication.rbs new file mode 100644 index 000000000..356c0b739 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/routes/security/authentication.rbs @@ -0,0 +1,14 @@ +module ForestAdminAgent + module Routes + module Security + class Authentication + def setup_routes: -> Authentication + def handle_authentication: (?Hash[untyped, untyped] args) -> {content: {authorizationUrl: String}} + def handle_authentication_callback: (?Hash[untyped, untyped] args) -> {content: {token: String, tokenData: String}} + def handle_authentication_logout: (?Hash[untyped, untyped] _args) -> {content: nil, status: Integer} + def auth: -> ForestAdminAgent::Auth::AuthManager + def get_and_check_rendering_id: (Hash[untyped, untyped] params) -> Integer + end + end + end +end diff --git a/packages/forest_admin_agent/sig/forest_admin_agent/routes/system/health_check.rbs b/packages/forest_admin_agent/sig/forest_admin_agent/routes/system/health_check.rbs new file mode 100644 index 000000000..1cfd45286 --- /dev/null +++ b/packages/forest_admin_agent/sig/forest_admin_agent/routes/system/health_check.rbs @@ -0,0 +1,10 @@ +module ForestAdminAgent + module Routes + module System + class HealthCheck + def setup_routes: -> HealthCheck + def handle_request: (?Hash[untyped, untyped] _args) -> {content: nil, status: Integer} + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/auth_manager_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/auth_manager_spec.rb new file mode 100644 index 000000000..db1aafed4 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/auth_manager_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require 'singleton' + +module ForestAdminAgent + module Auth + describe AuthManager do + subject(:auth_manager) { described_class.new } + + let(:oidc) { instance_double(ForestAdminAgent::Auth::OidcClientManager) } + let(:forest_provider) { instance_double(ForestAdminAgent::Auth::OAuth2::ForestProvider) } + let(:forest_resource_owner) { instance_double(ForestAdminAgent::Auth::OAuth2::ForestResourceOwner) } + let(:access_token) { instance_double(OpenIDConnect::AccessToken) } + + before do + allow(ForestAdminAgent::Auth::OidcClientManager).to receive(:new).and_return(oidc) + allow(oidc).to receive(:make_forest_provider).with(any_args).and_return(forest_provider) + allow(forest_provider).to receive(:authorization_uri).with(any_args).and_return('https://api.development.forestadmin.com/oidc/...') + allow(forest_provider).to receive(:authorization_code=).with(any_args).and_return(nil) + allow(forest_provider).to receive(:access_token!).with(any_args).and_return(access_token) + allow(forest_provider).to receive(:get_resource_owner).with(access_token).and_return(forest_resource_owner) + allow(forest_resource_owner).to receive(:make_jwt).and_return('jwt') + end + + context 'when testing the AuthManager class' do + it 'returns an auth url on the start method' do + result = auth_manager.start 10 + expect(result).to eq 'https://api.development.forestadmin.com/oidc/...' + end + + it 'returns a token on the verify_code_and_generate_token method' do + result = auth_manager.verify_code_and_generate_token 'code' => 'abc', + 'state' => "{'renderingId': 10}" + expect(result).to eq 'jwt' + end + + it 'raises an error when the state is missing' do + expect do + auth_manager.verify_code_and_generate_token 'code' => 'abc' + end.to raise_error(ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_MISSING) + end + + it 'raises an error when the rendering state is missing' do + expect do + auth_manager.verify_code_and_generate_token 'code' => 'abc', + 'state' => "{'key': 'value'}" + end.to raise_error(ForestAdminAgent::Utils::ErrorMessages::INVALID_STATE_RENDERING_ID) + end + + it 'raises an error when the renderingId is not valid' do + expect do + auth_manager.verify_code_and_generate_token 'code' => 'abc', + 'state' => "{'renderingId': 'abc'}" + end.to raise_error(ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID) + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_provider_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_provider_spec.rb new file mode 100644 index 000000000..ae881fa47 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_provider_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'faraday' + +module ForestAdminAgent + module Auth + module OAuth2 + include ForestAdminAgent::Utils + describe ForestProvider do + let(:rendering_id) { 10 } + let(:attributes) do + { + identifier: 'identifier', + redirect_uri: 'redirect_uri', + host: 'host', + secret: 'secret' + } + end + let(:access_token) { instance_double(OpenIDConnect::AccessToken) } + + subject(:forest_provider) { described_class.new(rendering_id, attributes) } + + context 'when creating a new ForestProvider' do + it 'initializes the forest provider' do + expect(forest_provider.rendering_id).to eq rendering_id + expect(forest_provider.authorization_endpoint).to eq '/oidc/auth' + expect(forest_provider.token_endpoint).to eq '/oidc/token' + expect(forest_provider.userinfo_endpoint).to eq "/liana/v2/renderings/#{rendering_id}/authorization" + end + end + + context 'when getting the resource owner' do + before do + allow(forest_provider).to receive_messages(secret: 'secret') + allow(forest_provider).to receive_messages(check_response: { data: 'data' }) + allow(access_token).to receive_messages(access_token: 'access_token') + allow(access_token).to receive_messages(client: forest_provider) + end + + it 'returns the resource owner' do + result = forest_provider.get_resource_owner(access_token) + expect(result).to be_instance_of ForestAdminAgent::Auth::OAuth2::ForestResourceOwner + end + end + + context 'when check response is called' do + before do + allow(access_token).to receive_messages(access_token: 'access_token') + allow(access_token).to receive_messages(client: forest_provider) + allow(Faraday::Connection).to receive(:new).and_return(faraday_connection) + end + + context 'when a response return 200 status' do + let(:faraday_connection) { instance_double(Faraday::Connection) } + let(:response_ok) { instance_double(Faraday::Response, status: 200, body: { 'data' => 'data' }) } + let(:response_ok_with_error) do + instance_double( + Faraday::Response, + status: 200, + body: { 'errors' => [{ 'name' => ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED }] } + ) + end + + it 'returns the response body' do + allow(faraday_connection).to receive(:get).and_return(response_ok) + result = forest_provider.get_resource_owner(access_token) + expect(result).to be_instance_of ForestAdminAgent::Auth::OAuth2::ForestResourceOwner + end + + it 'raise an error when the body contains an error' do + allow(faraday_connection).to receive(:get).and_return(response_ok_with_error) + expect do + forest_provider.get_resource_owner(access_token) + end.to raise_error(ErrorMessages::TWO_FACTOR_AUTHENTICATION_REQUIRED) + end + end + + context 'when checking a response not 200 status' do + let(:faraday_connection) { instance_double(Faraday::Connection) } + let(:response_bad_request) { instance_double(Faraday::Response, status: 400) } + let(:response_unauthorized) { instance_double(Faraday::Response, status: 401) } + let(:response_not_found) { instance_double(Faraday::Response, status: 404) } + let(:response_unprocessable) { instance_double(Faraday::Response, status: 422) } + let(:response_internal_error) { instance_double(Faraday::Response, status: 500) } + + it 'raises an error when the response is 400' do + allow(faraday_connection).to receive(:get).and_return(response_bad_request) + expect { forest_provider.get_resource_owner(access_token) }.to raise_error(OpenIDConnect::BadRequest) + end + + it 'raises an error when the response is 401' do + allow(faraday_connection).to receive(:get).and_return(response_unauthorized) + expect { forest_provider.get_resource_owner(access_token) }.to raise_error(OpenIDConnect::Unauthorized) + end + + it 'raises an error when the response is 404' do + allow(faraday_connection).to receive(:get).and_return(response_not_found) + expect { forest_provider.get_resource_owner(access_token) }.to raise_error(OpenIDConnect::HttpError) + end + + it 'raises an error when the response is 422' do + allow(faraday_connection).to receive(:get).and_return(response_unprocessable) + expect { forest_provider.get_resource_owner(access_token) }.to raise_error(OpenIDConnect::HttpError) + end + + it 'raises an error when the response is 500' do + allow(faraday_connection).to receive(:get).and_return(response_internal_error) + expect { forest_provider.get_resource_owner(access_token) }.to raise_error(OpenIDConnect::HttpError) + end + end + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_resource_owner_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_resource_owner_spec.rb new file mode 100644 index 000000000..d74b3335a --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oauth2/forest_resource_owner_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' +require 'singleton' + +module ForestAdminAgent + module Auth + module OAuth2 + describe ForestResourceOwner do + subject(:forest_resource_owner) { described_class.new(data, rendering_id) } + + let(:rendering_id) { 10 } + let(:data) do + { + 'id' => 'id', + 'attributes' => { + 'email' => 'email', + 'first_name' => 'john', + 'last_name' => 'doe', + 'teams' => ['team'], + 'tags' => ['tag'], + 'permission_level' => 'permission_level' + } + } + end + + context 'when creating a new ForestResourceOwner' do + it 'initializes the forest resource owner' do + expect(forest_resource_owner.id).to eq 'id' + expect(forest_resource_owner.expiration_in_seconds).to eq (DateTime.now + (1 / 24.0)).to_time.to_i + end + + it 'makes a jwt' do + jwt = forest_resource_owner.make_jwt + decoded_jwt = JWT.decode jwt, Facades::Container.cache(:auth_secret), true, { algorithm: 'HS256' } + puts decoded_jwt + h = { + 'id' => 'id', + 'email' => 'email', + 'first_name' => 'john', + 'last_name' => 'doe', + 'team' => 'team', + 'tags' => ['tag'], + 'rendering_id' => 10, + 'exp' => forest_resource_owner.expiration_in_seconds, + 'permission_level' => 'permission_level' + } + expect(decoded_jwt[0]).to eq h + end + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb new file mode 100644 index 000000000..cb0ee9f4e --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/auth/oidc_client_manager_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +module ForestAdminAgent + module Auth + describe OidcClientManager do + subject(:oidc_client_manager) { described_class.new } + + let(:rendering_id) { 10 } + let(:oidc_discover_response) { instance_double(OpenIDConnect::Discovery::Provider::Config::Response) } + let(:oidc_discover_resource) { instance_double(OpenIDConnect::Discovery::Provider::Config::Resource) } + let(:faraday_connection) { instance_double(Faraday::Connection) } + + context 'when then oidc is called and forest api is down' do + it 'raises an error' do + class_double(ForestAdminAgent::Auth::OAuth2::OidcConfig, discover!: OpenIDConnect::Discovery::DiscoveryFailed) + expect do + oidc_client_manager.make_forest_provider :rendering_id + end.to raise_error(ForestAdminAgent::Utils::ErrorMessages::SERVER_DOWN) + end + end + + context 'when then oidc is called' do + let(:register) do + instance_double(Faraday::Response, body: { 'client_id' => 'client_id', 'redirect_uris' => ['redirect_uri'] }) + end + + before do + allow(OpenIDConnect::Discovery::Provider::Config::Response).to receive(:new) + .and_return(oidc_discover_response) + allow(oidc_discover_response).to receive_messages(:expected_issuer= => 'https://api.development.forestadmin.com') + allow(oidc_discover_response).to receive_messages(validate!: true) + allow(oidc_discover_response).to receive(:raw) + .and_return({ 'registration_endpoint' => 'https://api.development.forestadmin.com/oidc/reg' }) + allow(OpenIDConnect::Discovery::Provider::Config::Resource).to receive(:new) + .and_return(oidc_discover_resource) + allow(oidc_discover_resource).to receive(:discover!).and_return(oidc_discover_response) + allow(Faraday::Connection).to receive(:new).and_return(faraday_connection) + allow(faraday_connection).to receive(:post).and_return(register) + end + + it 'returns a forest provider on the make_forest_provider method' do + result = oidc_client_manager.make_forest_provider :rendering_id + expect(result).to be_a ForestAdminAgent::Auth::OAuth2::ForestProvider + end + + it 'setups the cache on the setup_cache method' do + cache_key = "#{Facades::Container.cache(:auth_secret)}-client-data" + config_agent = ForestAdminAgent::Facades::Container.config_from_cache + cache = oidc_client_manager.send(:setup_cache, cache_key, config_agent) + expect(cache).to be_a Hash + expect(cache.key?(:client_id)).to be true + expect(cache[:redirect_uri]).to eq 'redirect_uri' + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/security/authentication_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/security/authentication_spec.rb new file mode 100644 index 000000000..be59aa5ab --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/security/authentication_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' +require 'singleton' + +module ForestAdminAgent + module Routes + module Security + describe Authentication do + subject(:authentication) { described_class.new } + + context 'when setup the routes' do + it 'adds the route forest_authentication' do + authentication.setup_routes + expect(authentication.routes.include?('forest_authentication')).to be true + expect(authentication.routes.include?('forest_authentication-callback')).to be true + expect(authentication.routes.include?('forest_logout')).to be true + expect(authentication.routes.length).to eq 3 + end + end + + context 'when handle the authentication' do + let(:rendering_id) { '10' } + let(:user) do + { + 'id' => '1', + 'email' => 'john.doe@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'team' => 'Operations', + 'tags' => [ + { + 'key' => 'demo', + 'value' => '1234' + } + ], + 'rendering_id' => rendering_id, + 'exp' => (DateTime.now + (1 / 24.0)).to_time.to_i, + 'permission_level' => 'admin' + } + end + let(:token) { JWT.encode :user, ForestAdminAgent::Facades::Container.cache(:auth_secret), 'HS256' } + let(:auth_manager) { instance_double(ForestAdminAgent::Auth::AuthManager) } + + before do + allow(ForestAdminAgent::Auth::AuthManager).to receive(:new).and_return(auth_manager) + allow(auth_manager).to receive(:start).with(any_args).and_return('https://api.development.forestadmin.com/oidc/...') + allow(auth_manager).to receive(:verify_code_and_generate_token).with(any_args).and_return(token) + end + + it 'returns an auth url on the handle_authentication method' do + params = { 'renderingId' => rendering_id } + result = authentication.handle_authentication params + expect(result[:content][:authorizationUrl]).to eq 'https://api.development.forestadmin.com/oidc/...' + end + + it 'raises an error if renderingId is not present' do + params = {} + expect do + authentication.handle_authentication params + end.to raise_error(Error, + ForestAdminAgent::Utils::ErrorMessages::MISSING_RENDERING_ID) + end + + it 'raises an error if renderingId is not an integer' do + params = { 'renderingId' => 'abc' } + expect do + authentication.handle_authentication params + end.to raise_error(Error, + ForestAdminAgent::Utils::ErrorMessages::INVALID_RENDERING_ID) + end + + it 'returns a token on the handle_authentication_callback method' do + result = authentication.handle_authentication_callback 'code' => 'abc', + 'state' => "{'renderingId': #{rendering_id}}" + expect(result[:content][:token]).to eq token + expect(result[:content][:tokenData]).to eq JWT.decode( + token, + Facades::Container.cache(:auth_secret), + true, + { algorithm: 'HS256' } + )[0] + end + end + + context 'when handle the logout route' do + it 'returns a 204 status code' do + result = authentication.handle_authentication_logout + expect(result[:status]).to eq 204 + end + end + + it 'when handle auth it should return an AuthManager instance' do + expect(authentication.auth).to be_an_instance_of(ForestAdminAgent::Auth::AuthManager) + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/system/healthcheck_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/system/healthcheck_spec.rb new file mode 100644 index 000000000..815884071 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/system/healthcheck_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'singleton' + +module ForestAdminAgent + module Routes + module System + describe HealthCheck do + subject(:healthcheck) { described_class.new } + + context 'when testing the HealthCheck class' do + it 'returns an empty content and a 204 status' do + result = healthcheck.handle_request + expect(result[:content]).to be_nil + expect(result[:status]).to eq 204 + end + + it 'adds the route forest' do + healthcheck.setup_routes + expect(healthcheck.routes.include?('forest')).to be true + expect(healthcheck.routes.length).to eq 1 + end + end + end + end + end +end diff --git a/packages/forest_admin_agent/spec/spec_helper.rb b/packages/forest_admin_agent/spec/spec_helper.rb index 1166c0792..15d1fa68a 100644 --- a/packages/forest_admin_agent/spec/spec_helper.rb +++ b/packages/forest_admin_agent/spec/spec_helper.rb @@ -1,9 +1,14 @@ +require 'lightly' require 'simplecov' require 'simplecov_json_formatter' +require 'forest_admin_agent' + SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter SimpleCov.start do add_filter 'spec' + add_filter 'lib/forest_admin_agent/auth/oauth2/oidc_config.rb' end + # Previous content of test helper now starts here # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. @@ -21,6 +26,22 @@ # # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| + config.before do + lightly = Lightly.new + lightly.clear 'config' + + agent_factory = ForestAdminAgent::Builder::AgentFactory.instance + agent_factory.setup( + { + auth_secret: 'cba803d01a4d43b55010cab41fa1ea1f1f51a95e', + env_secret: '89719c6d8e2e2de2694c2f220fe2dbf02d5289487364daf1e4c6b13733ed0cdb', + is_production: false, + cache_dir: 'tmp/cache/forest_admin', + forest_server_url: 'https://api.development.forestadmin.com', + debug: true + } + ) + end # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. diff --git a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb index 57b7e251b..de1dec70c 100644 --- a/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb +++ b/packages/forest_admin_rails/app/controllers/forest_admin_rails/forest_controller.rb @@ -1,11 +1,12 @@ module ForestAdminRails class ForestController < ActionController::Base + skip_forgery_protection + def index - route_alias = request.routes.named_routes.helper_names.first.delete_suffix('_path') - if ForestAdminAgent::Http::Router.routes.key? route_alias - route = ForestAdminAgent::Http::Router.routes[route_alias] + if ForestAdminAgent::Http::Router.routes.key? params['route_alias'] + route = ForestAdminAgent::Http::Router.routes[params['route_alias']] - forest_response route[:closure] + forest_response route[:closure].call(params) else render json: { error: 'Route not found' }, status: 404 end diff --git a/packages/forest_admin_rails/config/routes.rb b/packages/forest_admin_rails/config/routes.rb index ecb2139ce..81600ce2c 100644 --- a/packages/forest_admin_rails/config/routes.rb +++ b/packages/forest_admin_rails/config/routes.rb @@ -1,5 +1,5 @@ ForestAdminRails::Engine.routes.draw do ForestAdminAgent::Http::Router.routes.each do |name, agent_route| - match agent_route[:uri], to: 'forest#index', via: agent_route[:method], as: name + match agent_route[:uri], to: 'forest#index', via: agent_route[:method], as: name, route_alias: name end end diff --git a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb index c4157d7e7..0372588e6 100644 --- a/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb +++ b/packages/forest_admin_rails/lib/forest_admin_rails/engine.rb @@ -1,4 +1,22 @@ require 'forest_admin_agent' +require 'rack/cors' + +module Rack + class Cors + class Resource + def to_preflight_headers(env) + h = to_headers(env) + if env['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'] == 'true' + h['Access-Control-Allow-Private-Network'] = 'true' + end + if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS] + h['Access-Control-Allow-Headers'] = env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS] + end + h + end + end + end +end module ForestAdminRails class Engine < ::Rails::Engine @@ -8,6 +26,7 @@ class Engine < ::Rails::Engine agent_factory = ForestAdminAgent::Builder::AgentFactory.instance agent_factory.setup(ForestAdminRails.config) load_configuration + load_cors end def load_configuration @@ -17,5 +36,17 @@ def load_configuration ForestAdminAgent::Builder::AgentFactory.instance.build end + + def load_cors + config.middleware.insert_before 0, Rack::Cors do + allow do + hostnames = [/\A.*\.forestadmin\.com\z/] + hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS'] + + origins hostnames + resource '*', headers: :any, methods: :any, credentials: true, max_age: 86_400 + end + end + end end end diff --git a/packages/forest_admin_rails/lib/generators/forest_admin_rails/install_generator.rb b/packages/forest_admin_rails/lib/generators/forest_admin_rails/install_generator.rb index 6b4d2397a..9db3f71ed 100644 --- a/packages/forest_admin_rails/lib/generators/forest_admin_rails/install_generator.rb +++ b/packages/forest_admin_rails/lib/generators/forest_admin_rails/install_generator.rb @@ -9,7 +9,7 @@ def install @env_secret = env_secret template 'initializers/config.rb', 'config/initializers/forest_admin_rails.rb' template 'forest_admin.rb', 'config/forest_admin.rb' - route "mount ForestAdminRails::Engine => '/forest'" + route "mount ForestAdminRails::Engine => '/#{ForestAdminRails.config[:prefix]}'" end end end