osso 0.0.5.pre.lambda → 0.0.6

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +6 -4
  3. data/.github/dependabot.yml +8 -0
  4. data/.github/workflows/automerge.yml +19 -0
  5. data/.rubocop.yml +4 -1
  6. data/Gemfile +1 -1
  7. data/Gemfile.lock +48 -27
  8. data/bin/annotate +3 -1
  9. data/db/schema.rb +40 -3
  10. data/lib/osso.rb +0 -1
  11. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  12. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  13. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  14. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  15. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  16. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  17. data/lib/osso/error/account_configuration_error.rb +1 -0
  18. data/lib/osso/error/oauth_error.rb +6 -3
  19. data/lib/osso/graphql/mutation.rb +1 -0
  20. data/lib/osso/graphql/mutations.rb +1 -0
  21. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  22. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  23. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  24. data/lib/osso/graphql/query.rb +8 -0
  25. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
  26. data/lib/osso/graphql/types.rb +2 -2
  27. data/lib/osso/graphql/types/admin_user.rb +9 -0
  28. data/lib/osso/graphql/types/base_object.rb +1 -1
  29. data/lib/osso/graphql/types/identity_provider.rb +2 -0
  30. data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
  31. data/lib/osso/lib/app_config.rb +1 -1
  32. data/lib/osso/lib/route_map.rb +0 -16
  33. data/lib/osso/lib/saml_handler.rb +5 -0
  34. data/lib/osso/models/access_token.rb +4 -2
  35. data/lib/osso/models/account.rb +34 -0
  36. data/lib/osso/models/authorization_code.rb +2 -1
  37. data/lib/osso/models/enterprise_account.rb +3 -1
  38. data/lib/osso/models/identity_provider.rb +18 -4
  39. data/lib/osso/models/models.rb +1 -0
  40. data/lib/osso/models/oauth_client.rb +0 -1
  41. data/lib/osso/routes/admin.rb +39 -33
  42. data/lib/osso/routes/auth.rb +9 -9
  43. data/lib/osso/routes/oauth.rb +34 -16
  44. data/lib/osso/version.rb +1 -1
  45. data/lib/osso/views/admin.erb +5 -0
  46. data/lib/osso/views/error.erb +1 -0
  47. data/lib/osso/views/layout.erb +0 -0
  48. data/lib/osso/views/multiple_providers.erb +1 -0
  49. data/lib/osso/views/welcome.erb +0 -0
  50. data/lib/tasks/bootstrap.rake +25 -4
  51. data/osso-rb.gemspec +5 -0
  52. data/spec/factories/account.rb +24 -0
  53. data/spec/factories/enterprise_account.rb +11 -3
  54. data/spec/factories/identity_providers.rb +10 -2
  55. data/spec/factories/user.rb +4 -0
  56. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  57. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  58. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  59. data/spec/graphql/query/identity_provider_spec.rb +2 -2
  60. data/spec/models/enterprise_account_spec.rb +18 -0
  61. data/spec/models/identity_provider_spec.rb +24 -3
  62. data/spec/routes/admin_spec.rb +7 -41
  63. data/spec/routes/auth_spec.rb +17 -18
  64. data/spec/routes/oauth_spec.rb +87 -5
  65. data/spec/spec_helper.rb +3 -3
  66. data/spec/support/views/layout.erb +1 -0
  67. metadata +98 -7
  68. data/lib/osso/helpers/auth.rb +0 -94
  69. data/lib/osso/helpers/helpers.rb +0 -8
  70. data/spec/helpers/auth_spec.rb +0 -269
@@ -7,14 +7,19 @@ module Osso
7
7
  belongs_to :enterprise_account
8
8
  belongs_to :oauth_client
9
9
  has_many :users, dependent: :delete_all
10
+ before_create :set_sso_issuer
10
11
  before_save :set_status
11
12
  validate :sso_cert_valid
12
13
 
13
- enum status: { pending: "PENDING", configured: 'CONFIGURED', active: "ACTIVE", error: "ERROR"}
14
+ enum status: { pending: 'PENDING', configured: 'CONFIGURED', active: 'ACTIVE', error: 'ERROR' }
14
15
 
15
16
  PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"
16
17
  PEM_FOOTER = "\n-----END CERTIFICATE-----"
17
18
 
19
+ ENTITY_ID_URI_REQUIRED = [
20
+ 'PING',
21
+ ]
22
+
18
23
  def name
19
24
  service.titlecase
20
25
  end
@@ -24,7 +29,7 @@ module Osso
24
29
  domain: domain,
25
30
  idp_sso_target_url: sso_url,
26
31
  idp_cert: sso_cert,
27
- issuer: domain,
32
+ issuer: sso_issuer,
28
33
  }
29
34
  end
30
35
 
@@ -48,6 +53,14 @@ module Osso
48
53
  self.status = 'configured' if sso_url && sso_cert && pending?
49
54
  end
50
55
 
56
+ def set_sso_issuer
57
+ parts = [domain, oauth_client_id]
58
+
59
+ parts.unshift('https:/') if ENTITY_ID_URI_REQUIRED.any?(service)
60
+
61
+ self.sso_issuer = parts.join('/')
62
+ end
63
+
51
64
  def active!
52
65
  update(status: 'active')
53
66
  end
@@ -85,15 +98,16 @@ end
85
98
  # Table name: identity_providers
86
99
  #
87
100
  # id :uuid not null, primary key
88
- # service :string
101
+ # service :enum
89
102
  # domain :string not null
90
103
  # sso_url :string
91
104
  # sso_cert :text
92
105
  # enterprise_account_id :uuid
93
106
  # oauth_client_id :uuid
94
- # status :enum default("PENDING")
107
+ # status :enum default("pending")
95
108
  # created_at :datetime
96
109
  # updated_at :datetime
110
+ # users_count :integer default(0)
97
111
  #
98
112
  # Indexes
99
113
  #
@@ -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
@@ -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.fetch('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,14 +16,13 @@ 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
25
+ @providers = identity_providers.not_pending
27
26
  return erb :multiple_providers if @providers.count > 1
28
27
 
29
28
  raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
@@ -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-lambda'
4
+ VERSION = '0.0.6'
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,33 @@ 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
+ base_uri = URI.parse(ENV['BASE_URL'])
28
+
29
+ rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
30
+ 'HTTP_HOST' => base_uri.host,
31
+ 'SERVER_NAME' => base_uri.to_s,
32
+ 'rack.url_scheme' => base_uri.scheme
33
+ }))
34
+
35
+ account = rodauth.account_from_login(admin_email)
36
+ rodauth.setup_account_verification
37
+ end
17
38
  end
18
39
  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.6.0'
30
+ spec.add_runtime_dependency 'sequel', '~> 5.37.0'
31
+ spec.add_runtime_dependency 'sequel-activerecord_connection', '>= 0.3', '< 2.0'
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