osso 0.0.5.pre.zeta → 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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +4 -2
  3. data/.rubocop.yml +4 -1
  4. data/Gemfile.lock +48 -32
  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 +2 -0
  18. data/lib/osso/graphql/mutations.rb +2 -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/delete_identity_provider.rb +24 -0
  22. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  23. data/lib/osso/graphql/query.rb +8 -0
  24. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +3 -3
  25. data/lib/osso/graphql/types.rb +2 -2
  26. data/lib/osso/graphql/types/admin_user.rb +9 -0
  27. data/lib/osso/graphql/types/base_object.rb +1 -1
  28. data/lib/osso/graphql/types/enterprise_account.rb +1 -0
  29. data/lib/osso/graphql/types/identity_provider.rb +3 -0
  30. data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
  31. data/lib/osso/helpers/auth.rb +1 -1
  32. data/lib/osso/lib/route_map.rb +0 -15
  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 +23 -5
  39. data/lib/osso/models/models.rb +1 -0
  40. data/lib/osso/models/oauth_client.rb +0 -1
  41. data/lib/osso/models/user.rb +2 -2
  42. data/lib/osso/routes/admin.rb +39 -33
  43. data/lib/osso/routes/auth.rb +9 -9
  44. data/lib/osso/routes/oauth.rb +35 -17
  45. data/lib/osso/version.rb +1 -1
  46. data/lib/osso/views/admin.erb +5 -0
  47. data/lib/osso/views/error.erb +1 -0
  48. data/lib/osso/views/layout.erb +0 -0
  49. data/lib/osso/views/multiple_providers.erb +1 -0
  50. data/lib/osso/views/welcome.erb +0 -0
  51. data/lib/tasks/bootstrap.rake +18 -4
  52. data/osso-rb.gemspec +5 -0
  53. data/spec/factories/account.rb +24 -0
  54. data/spec/factories/enterprise_account.rb +11 -3
  55. data/spec/factories/identity_providers.rb +10 -2
  56. data/spec/factories/user.rb +4 -0
  57. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  58. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  59. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  60. data/spec/graphql/query/identity_provider_spec.rb +3 -2
  61. data/spec/models/enterprise_account_spec.rb +18 -0
  62. data/spec/models/identity_provider_spec.rb +35 -1
  63. data/spec/routes/admin_spec.rb +7 -41
  64. data/spec/routes/auth_spec.rb +17 -18
  65. data/spec/routes/oauth_spec.rb +88 -5
  66. data/spec/spec_helper.rb +3 -3
  67. data/spec/support/views/layout.erb +1 -0
  68. data/spec/support/views/multiple_providers.erb +1 -0
  69. metadata +92 -5
  70. data/spec/helpers/auth_spec.rb +0 -97
@@ -7,7 +7,7 @@ module Osso
7
7
 
8
8
  def access_token
9
9
  @access_token ||= expired! &&
10
- user.access_tokens.create(oauth_client: oauth_client)
10
+ user.access_tokens.create(oauth_client: oauth_client, requested: requested)
11
11
  end
12
12
  end
13
13
  end
@@ -25,6 +25,7 @@ end
25
25
  # updated_at :datetime not null
26
26
  # user_id :uuid
27
27
  # oauth_client_id :uuid
28
+ # requested :jsonb
28
29
  #
29
30
  # Indexes
30
31
  #
@@ -8,10 +8,11 @@ module Osso
8
8
  # includes fields for external IDs such that you can persist
9
9
  # your ID for an account in your Osso instance.
10
10
  class EnterpriseAccount < ActiveRecord::Base
11
- belongs_to :oauth_client
12
11
  has_many :users
13
12
  has_many :identity_providers
14
13
 
14
+ validates_format_of :domain, with: /\A[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/
15
+
15
16
  def single_provider?
16
17
  identity_providers.not_pending.one?
17
18
  end
@@ -40,6 +41,7 @@ end
40
41
  # name :string not null
41
42
  # created_at :datetime not null
42
43
  # updated_at :datetime not null
44
+ # users_count :integer default(0)
43
45
  #
44
46
  # Indexes
45
47
  #
@@ -6,15 +6,20 @@ module Osso
6
6
  class IdentityProvider < ActiveRecord::Base
7
7
  belongs_to :enterprise_account
8
8
  belongs_to :oauth_client
9
- has_many :users
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
 
@@ -40,10 +45,22 @@ module Osso
40
45
 
41
46
  alias acs_url assertion_consumer_service_url
42
47
 
48
+ def acs_url_validator
49
+ Regexp.escape(acs_url)
50
+ end
51
+
43
52
  def set_status
44
53
  self.status = 'configured' if sso_url && sso_cert && pending?
45
54
  end
46
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
+
47
64
  def active!
48
65
  update(status: 'active')
49
66
  end
@@ -81,15 +98,16 @@ end
81
98
  # Table name: identity_providers
82
99
  #
83
100
  # id :uuid not null, primary key
84
- # service :string
101
+ # service :enum
85
102
  # domain :string not null
86
103
  # sso_url :string
87
104
  # sso_cert :text
88
105
  # enterprise_account_id :uuid
89
106
  # oauth_client_id :uuid
90
- # status :enum default("PENDING")
107
+ # status :enum default("pending")
91
108
  # created_at :datetime
92
109
  # updated_at :datetime
110
+ # users_count :integer default(0)
93
111
  #
94
112
  # Indexes
95
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
@@ -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-zeta'
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'