osso 0.0.5.pre.iota → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +4 -2
  3. data/.rubocop.yml +4 -1
  4. data/Gemfile.lock +41 -23
  5. data/LICENSE +21 -23
  6. data/bin/annotate +3 -1
  7. data/db/schema.rb +41 -3
  8. data/lib/osso/db/migrate/20200929154117_add_users_count_to_identity_providers_and_enterprise_accounts.rb +6 -0
  9. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  10. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  11. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  12. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  13. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  14. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  15. data/lib/osso/error/account_configuration_error.rb +1 -0
  16. data/lib/osso/error/oauth_error.rb +6 -3
  17. data/lib/osso/graphql/mutation.rb +1 -0
  18. data/lib/osso/graphql/mutations.rb +1 -0
  19. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  20. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  21. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  22. data/lib/osso/graphql/query.rb +8 -0
  23. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +3 -3
  24. data/lib/osso/graphql/types.rb +2 -2
  25. data/lib/osso/graphql/types/admin_user.rb +9 -0
  26. data/lib/osso/graphql/types/base_object.rb +1 -1
  27. data/lib/osso/graphql/types/enterprise_account.rb +1 -0
  28. data/lib/osso/graphql/types/identity_provider.rb +2 -0
  29. data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
  30. data/lib/osso/lib/route_map.rb +0 -16
  31. data/lib/osso/lib/saml_handler.rb +5 -0
  32. data/lib/osso/models/access_token.rb +4 -2
  33. data/lib/osso/models/account.rb +34 -0
  34. data/lib/osso/models/authorization_code.rb +2 -1
  35. data/lib/osso/models/enterprise_account.rb +3 -1
  36. data/lib/osso/models/identity_provider.rb +18 -4
  37. data/lib/osso/models/models.rb +1 -0
  38. data/lib/osso/models/oauth_client.rb +0 -1
  39. data/lib/osso/models/user.rb +2 -2
  40. data/lib/osso/routes/admin.rb +39 -33
  41. data/lib/osso/routes/auth.rb +9 -9
  42. data/lib/osso/routes/oauth.rb +35 -17
  43. data/lib/osso/version.rb +1 -1
  44. data/lib/osso/views/admin.erb +5 -0
  45. data/lib/osso/views/error.erb +1 -0
  46. data/lib/osso/views/layout.erb +0 -0
  47. data/lib/osso/views/multiple_providers.erb +1 -0
  48. data/lib/osso/views/welcome.erb +0 -0
  49. data/lib/tasks/bootstrap.rake +18 -4
  50. data/osso-rb.gemspec +5 -0
  51. data/spec/factories/account.rb +24 -0
  52. data/spec/factories/enterprise_account.rb +11 -3
  53. data/spec/factories/identity_providers.rb +10 -2
  54. data/spec/factories/user.rb +4 -0
  55. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  56. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  57. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  58. data/spec/graphql/query/identity_provider_spec.rb +2 -2
  59. data/spec/models/enterprise_account_spec.rb +18 -0
  60. data/spec/models/identity_provider_spec.rb +24 -3
  61. data/spec/routes/admin_spec.rb +7 -41
  62. data/spec/routes/auth_spec.rb +17 -18
  63. data/spec/routes/oauth_spec.rb +88 -5
  64. data/spec/spec_helper.rb +3 -3
  65. data/spec/support/views/layout.erb +1 -0
  66. data/spec/support/views/multiple_providers.erb +1 -0
  67. metadata +91 -5
  68. data/spec/helpers/auth_spec.rb +0 -269
@@ -10,6 +10,7 @@ module Osso
10
10
  end
11
11
 
12
12
  require_relative 'access_token'
13
+ require_relative 'account'
13
14
  require_relative 'app_config'
14
15
  require_relative 'authorization_code'
15
16
  require_relative 'enterprise_account'
@@ -5,7 +5,6 @@ module Osso
5
5
  module Models
6
6
  class OauthClient < ActiveRecord::Base
7
7
  has_many :access_tokens
8
- has_many :enterprise_accounts
9
8
  has_many :refresh_tokens
10
9
  has_many :identity_providers
11
10
  has_many :redirect_uris
@@ -3,8 +3,8 @@
3
3
  module Osso
4
4
  module Models
5
5
  class User < ActiveRecord::Base
6
- belongs_to :enterprise_account
7
- belongs_to :identity_provider
6
+ belongs_to :enterprise_account, counter_cache: true
7
+ belongs_to :identity_provider, counter_cache: true
8
8
  has_many :authorization_codes, dependent: :delete_all
9
9
  has_many :access_tokens, dependent: :delete_all
10
10
 
@@ -1,53 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt'
3
+ require 'roda'
4
+ require 'sequel/core'
4
5
 
5
- module Osso
6
- class Admin < Sinatra::Base
7
- include AppConfig
8
- helpers Helpers::Auth
9
- register Sinatra::Namespace
6
+ DEFAULT_VIEWS_DIR = File.join(File.expand_path(Bundler.root), 'views/rodauth')
10
7
 
11
- before do
12
- chomp_token
8
+ module Osso
9
+ class Admin < Roda
10
+ DB = Sequel.postgres(extensions: :activerecord_connection)
11
+ use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
12
+
13
+ plugin :middleware
14
+ plugin :render, engine: 'erb', views: ENV['RODAUTH_VIEWS'] || DEFAULT_VIEWS_DIR
15
+ plugin :route_csrf
16
+
17
+ plugin :rodauth do
18
+ enable :login, :verify_account
19
+ verify_account_set_password? true
20
+ already_logged_in { redirect login_redirect }
21
+ use_database_authentication_functions? false
22
+
23
+ before_create_account_route do
24
+ request.halt unless DB[:accounts].empty?
25
+ end
13
26
  end
14
27
 
15
- namespace '/admin' do
16
- get '/login' do
17
- token_protected!
18
-
19
- erb :admin, layout: false
20
- end
28
+ alias erb render
21
29
 
22
- get '' do
23
- internal_protected!
30
+ route do |r|
31
+ r.rodauth
24
32
 
25
- erb :admin, layout: false
33
+ def current_account
34
+ Osso::Models::Account.find(rodauth.session['account_id']).
35
+ context.
36
+ merge({ rodauth: rodauth })
26
37
  end
27
38
 
28
- get '/enterprise' do
29
- token_protected!
30
-
39
+ r.on 'admin' do
40
+ rodauth.require_authentication
31
41
  erb :admin, layout: false
32
42
  end
33
43
 
34
- get '/enterprise/:domain' do
35
- enterprise_protected!(params[:domain])
44
+ r.post 'graphql' do
45
+ rodauth.require_authentication
36
46
 
37
- erb :admin, layout: false
38
- end
47
+ result = Osso::GraphQL::Schema.execute(
48
+ r.params['query'],
49
+ variables: r.params['variables'],
50
+ context: current_account,
51
+ )
39
52
 
40
- get '/config' do
41
- admin_protected!
42
-
43
- erb :admin, layout: false
53
+ result.to_json
44
54
  end
45
55
 
46
- get '/config/:id' do
47
- admin_protected!
48
-
49
- erb :admin, layout: false
50
- end
56
+ env['rodauth'] = rodauth
51
57
  end
52
58
  end
53
59
  end
@@ -13,7 +13,7 @@ module Osso
13
13
  UUID_REGEXP =
14
14
  /[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/.
15
15
  freeze
16
-
16
+
17
17
  use OmniAuth::Builder do
18
18
  OmniAuth::MultiProvider.register(
19
19
  self,
@@ -26,7 +26,8 @@ module Osso
26
26
  not_pending.
27
27
  find(identity_provider_id).
28
28
  saml_options
29
- rescue
29
+ rescue StandardError => e
30
+ Raven.capture_exception(e) if defined?(Raven)
30
31
  {}
31
32
  end
32
33
  end
@@ -38,21 +39,20 @@ module Osso
38
39
  namespace '/auth' do
39
40
  get '/failure' do
40
41
  # ??? invalid ticket, warden throws, ugh
41
-
42
+
42
43
  # confirmed:
43
- # - a valid but wrong cert will throw here
44
+ # - a valid but wrong cert will throw here
44
45
  # (OneLogin::RubySaml::ValidationError, Fingerprint mismatch)
45
46
  # but an _invalid_ cert is not caught. we do validate certs on
46
47
  # configuration, so this may be ok
47
48
  #
48
49
  # - a valid but wrong ACS URL will throw here. the urls
49
50
  # are pretty complex, but it has come up
50
- #
51
+ #
51
52
  # - specifying the wrong "recipient" in your IDP. Only OL so far
52
53
  # (OneLogin::RubySaml::ValidationError, The response was received
53
- # at vcardme.com instead of
54
- # http://localhost:9292/auth/saml/e54a9a92-b4b5-4ea5-b0e3-b1423eb20b76/callback)
55
-
54
+ # at vcardme.com instead of
55
+ # http://localhost:9292/auth/saml/e54a9a92-b4b5-4ea5-b0e3-b1423eb20b76/callback)
56
56
 
57
57
  @error = Osso::Error::SamlConfigError.new
58
58
  erb :error
@@ -73,7 +73,7 @@ module Osso
73
73
  rescue Osso::Error::Base => e
74
74
  @error = e
75
75
  erb :error
76
- end
76
+ end
77
77
  end
78
78
  end
79
79
  end
@@ -16,15 +16,14 @@ module Osso
16
16
  # Once they complete IdP login, they will be returned to the
17
17
  # redirect_uri with an authorization code parameter.
18
18
  get '/authorize' do
19
- client = find_client(params[:client_id])
20
- enterprise = find_account(domain: params[:domain], client_id: client.id)
19
+ identity_providers = find_providers
21
20
 
22
21
  validate_oauth_request(env)
23
22
 
24
- redirect "/auth/saml/#{enterprise.provider.id}" if enterprise.single_provider?
23
+ redirect "/auth/saml/#{identity_providers.first.id}" if identity_providers.one?
25
24
 
26
- @providers = enterprise.identity_providers.not_pending
27
- erb :multiple_providers if @providers.count > 1
25
+ @providers = identity_providers.not_pending
26
+ return erb :multiple_providers if @providers.count > 1
28
27
 
29
28
  raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
30
29
  rescue Osso::Error::Base => e
@@ -38,11 +37,12 @@ module Osso
38
37
  # and client secret
39
38
  post '/token' do
40
39
  Rack::OAuth2::Server::Token.new do |req, res|
41
- code = Models::AuthorizationCode.
42
- find_by_token!(params[:code])
43
40
  client = Models::OauthClient.find_by!(identifier: req.client_id)
44
41
  req.invalid_client! if client.secret != req.client_secret
42
+
43
+ code = Models::AuthorizationCode.find_by_token!(params[:code])
45
44
  req.invalid_grant! if code.redirect_uri != req.redirect_uri
45
+
46
46
  res.access_token = code.access_token.to_bearer_token
47
47
  end.call(env)
48
48
  end
@@ -50,22 +50,35 @@ module Osso
50
50
  # Use the access token to request a profile for the user who
51
51
  # just logged in. Access tokens are short-lived.
52
52
  get '/me' do
53
- json Models::AccessToken.
53
+ token = Models::AccessToken.
54
54
  includes(:user).
55
55
  valid.
56
- find_by_token!(params[:access_token]).
57
- user
56
+ find_by_token!(access_token)
57
+
58
+ json token.user.as_json.merge(requested: token.requested)
58
59
  end
59
60
  end
60
61
 
61
62
  private
62
63
 
63
- def find_account(domain:, client_id:)
64
- Models::EnterpriseAccount.
65
- includes(:identity_providers).
66
- find_by!(domain: domain, oauth_client_id: client_id)
67
- rescue ActiveRecord::RecordNotFound
68
- raise Osso::Error::NoAccountForOAuthClientError.new(domain: params[:domain])
64
+ def find_providers
65
+ if params[:email]
66
+ user = Osso::Models::User.
67
+ includes(:identity_provider).
68
+ find_by(email: params[:email])
69
+ return [user.identity_provider] if user
70
+ end
71
+
72
+ Osso::Models::IdentityProvider.
73
+ joins(:oauth_client).
74
+ where(
75
+ domain: domain_from_params,
76
+ oauth_clients: { identifier: params[:client_id] },
77
+ )
78
+ end
79
+
80
+ def domain_from_params
81
+ params[:domain] || params[:email].split('@')[1]
69
82
  end
70
83
 
71
84
  def find_client(identifier)
@@ -74,14 +87,19 @@ module Osso
74
87
  raise Osso::Error::InvalidOAuthClientIdentifier
75
88
  end
76
89
 
77
- def validate_oauth_request(env)
90
+ def validate_oauth_request(env) # rubocop:disable Metrics/AbcSize
78
91
  Rack::OAuth2::Server::Authorize.new do |req, _res|
79
92
  client = find_client(req[:client_id])
80
93
  session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
81
94
  session[:osso_oauth_state] = params[:state]
95
+ session[:osso_oauth_requested] = { domain: req[:domain], email: req[:email] }
82
96
  end.call(env)
83
97
  rescue Rack::OAuth2::Server::Authorize::BadRequest
84
98
  raise Osso::Error::InvalidRedirectUri.new(redirect_uri: params[:redirect_uri])
85
99
  end
100
+
101
+ def access_token
102
+ params[:access_token] || env.fetch('HTTP_AUTHORIZATION', '').slice(-64..-1)
103
+ end
86
104
  end
87
105
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Osso
4
- VERSION = '0.0.5-iota'
4
+ VERSION = '0.0.5'
5
5
  end
@@ -0,0 +1,5 @@
1
+ <%#
2
+ NB: this file exists so that the admin routes have something to render in spec.
3
+ In real-world usage, those routes render an index.html file that includes the
4
+ React app.
5
+ %>
@@ -0,0 +1 @@
1
+ <%= @error %>
File without changes
@@ -0,0 +1 @@
1
+ MULITPLE PROVIDERS
File without changes
@@ -7,12 +7,26 @@ require 'osso'
7
7
  namespace :osso do
8
8
  desc 'Bootstrap Osso data for a deployment'
9
9
  task :bootstrap do
10
- %w[Production Staging Development].each do |environement|
10
+ %w[Production Staging Development].each do |environment|
11
11
  Osso::Models::OauthClient.create!(
12
- name: environement,
13
- )
12
+ name: environment,
13
+ ) unless Osso::Models::OauthClient.find_by_name(environment)
14
14
  end
15
15
 
16
- Osso::Models::AppConfig.create!
16
+ Osso::Models::AppConfig.create
17
+
18
+ admin_email = ENV['ADMIN_EMAIL']
19
+
20
+ if admin_email
21
+ admin = Osso::Models::Account.create!(
22
+ email: admin_email,
23
+ status_id: 1,
24
+ role: 'admin',
25
+ )
26
+
27
+ rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({}))
28
+ account = rodauth.account_from_login(admin_email)
29
+ rodauth.setup_account_verification
30
+ end
17
31
  end
18
32
  end
@@ -16,14 +16,19 @@ Gem::Specification.new do |spec|
16
16
  spec.license = 'MIT'
17
17
 
18
18
  spec.add_runtime_dependency 'activesupport', '>= 6.0.3.2'
19
+ spec.add_runtime_dependency 'bcrypt', '~> 3.1.13'
19
20
  spec.add_runtime_dependency 'graphql'
20
21
  spec.add_runtime_dependency 'jwt'
22
+ spec.add_runtime_dependency 'mail', '~> 2.7.1'
21
23
  spec.add_runtime_dependency 'omniauth-multi-provider'
22
24
  spec.add_runtime_dependency 'omniauth-saml'
23
25
  spec.add_runtime_dependency 'rack', '>= 2.1.4'
24
26
  spec.add_runtime_dependency 'rack-contrib'
25
27
  spec.add_runtime_dependency 'rack-oauth2'
26
28
  spec.add_runtime_dependency 'rake'
29
+ spec.add_runtime_dependency 'rodauth', '~> 2.5.0'
30
+ spec.add_runtime_dependency 'sequel', '~> 5.37.0'
31
+ spec.add_runtime_dependency 'sequel-activerecord_connection', '~> 0.3'
27
32
  spec.add_runtime_dependency 'sinatra'
28
33
  spec.add_runtime_dependency 'sinatra-activerecord'
29
34
  spec.add_runtime_dependency 'sinatra-contrib'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB = Sequel.postgres(extensions: :activerecord_connection)
4
+
5
+ FactoryBot.define do
6
+ factory :account, class: Osso::Models::Account do
7
+ id { SecureRandom.uuid }
8
+ email { Faker::Internet.email }
9
+ end
10
+
11
+ factory :verified_account, parent: :account do
12
+ transient do
13
+ password { SecureRandom.urlsafe_base64(8) }
14
+ end
15
+ status_id { 2 }
16
+
17
+ after :create do |account|
18
+ DB[:account_password_hashes].insert(
19
+ id: account.id,
20
+ password_hash: BCrypt::Password.create('secret', cost: BCrypt::Engine::MIN_COST).to_s,
21
+ )
22
+ end
23
+ end
24
+ end
@@ -2,19 +2,22 @@
2
2
 
3
3
  FactoryBot.define do
4
4
  factory :enterprise_account, class: Osso::Models::EnterpriseAccount do
5
+ transient do
6
+ oauth_client { create(:oauth_client) }
7
+ end
5
8
  id { SecureRandom.uuid }
6
9
  name { Faker::Company.name }
7
10
  domain { Faker::Internet.domain_name }
8
- oauth_client
9
11
  end
10
12
 
11
13
  factory :enterprise_with_okta, parent: :enterprise_account do
12
- after :create do |enterprise|
14
+ after :create do |enterprise, evaluator|
13
15
  create(
14
16
  :configured_identity_provider,
15
17
  service: 'OKTA',
16
18
  domain: enterprise.domain,
17
19
  enterprise_account_id: enterprise.id,
20
+ oauth_client: evaluator.oauth_client,
18
21
  )
19
22
  end
20
23
  end
@@ -31,12 +34,16 @@ FactoryBot.define do
31
34
  end
32
35
 
33
36
  factory :enterprise_with_multiple_providers, parent: :enterprise_account do
34
- after :create do |enterprise|
37
+ transient do
38
+ oauth_client { nil }
39
+ end
40
+ after :create do |enterprise, evaluator|
35
41
  create(
36
42
  :configured_identity_provider,
37
43
  service: 'OKTA',
38
44
  domain: enterprise.domain,
39
45
  enterprise_account_id: enterprise.id,
46
+ oauth_client: evaluator.oauth_client,
40
47
  )
41
48
 
42
49
  create(
@@ -44,6 +51,7 @@ FactoryBot.define do
44
51
  service: 'AZURE',
45
52
  domain: enterprise.domain,
46
53
  enterprise_account_id: enterprise.id,
54
+ oauth_client: evaluator.oauth_client,
47
55
  )
48
56
  end
49
57
  end
@@ -21,6 +21,13 @@ FactoryBot.define do
21
21
  end
22
22
  end
23
23
 
24
+ factory :ping_identity_provider, parent: :identity_provider do
25
+ service { 'PING' }
26
+ sso_url do
27
+ 'https://auth.pingone.com/42cd503f-f0ba-47c7-a5b5-e69e9d8fab47/saml20/idp/sso'
28
+ end
29
+ end
30
+
24
31
  factory :configured_identity_provider, parent: :identity_provider do
25
32
  status { 'CONFIGURED' }
26
33
  sso_cert do
@@ -55,15 +62,16 @@ end
55
62
  # Table name: identity_providers
56
63
  #
57
64
  # id :uuid not null, primary key
58
- # service :string
65
+ # service :enum
59
66
  # domain :string not null
60
67
  # sso_url :string
61
68
  # sso_cert :text
62
69
  # enterprise_account_id :uuid
63
70
  # oauth_client_id :uuid
64
- # status :enum default("PENDING")
71
+ # status :enum default("pending")
65
72
  # created_at :datetime
66
73
  # updated_at :datetime
74
+ # users_count :integer default(0)
67
75
  #
68
76
  # Indexes
69
77
  #