Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
69f6c90
fix: readme agent name
matthv Sep 13, 2023
7ee3b57
chore: manage route with parameters
matthv Sep 14, 2023
3128559
chore: add route_alias on routing
matthv Sep 15, 2023
b55546f
chore: add package openid_connect
matthv Sep 15, 2023
1c5ce0c
chore: route setup request & args on route
matthv Sep 15, 2023
417f2ba
chore: add utils error_messages
matthv Sep 15, 2023
2802dd9
chore: lint
matthv Sep 15, 2023
5d588e2
feat(auth): add logic to auth user before callback
matthv Sep 15, 2023
eeba9af
feat(cors): add cors support on the engine
matthv Sep 18, 2023
35dc260
fix: cors prefix
matthv Sep 18, 2023
3a4414b
chore: add new facade to container
matthv Sep 19, 2023
f2bbd15
feat(auth): set provider with resource owner
matthv Sep 19, 2023
223e2dc
feat(auth): add secret key on provider
matthv Sep 19, 2023
4c4362a
feat(auth): add response
matthv Sep 19, 2023
ebdf649
fix(facade): typo
matthv Sep 19, 2023
d9925e1
chore: typing added on auth classes
matthv Sep 19, 2023
66d2856
chore: update script run_rspec errors to English
matthv Sep 19, 2023
1a99463
feat(auth): add test on healthcheck
matthv Sep 21, 2023
63251ff
fix: test healthcheck
matthv Sep 21, 2023
a007c9f
chore: add tests on authentication
matthv Sep 21, 2023
79f0116
chore: auth manager call debug from container for implementing ssl_ve…
matthv Sep 22, 2023
fa060a3
chore: add tests on auth manager
matthv Sep 22, 2023
cb246d7
fix: remove useless comment
matthv Sep 22, 2023
6317b97
chore: add test on oidc client manager
matthv Sep 22, 2023
10b1842
chore: add sig on rendering_id forestProvider
matthv Sep 22, 2023
6389801
chore: remove oidc_config from coverage
matthv Sep 22, 2023
51ec4bd
chore: autoloader set inflection for oauth2 folder
matthv Sep 22, 2023
fec970c
fix: test on oidc client manager
matthv Sep 25, 2023
073595e
fix(auth): forest_provider exception call
matthv Sep 25, 2023
77549dd
chore: add test on forest_provider
matthv Sep 25, 2023
c097c2f
fix: test on oidc client manager and error when forest is down
matthv Sep 25, 2023
d447e8c
chore: add test on forest resource owner
matthv Sep 25, 2023
e0cb5a4
chore: test cache dir
matthv Sep 26, 2023
6322c59
fix: dir cache
matthv Sep 26, 2023
4e49bbe
refactor: oidc client manager
matthv Sep 26, 2023
1d2a2c9
chore: move config agent factory to spec_helper
matthv Sep 26, 2023
1d3b034
chore: remove test on rubocop
matthv Sep 26, 2023
c7edc0f
refactor: rename get method from facade container
matthv Sep 26, 2023
ffe4cd2
fix: test
matthv Sep 26, 2023
7280ab0
chore: update spec_helper
matthv Sep 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .rubocop.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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'
Expand DownExpand Up@@ -99,3 +114,12 @@ Style/RedundantConstantBase:

Layout/LineLength:
Max: 120

RSpec/ExampleLength:
Max: 20

RSpec/MultipleExpectations:
Max: 5

RSpec/MultipleMemoizedHelpers:
Max: 10
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
@@ -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.
Expand Down
8 changes: 4 additions & 4 deletions bin/run_rspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
3 changes: 3 additions & 0 deletions packages/forest_admin_agent/forest_admin_agent.gemspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
1 change: 1 addition & 0 deletions packages/forest_admin_agent/lib/forest_admin_agent.rb
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@
require 'zeitwerk'

loader = Zeitwerk::Loader.for_gem
loader.inflector.inflect('oauth2' => 'OAuth2')
loader.setup

module ForestAdminAgent
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
module ForestAdminAgent
module Routes
class AbstractRoute
attr_reader :request

def initialize
@request = ActionDispatch::Request.new({})
@routes = {}
setup_routes
end
Expand Down
Loading