From c970d175823607fb0fbc40eea9173ffa1c84bb38 Mon Sep 17 00:00:00 2001 From: Sam Bauch Date: Fri, 12 Feb 2021 12:54:52 -0500 Subject: [PATCH 1/5] OneLogin create user --- Gemfile.lock | 16 ++++- lib/osso.rb | 2 + lib/osso/lib/route_map.rb | 1 + lib/osso/lib/scim_query_parser.rb | 57 +++++++++++++++ lib/osso/lib/scim_schemas.rb | 17 +++++ lib/osso/models/identity_provider.rb | 11 +++ lib/osso/routes/routes.rb | 1 + lib/osso/routes/scim.rb | 104 +++++++++++++++++++++++++++ osso-rb.gemspec | 1 + 9 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 lib/osso/lib/scim_query_parser.rb create mode 100644 lib/osso/lib/scim_schemas.rb create mode 100644 lib/osso/routes/scim.rb diff --git a/Gemfile.lock b/Gemfile.lock index d0ea65c..4e76274 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,6 +16,7 @@ PATH rack-protection (~> 2.1.0) rake rodauth (~> 2.9) + scim-kit (~> 0.5.1) sequel (~> 5.40) sequel-activerecord_connection (>= 0.3, < 2.0) sinatra @@ -68,6 +69,8 @@ GEM httpclient (2.8.3) i18n (1.8.8) concurrent-ruby (~> 1.0) + jbuilder (2.11.2) + activesupport (>= 5.0.0) json-jwt (1.13.0) activesupport (>= 4.2) aes_key_wrap @@ -82,10 +85,11 @@ GEM multi_json (1.15.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) + net-hippie (0.3.2) nokogiri (1.11.1) mini_portile2 (~> 2.5.0) racc (~> 1.4) - omniauth (2.0.1) + omniauth (2.0.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) rack-protection @@ -97,6 +101,7 @@ GEM parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) + parslet (1.8.2) pg (1.2.3) posthog-ruby (1.1.0) pry (0.14.0) @@ -154,6 +159,12 @@ GEM ruby-saml (1.11.0) nokogiri (>= 1.5.10) ruby2_keywords (0.0.4) + scim-kit (0.5.1) + activemodel (>= 5.2.0) + net-hippie (~> 0.3) + parslet (~> 1.8) + tilt (~> 2.0) + tilt-jbuilder (~> 0.7) sequel (5.41.0) sequel-activerecord_connection (1.2.2) activerecord (>= 4.2, < 7) @@ -180,6 +191,9 @@ GEM sinatra (= 2.1.0) tilt (~> 2.0) tilt (2.0.10) + tilt-jbuilder (0.7.1) + jbuilder + tilt (>= 1.3.0, < 3) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (2.0.0) diff --git a/lib/osso.rb b/lib/osso.rb index f2557a0..d67c7f7 100644 --- a/lib/osso.rb +++ b/lib/osso.rb @@ -7,6 +7,8 @@ module Osso require_relative 'osso/lib/oauth2_token' require_relative 'osso/lib/route_map' require_relative 'osso/lib/saml_handler' + require_relative 'osso/lib/scim_query_parser' + require_relative 'osso/lib/scim_schemas' require_relative 'osso/models/models' require_relative 'osso/routes/routes' require_relative 'osso/graphql/schema' diff --git a/lib/osso/lib/route_map.rb b/lib/osso/lib/route_map.rb index 0b65442..989bb0d 100644 --- a/lib/osso/lib/route_map.rb +++ b/lib/osso/lib/route_map.rb @@ -9,6 +9,7 @@ def self.included(klass) use Osso::Admin use Osso::Auth use Osso::Oauth + use Osso::Scim end end end diff --git a/lib/osso/lib/scim_query_parser.rb b/lib/osso/lib/scim_query_parser.rb new file mode 100644 index 0000000..cf53e3b --- /dev/null +++ b/lib/osso/lib/scim_query_parser.rb @@ -0,0 +1,57 @@ +module Osso + class ScimQueryParser + attr_accessor :query_elements + + def self.perform(qs) + new(qs).raw_sql + end + + def initialize(query_string) + self.query_elements = query_string.split(" ") + end + + def raw_sql + "#{attribute} #{comparator} #{parameter}" + end + + def attribute + attribute = query_elements.dig(0) + raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank? + attribute = attribute.to_sym + + mapped_attribute = attribute_mapping(attribute) + raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank? + ActiveRecord::Base.connection.quote(mapped_attribute) + end + + def comparator + sql_comparator(query_elements.dig(1)) + end + + def parameter + parameter = query_elements[2..-1].join(" ") + return if parameter.blank? + ActiveRecord::Base.connection.quote(parameter.gsub(/"/, "")) + end + + private + + def attribute_mapping(attribute) + { + userName: :email + }[attribute] + end + + def sql_comparator(element) + case element + when "eq" + "=" + when "sw" + "ILIKE %" + else + # TODO: implement additional query filters + raise ScimRails::ExceptionHandler::InvalidQuery + end + end + end +end \ No newline at end of file diff --git a/lib/osso/lib/scim_schemas.rb b/lib/osso/lib/scim_schemas.rb new file mode 100644 index 0000000..97173ff --- /dev/null +++ b/lib/osso/lib/scim_schemas.rb @@ -0,0 +1,17 @@ +require 'scim-kit' + +module Osso + module ScimSchema + SCHEMA_URI = 'urn:scim:osso:default:schema' + def user_schema + { + schemas: ["urn:scim:schemas:core:1.0", SCHEMA_URI], + userName: "{$user.email}", + SCHEMA_URI: { + idp_id: "{$user.id}", + email: "{$user.email}" + } + }.to_json + end + end +end \ No newline at end of file diff --git a/lib/osso/models/identity_provider.rb b/lib/osso/models/identity_provider.rb index 582a099..fd6b7d3 100644 --- a/lib/osso/models/identity_provider.rb +++ b/lib/osso/models/identity_provider.rb @@ -4,6 +4,7 @@ module Osso module Models # Base class for SAML Providers class IdentityProvider < ActiveRecord::Base + include ScimSchema belongs_to :enterprise_account belongs_to :oauth_client has_many :users, dependent: :delete_all @@ -50,6 +51,16 @@ def acs_url_validator Regexp.escape(acs_url) end + def bearer_token + payload = { + id: id, + domain: domain, + } + + token = JWT.encode(payload, ENV['SESSION_SECRET'], 'HS256') + Base64.urlsafe_encode64(token) + end + def set_status self.status = 'configured' if sso_url && sso_cert && pending? end diff --git a/lib/osso/routes/routes.rb b/lib/osso/routes/routes.rb index c832a39..695c794 100755 --- a/lib/osso/routes/routes.rb +++ b/lib/osso/routes/routes.rb @@ -8,3 +8,4 @@ require_relative 'admin' require_relative 'auth' require_relative 'oauth' +require_relative 'scim' diff --git a/lib/osso/routes/scim.rb b/lib/osso/routes/scim.rb new file mode 100644 index 0000000..c3f9089 --- /dev/null +++ b/lib/osso/routes/scim.rb @@ -0,0 +1,104 @@ +module Osso + class Scim < Sinatra::Base + include AppConfig + register Sinatra::Namespace + + before do + error 401 unless authorized? + end + + namespace '/scim/v2' do + get '/Users' do + users = Models::User + users = users.where(identity_provider: current_identity_provider) + users = users.where(ScimQueryParser.perform(params['filter'])) if params['filter'] + + json scim_response(count: users.count, resources: users.first(10)) + end + + post '/Users' do + user = Models::User.create( + params[ScimSchema::SCHEMA_URI].symbolize_keys.merge( + identity_provider: current_identity_provider + ) + ) + + # send webhook to app + + json created_scim_response(user) + end + + get 'Users/:id' do + + end + + put 'Users/:id' do + + end + + patch 'Users/:id' do + + end + + get 'ServiceProviderConfig' do + + end + + get 'ResourceTypes' do + + end + + get 'Schemas' do + + end + + post 'Bulk' do + + end + end + + private + + def scim_response(count: 0, resources: []) + { + "totalResults": count, + "itemsPerPage":10, + "startIndex":1, + "schemas":[ + "urn:scim:schemas:core:1.0", + ScimSchema::SCHEMA_URI, + ], + "Resources": resources, + } + end + + def created_scim_response(user) + { + "schemas":[ + "urn:scim:schemas:core:1.0", + ScimSchema::SCHEMA_URI, + ], + "id": user.id, + "userName": user.email, + } + end + + def authorized? + true if current_identity_provider + end + + def current_identity_provider + return @current_identity_provider if defined?(@current_identity_provider) + + jwt = Base64.urlsafe_decode64(bearer_token) + payload = JWT.decode(jwt, ENV['SESSION_SECRET'], 'HS256')[0] + @current_identity_provider = Models::IdentityProvider.find_by(payload) + end + + def bearer_token + pattern = /^Bearer / + header = request.env["HTTP_AUTHORIZATION"] + header.gsub(pattern, '') if header&.match(pattern) + end + end +end diff --git a/osso-rb.gemspec b/osso-rb.gemspec index 3aee9b4..29cd8cf 100644 --- a/osso-rb.gemspec +++ b/osso-rb.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'rack-protection', '~> 2.1.0' spec.add_runtime_dependency 'rake' spec.add_runtime_dependency 'rodauth', '~> 2.9' + spec.add_runtime_dependency 'scim-kit', '~> 0.5.1' spec.add_runtime_dependency 'sequel', '~> 5.40' spec.add_runtime_dependency 'sequel-activerecord_connection', '>= 0.3', '< 2.0' spec.add_runtime_dependency 'sinatra' From 63b969703290206a3786337a7ae31c97884e0feb Mon Sep 17 00:00:00 2001 From: Sam Bauch Date: Fri, 12 Feb 2021 18:33:31 -0500 Subject: [PATCH 2/5] okta spike --- lib/osso/models/user.rb | 16 +++++++++++ lib/osso/routes/scim.rb | 61 ++++++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/lib/osso/models/user.rb b/lib/osso/models/user.rb index a8d3716..c4e86a8 100644 --- a/lib/osso/models/user.rb +++ b/lib/osso/models/user.rb @@ -19,6 +19,22 @@ def as_json(*) idp: identity_provider.name, } end + + def as_scim_json(*) + { + id: id, + email: email, + name: { + familyName: 'familyName', + givenName: 'givenName', + }, + userName: email, + active: true, + emails: [ + { value: email, primary: true} + ] + } + end end end end diff --git a/lib/osso/routes/scim.rb b/lib/osso/routes/scim.rb index c3f9089..dd93d0e 100644 --- a/lib/osso/routes/scim.rb +++ b/lib/osso/routes/scim.rb @@ -13,30 +13,58 @@ class Scim < Sinatra::Base users = users.where(identity_provider: current_identity_provider) users = users.where(ScimQueryParser.perform(params['filter'])) if params['filter'] - json scim_response(count: users.count, resources: users.first(10)) + json list_scim_response(count: users.count, resources: users.first(10)) end post '/Users' do + # OneLogin takes our custom schema and passes email and idp_id + # + # user = Models::User.create( + # params[ScimSchema::SCHEMA_URI].merge( + # identity_provider: current_identity_provider + # ) + # ) + + # Okta does not support providing a custom schema template, but + # we can instruct Okta users to map attributes user = Models::User.create( - params[ScimSchema::SCHEMA_URI].symbolize_keys.merge( - identity_provider: current_identity_provider - ) + email: params[:userName], + idp_id: params[:userName], + identity_provider: current_identity_provider, ) # send webhook to app + + status 201 + json user_scim_response(user) + rescue ActiveRecord::RecordNotUnique + status 409 + end + + get '/Users/:id' do + user = Models::User.find(params[:id]) + + json user_scim_response(user) + rescue + status 404 - json created_scim_response(user) + json ({ + detail: "No user for ID", + schemas: [ + 'urn:ietf:params:scim:api:messages:2.0:Error' # Okta + ] + }) end - get 'Users/:id' do + put '/Users/:id' do end - put 'Users/:id' do + patch '/Users/:id' do end - patch 'Users/:id' do + get '/Groups' do end @@ -59,28 +87,29 @@ class Scim < Sinatra::Base private - def scim_response(count: 0, resources: []) + def list_scim_response(count: 0, resources: []) + # TODO: seems this needs to be paginated, but a bit unclear { "totalResults": count, "itemsPerPage":10, "startIndex":1, "schemas":[ "urn:scim:schemas:core:1.0", - ScimSchema::SCHEMA_URI, + 'urn:ietf:params:scim:api:messages:2.0:ListResponse', # Okta required + ScimSchema::SCHEMA_URI ], - "Resources": resources, + "Resources": resources.map(&:as_scim_json), } end - def created_scim_response(user) + def user_scim_response(user) { "schemas":[ - "urn:scim:schemas:core:1.0", + "urn:scim:schemas:core:2.0", ScimSchema::SCHEMA_URI, + "urn:ietf:params:scim:schemas:core:2.0:User" # okta ], - "id": user.id, - "userName": user.email, - } + }.merge(user.as_scim_json) end def authorized? From a12793fdd0469529207bf9499d0d10b50123bb56 Mon Sep 17 00:00:00 2001 From: Sam Bauch Date: Fri, 12 Feb 2021 18:44:38 -0500 Subject: [PATCH 3/5] move before action into namespace --- lib/osso/routes/scim.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/osso/routes/scim.rb b/lib/osso/routes/scim.rb index dd93d0e..f28167a 100644 --- a/lib/osso/routes/scim.rb +++ b/lib/osso/routes/scim.rb @@ -3,11 +3,11 @@ class Scim < Sinatra::Base include AppConfig register Sinatra::Namespace - before do - error 401 unless authorized? - end - namespace '/scim/v2' do + before do + error 401 unless authorized? + end + get '/Users' do users = Models::User users = users.where(identity_provider: current_identity_provider) From efe9b0cb384cb47ad0c6961169c4338c66515e13 Mon Sep 17 00:00:00 2001 From: Sam Bauch Date: Fri, 12 Feb 2021 18:48:46 -0500 Subject: [PATCH 4/5] query parser --- Gemfile.lock | 14 -------------- lib/osso/lib/scim_query_parser.rb | 6 +++++- lib/osso/lib/scim_schemas.rb | 2 -- osso-rb.gemspec | 1 - 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4e76274..06bb91d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,7 +16,6 @@ PATH rack-protection (~> 2.1.0) rake rodauth (~> 2.9) - scim-kit (~> 0.5.1) sequel (~> 5.40) sequel-activerecord_connection (>= 0.3, < 2.0) sinatra @@ -69,8 +68,6 @@ GEM httpclient (2.8.3) i18n (1.8.8) concurrent-ruby (~> 1.0) - jbuilder (2.11.2) - activesupport (>= 5.0.0) json-jwt (1.13.0) activesupport (>= 4.2) aes_key_wrap @@ -85,7 +82,6 @@ GEM multi_json (1.15.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - net-hippie (0.3.2) nokogiri (1.11.1) mini_portile2 (~> 2.5.0) racc (~> 1.4) @@ -101,7 +97,6 @@ GEM parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) - parslet (1.8.2) pg (1.2.3) posthog-ruby (1.1.0) pry (0.14.0) @@ -159,12 +154,6 @@ GEM ruby-saml (1.11.0) nokogiri (>= 1.5.10) ruby2_keywords (0.0.4) - scim-kit (0.5.1) - activemodel (>= 5.2.0) - net-hippie (~> 0.3) - parslet (~> 1.8) - tilt (~> 2.0) - tilt-jbuilder (~> 0.7) sequel (5.41.0) sequel-activerecord_connection (1.2.2) activerecord (>= 4.2, < 7) @@ -191,9 +180,6 @@ GEM sinatra (= 2.1.0) tilt (~> 2.0) tilt (2.0.10) - tilt-jbuilder (0.7.1) - jbuilder - tilt (>= 1.3.0, < 3) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (2.0.0) diff --git a/lib/osso/lib/scim_query_parser.rb b/lib/osso/lib/scim_query_parser.rb index cf53e3b..3bd4f99 100644 --- a/lib/osso/lib/scim_query_parser.rb +++ b/lib/osso/lib/scim_query_parser.rb @@ -1,5 +1,7 @@ module Osso class ScimQueryParser + # TODO: cribbed this from an open source rails SCIM gem. + # stylistically not quite what I love attr_accessor :query_elements def self.perform(qs) @@ -50,7 +52,9 @@ def sql_comparator(element) "ILIKE %" else # TODO: implement additional query filters - raise ScimRails::ExceptionHandler::InvalidQuery + # and also this is not the right error class but + # whatever for now + raise NotImplementedError end end end diff --git a/lib/osso/lib/scim_schemas.rb b/lib/osso/lib/scim_schemas.rb index 97173ff..8d31cc1 100644 --- a/lib/osso/lib/scim_schemas.rb +++ b/lib/osso/lib/scim_schemas.rb @@ -1,5 +1,3 @@ -require 'scim-kit' - module Osso module ScimSchema SCHEMA_URI = 'urn:scim:osso:default:schema' diff --git a/osso-rb.gemspec b/osso-rb.gemspec index 29cd8cf..3aee9b4 100644 --- a/osso-rb.gemspec +++ b/osso-rb.gemspec @@ -29,7 +29,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'rack-protection', '~> 2.1.0' spec.add_runtime_dependency 'rake' spec.add_runtime_dependency 'rodauth', '~> 2.9' - spec.add_runtime_dependency 'scim-kit', '~> 0.5.1' spec.add_runtime_dependency 'sequel', '~> 5.40' spec.add_runtime_dependency 'sequel-activerecord_connection', '>= 0.3', '< 2.0' spec.add_runtime_dependency 'sinatra' From 47d351682bc29da5c6d0f8bc56c1f4a629c10ad7 Mon Sep 17 00:00:00 2001 From: Sam Bauch Date: Fri, 12 Feb 2021 19:07:13 -0500 Subject: [PATCH 5/5] self review --- lib/osso/lib/scim_query_parser.rb | 12 +++++++++--- lib/osso/models/identity_provider.rb | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/osso/lib/scim_query_parser.rb b/lib/osso/lib/scim_query_parser.rb index 3bd4f99..db2b727 100644 --- a/lib/osso/lib/scim_query_parser.rb +++ b/lib/osso/lib/scim_query_parser.rb @@ -1,7 +1,13 @@ module Osso class ScimQueryParser # TODO: cribbed this from an open source rails SCIM gem. - # stylistically not quite what I love + # stylistically not quite what I love. + # Anyhow, SCIM decided to go and create their own query language 🤷‍♂️ + # So we need to take the filter param and convert it into something we + # can use to query the DB. Maybe we should be explicit about mapping + # attributes and values to avoid SQL injection but like, if Okta + # is sending authneticated requests with SQL injection I think we + # have bigger problems?? attr_accessor :query_elements def self.perform(qs) @@ -18,11 +24,11 @@ def raw_sql def attribute attribute = query_elements.dig(0) - raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank? + raise StandardError if attribute.blank? attribute = attribute.to_sym mapped_attribute = attribute_mapping(attribute) - raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank? + raise StandardError if mapped_attribute.blank? ActiveRecord::Base.connection.quote(mapped_attribute) end diff --git a/lib/osso/models/identity_provider.rb b/lib/osso/models/identity_provider.rb index fd6b7d3..5f57f27 100644 --- a/lib/osso/models/identity_provider.rb +++ b/lib/osso/models/identity_provider.rb @@ -4,7 +4,6 @@ module Osso module Models # Base class for SAML Providers class IdentityProvider < ActiveRecord::Base - include ScimSchema belongs_to :enterprise_account belongs_to :oauth_client has_many :users, dependent: :delete_all