osso 0.0.5.pre.zeta → 0.0.8

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 (79) 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 +2 -2
  7. data/Gemfile.lock +69 -51
  8. data/LICENSE +21 -23
  9. data/Rakefile +2 -0
  10. data/bin/annotate +3 -1
  11. data/db/schema.rb +41 -3
  12. data/lib/osso.rb +0 -1
  13. data/lib/osso/db/migrate/20200929154117_add_users_count_to_identity_providers_and_enterprise_accounts.rb +6 -0
  14. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  15. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  16. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  17. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  18. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  19. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  20. data/lib/osso/db/migrate/20201125143501_add_salesforce_to_provider_service_enum.rb +28 -0
  21. data/lib/osso/error/account_configuration_error.rb +1 -0
  22. data/lib/osso/error/oauth_error.rb +6 -3
  23. data/lib/osso/graphql/mutation.rb +2 -0
  24. data/lib/osso/graphql/mutations.rb +2 -0
  25. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  26. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  27. data/lib/osso/graphql/mutations/delete_identity_provider.rb +24 -0
  28. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  29. data/lib/osso/graphql/query.rb +8 -0
  30. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +3 -3
  31. data/lib/osso/graphql/types.rb +2 -2
  32. data/lib/osso/graphql/types/admin_user.rb +9 -0
  33. data/lib/osso/graphql/types/base_object.rb +1 -1
  34. data/lib/osso/graphql/types/enterprise_account.rb +1 -0
  35. data/lib/osso/graphql/types/identity_provider.rb +3 -0
  36. data/lib/osso/graphql/types/identity_provider_service.rb +3 -1
  37. data/lib/osso/lib/app_config.rb +1 -1
  38. data/lib/osso/lib/route_map.rb +0 -15
  39. data/lib/osso/lib/saml_handler.rb +5 -0
  40. data/lib/osso/models/access_token.rb +4 -2
  41. data/lib/osso/models/account.rb +34 -0
  42. data/lib/osso/models/authorization_code.rb +2 -1
  43. data/lib/osso/models/enterprise_account.rb +3 -1
  44. data/lib/osso/models/identity_provider.rb +24 -5
  45. data/lib/osso/models/models.rb +1 -0
  46. data/lib/osso/models/oauth_client.rb +0 -1
  47. data/lib/osso/models/user.rb +2 -2
  48. data/lib/osso/routes/admin.rb +39 -33
  49. data/lib/osso/routes/auth.rb +9 -9
  50. data/lib/osso/routes/oauth.rb +42 -18
  51. data/lib/osso/version.rb +1 -1
  52. data/lib/osso/views/admin.erb +5 -0
  53. data/lib/osso/views/error.erb +1 -0
  54. data/lib/osso/views/layout.erb +0 -0
  55. data/lib/osso/views/multiple_providers.erb +1 -0
  56. data/lib/osso/views/welcome.erb +0 -0
  57. data/lib/tasks/bootstrap.rake +25 -4
  58. data/osso-rb.gemspec +5 -0
  59. data/spec/factories/account.rb +24 -0
  60. data/spec/factories/enterprise_account.rb +11 -3
  61. data/spec/factories/identity_providers.rb +10 -2
  62. data/spec/factories/user.rb +4 -0
  63. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  64. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  65. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  66. data/spec/graphql/query/identity_provider_spec.rb +3 -2
  67. data/spec/models/enterprise_account_spec.rb +18 -0
  68. data/spec/models/identity_provider_spec.rb +36 -1
  69. data/spec/routes/admin_spec.rb +7 -41
  70. data/spec/routes/auth_spec.rb +17 -18
  71. data/spec/routes/oauth_spec.rb +102 -5
  72. data/spec/spec_helper.rb +3 -3
  73. data/spec/support/views/hosted_login.erb +1 -0
  74. data/spec/support/views/layout.erb +1 -0
  75. data/spec/support/views/multiple_providers.erb +1 -0
  76. metadata +108 -7
  77. data/lib/osso/helpers/auth.rb +0 -94
  78. data/lib/osso/helpers/helpers.rb +0 -8
  79. data/spec/helpers/auth_spec.rb +0 -97
@@ -5,9 +5,11 @@ module Osso
5
5
  module Types
6
6
  class IdentityProviderService < BaseEnum
7
7
  value('AZURE', 'Microsoft Azure Identity Provider', value: 'AZURE')
8
+ value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
8
9
  value('OKTA', 'Okta Identity Provider', value: 'OKTA')
9
10
  value('ONELOGIN', 'OneLogin Identity Provider', value: 'ONELOGIN')
10
- value('GOOGLE', 'Google SAML Identity Provider', value: 'GOOGLE')
11
+ value('PING', 'PingID Identity Provider', value: 'PING')
12
+ value('SALESFORCE', 'Salesforce Identity Provider', value: 'SALESFORCE')
11
13
  end
12
14
  end
13
15
  end
@@ -7,7 +7,7 @@ module Osso
7
7
  def self.included(klass)
8
8
  klass.class_eval do
9
9
  use Rack::JSONBodyParser
10
- use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
10
+ use Rack::Session::Cookie, secret: ENV.fetch('SESSION_SECRET')
11
11
 
12
12
  error ActiveRecord::RecordNotFound do
13
13
  status 404
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/MethodLength
4
-
5
3
  module Osso
6
4
  module RouteMap
7
5
  def self.included(klass)
@@ -9,20 +7,7 @@ module Osso
9
7
  use Osso::Admin
10
8
  use Osso::Auth
11
9
  use Osso::Oauth
12
-
13
- post '/graphql' do
14
- token_protected!
15
-
16
- result = Osso::GraphQL::Schema.execute(
17
- params[:query],
18
- variables: params[:variables],
19
- context: current_user.symbolize_keys,
20
- )
21
-
22
- json result
23
- end
24
10
  end
25
11
  end
26
12
  end
27
13
  end
28
- # rubocop:enable Metrics/MethodLength
@@ -55,6 +55,7 @@ module Osso
55
55
  @authorization_code ||= user.authorization_codes.create!(
56
56
  oauth_client: provider.oauth_client,
57
57
  redirect_uri: redirect_uri_base,
58
+ requested: requested_param,
58
59
  )
59
60
  end
60
61
 
@@ -81,5 +82,9 @@ module Osso
81
82
  def valid_idp_initiated_flow
82
83
  !session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
83
84
  end
85
+
86
+ def requested_param
87
+ @session.delete(:osso_oauth_requested)
88
+ end
84
89
  end
85
90
  end
@@ -39,9 +39,11 @@ end
39
39
  # updated_at :datetime not null
40
40
  # user_id :uuid
41
41
  # oauth_client_id :uuid
42
+ # requested :jsonb
42
43
  #
43
44
  # Indexes
44
45
  #
45
- # index_access_tokens_on_oauth_client_id (oauth_client_id)
46
- # index_access_tokens_on_user_id (user_id)
46
+ # index_access_tokens_on_oauth_client_id (oauth_client_id)
47
+ # index_access_tokens_on_token_and_expires_at (token,expires_at) UNIQUE
48
+ # index_access_tokens_on_user_id (user_id)
47
49
  #
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Models
5
+ class Account < ::ActiveRecord::Base
6
+ enum status_id: { 1 => :Unverified, 2 => :Verified, 3 => :Closed }
7
+
8
+ def context
9
+ {
10
+ email: email,
11
+ id: id,
12
+ scope: role,
13
+ oauth_client_id: oauth_client_id,
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # == Schema Information
21
+ #
22
+ # Table name: accounts
23
+ #
24
+ # id :uuid not null, primary key
25
+ # email :citext not null
26
+ # status_id :integer default(NULL), not null
27
+ # role :string default("admin"), not null
28
+ # oauth_client_id :string
29
+ #
30
+ # Indexes
31
+ #
32
+ # index_accounts_on_email (email) UNIQUE WHERE (status_id = ANY (ARRAY[1, 2]))
33
+ # index_accounts_on_oauth_client_id (oauth_client_id)
34
+ #
@@ -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,8 @@ 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,
33
+ name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
28
34
  }
29
35
  end
30
36
 
@@ -40,10 +46,22 @@ module Osso
40
46
 
41
47
  alias acs_url assertion_consumer_service_url
42
48
 
49
+ def acs_url_validator
50
+ Regexp.escape(acs_url)
51
+ end
52
+
43
53
  def set_status
44
54
  self.status = 'configured' if sso_url && sso_cert && pending?
45
55
  end
46
56
 
57
+ def set_sso_issuer
58
+ parts = [domain, oauth_client_id]
59
+
60
+ parts.unshift('https:/') if ENTITY_ID_URI_REQUIRED.any?(service)
61
+
62
+ self.sso_issuer = parts.join('/')
63
+ end
64
+
47
65
  def active!
48
66
  update(status: 'active')
49
67
  end
@@ -81,15 +99,16 @@ end
81
99
  # Table name: identity_providers
82
100
  #
83
101
  # id :uuid not null, primary key
84
- # service :string
102
+ # service :enum
85
103
  # domain :string not null
86
104
  # sso_url :string
87
105
  # sso_cert :text
88
106
  # enterprise_account_id :uuid
89
107
  # oauth_client_id :uuid
90
- # status :enum default("PENDING")
108
+ # status :enum default("pending")
91
109
  # created_at :datetime
92
110
  # updated_at :datetime
111
+ # users_count :integer default(0)
93
112
  #
94
113
  # Indexes
95
114
  #
@@ -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.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,15 +16,15 @@ 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)
21
-
22
19
  validate_oauth_request(env)
23
20
 
24
- redirect "/auth/saml/#{enterprise.provider.id}" if enterprise.single_provider?
21
+ return erb :hosted_login if render_hosted_login?
22
+
23
+ @providers = find_providers
25
24
 
26
- @providers = enterprise.identity_providers.not_pending
27
- erb :multiple_providers if @providers.count > 1
25
+ redirect "/auth/saml/#{@providers.first.id}" if @providers.one?
26
+
27
+ return erb :multiple_providers if @providers.count > 1
28
28
 
29
29
  raise Osso::Error::MissingConfiguredIdentityProvider.new(domain: params[:domain])
30
30
  rescue Osso::Error::Base => e
@@ -38,11 +38,12 @@ module Osso
38
38
  # and client secret
39
39
  post '/token' do
40
40
  Rack::OAuth2::Server::Token.new do |req, res|
41
- code = Models::AuthorizationCode.
42
- find_by_token!(params[:code])
43
41
  client = Models::OauthClient.find_by!(identifier: req.client_id)
44
42
  req.invalid_client! if client.secret != req.client_secret
43
+
44
+ code = Models::AuthorizationCode.find_by_token!(params[:code])
45
45
  req.invalid_grant! if code.redirect_uri != req.redirect_uri
46
+
46
47
  res.access_token = code.access_token.to_bearer_token
47
48
  end.call(env)
48
49
  end
@@ -50,22 +51,40 @@ module Osso
50
51
  # Use the access token to request a profile for the user who
51
52
  # just logged in. Access tokens are short-lived.
52
53
  get '/me' do
53
- json Models::AccessToken.
54
+ token = Models::AccessToken.
54
55
  includes(:user).
55
56
  valid.
56
- find_by_token!(params[:access_token]).
57
- user
57
+ find_by_token!(access_token)
58
+
59
+ json token.user.as_json.merge(requested: token.requested)
58
60
  end
59
61
  end
60
62
 
61
63
  private
62
64
 
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])
65
+ def render_hosted_login?
66
+ [params[:email], params[:domain]].all?(&:nil?)
67
+ end
68
+
69
+ def find_providers
70
+ if params[:email]
71
+ user = Osso::Models::User.
72
+ includes(:identity_provider).
73
+ find_by(email: params[:email])
74
+ return [user.identity_provider] if user
75
+ end
76
+
77
+ Osso::Models::IdentityProvider.
78
+ joins(:oauth_client).
79
+ not_pending.
80
+ where(
81
+ domain: domain_from_params,
82
+ oauth_clients: { identifier: params[:client_id] },
83
+ )
84
+ end
85
+
86
+ def domain_from_params
87
+ params[:domain] || params[:email].split('@')[1]
69
88
  end
70
89
 
71
90
  def find_client(identifier)
@@ -74,14 +93,19 @@ module Osso
74
93
  raise Osso::Error::InvalidOAuthClientIdentifier
75
94
  end
76
95
 
77
- def validate_oauth_request(env)
96
+ def validate_oauth_request(env) # rubocop:disable Metrics/AbcSize
78
97
  Rack::OAuth2::Server::Authorize.new do |req, _res|
79
98
  client = find_client(req[:client_id])
80
99
  session[:osso_oauth_redirect_uri] = req.verify_redirect_uri!(client.redirect_uri_values)
81
100
  session[:osso_oauth_state] = params[:state]
101
+ session[:osso_oauth_requested] = { domain: req[:domain], email: req[:email] }
82
102
  end.call(env)
83
103
  rescue Rack::OAuth2::Server::Authorize::BadRequest
84
104
  raise Osso::Error::InvalidRedirectUri.new(redirect_uri: params[:redirect_uri])
85
105
  end
106
+
107
+ def access_token
108
+ params[:access_token] || env.fetch('HTTP_AUTHORIZATION', '').slice(-64..-1)
109
+ end
86
110
  end
87
111
  end