osso 0.0.3.3 → 0.0.3.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +4 -1
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +1 -2
  5. data/Gemfile.lock +3 -1
  6. data/bin/console +4 -3
  7. data/config/database.yml +2 -2
  8. data/{lib/osso/db → db}/schema.rb +7 -15
  9. data/lib/osso.rb +2 -0
  10. data/lib/osso/db/migrate/20200328143305_create_identity_providers.rb +12 -0
  11. data/lib/osso/db/migrate/20200411184535_add_provider_id_to_users.rb +2 -2
  12. data/lib/osso/db/migrate/20200411192645_create_enterprise_accounts.rb +1 -1
  13. data/lib/osso/db/migrate/20200502135008_add_oauth_client_id_to_enterprise_accounts_and_identity_providers.rb +6 -0
  14. data/lib/osso/db/migrate/20200714223226_add_identity_provider_service_enum.rb +17 -0
  15. data/lib/osso/db/migrate/20200715154211_rename_idp_fields_on_identity_provider_to_sso.rb +6 -0
  16. data/lib/osso/db/migrate/20200715205801_add_name_to_enterprise_account.rb +5 -0
  17. data/lib/osso/db/migrate/20200722230116_add_identity_provider_status_enum_and_use_on_identity_providers.rb +15 -0
  18. data/lib/osso/graphql/mutation.rb +16 -0
  19. data/lib/osso/graphql/mutations.rb +12 -0
  20. data/lib/osso/graphql/mutations/base_mutation.rb +41 -0
  21. data/lib/osso/graphql/mutations/configure_identity_provider.rb +36 -0
  22. data/lib/osso/graphql/mutations/create_enterprise_account.rb +25 -0
  23. data/lib/osso/graphql/mutations/create_identity_provider.rb +30 -0
  24. data/lib/osso/graphql/mutations/set_identity_provider.rb +27 -0
  25. data/lib/osso/graphql/query.rb +25 -0
  26. data/lib/osso/graphql/resolvers.rb +12 -0
  27. data/lib/osso/graphql/resolvers/enterprise_account.rb +25 -0
  28. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +17 -0
  29. data/lib/osso/graphql/resolvers/oauth_clients.rb +15 -0
  30. data/lib/osso/graphql/schema.rb +46 -0
  31. data/lib/osso/graphql/types.rb +16 -0
  32. data/lib/osso/graphql/types/base_enum.rb +10 -0
  33. data/lib/osso/graphql/types/base_input_object.rb +10 -0
  34. data/lib/osso/graphql/types/base_object.rb +12 -0
  35. data/lib/osso/graphql/types/enterprise_account.rb +33 -0
  36. data/lib/osso/graphql/types/identity_provider.rb +33 -0
  37. data/lib/osso/graphql/types/identity_provider_service.rb +12 -0
  38. data/lib/osso/graphql/types/identity_provider_status.rb +14 -0
  39. data/lib/osso/graphql/types/oauth_client.rb +20 -0
  40. data/lib/osso/graphql/types/user.rb +17 -0
  41. data/lib/osso/helpers/auth.rb +53 -49
  42. data/lib/osso/helpers/helpers.rb +3 -1
  43. data/lib/osso/lib/app_config.rb +1 -1
  44. data/lib/osso/lib/route_map.rb +28 -0
  45. data/lib/osso/models/enterprise_account.rb +4 -4
  46. data/lib/osso/models/identity_provider.rb +56 -0
  47. data/lib/osso/models/models.rb +1 -1
  48. data/lib/osso/models/oauth_client.rb +2 -2
  49. data/lib/osso/models/saml_provider.rb +13 -16
  50. data/lib/osso/models/saml_providers/azure_saml_provider.rb +2 -2
  51. data/lib/osso/models/saml_providers/okta_saml_provider.rb +1 -1
  52. data/lib/osso/models/user.rb +3 -3
  53. data/lib/osso/routes/admin.rb +18 -19
  54. data/lib/osso/routes/auth.rb +30 -27
  55. data/lib/osso/routes/oauth.rb +50 -44
  56. data/lib/osso/version.rb +1 -1
  57. data/osso-rb.gemspec +3 -3
  58. data/spec/factories/enterprise_account.rb +5 -4
  59. data/spec/factories/identity_providers.rb +49 -0
  60. data/spec/factories/user.rb +1 -1
  61. data/spec/graphql/mutations/configure_identity_provider_spec.rb +75 -0
  62. data/spec/graphql/mutations/create_enterprise_account_spec.rb +68 -0
  63. data/spec/graphql/mutations/create_identity_provider_spec.rb +104 -0
  64. data/spec/graphql/query/enterprise_account_spec.rb +68 -0
  65. data/spec/graphql/query/enterprise_accounts_spec.rb +44 -0
  66. data/spec/graphql/query/identity_provider_spec.rb +65 -0
  67. data/spec/graphql/query/oauth_clients_account_spec.rb +48 -0
  68. data/spec/models/azure_saml_provider_spec.rb +14 -14
  69. data/spec/models/identity_provider_spec.rb +17 -0
  70. data/spec/models/okta_saml_provider_spec.rb +15 -15
  71. data/spec/routes/admin_spec.rb +3 -0
  72. data/spec/routes/auth_spec.rb +9 -9
  73. data/spec/routes/oauth_spec.rb +1 -1
  74. data/spec/spec_helper.rb +8 -5
  75. data/spec/support/spec_app.rb +9 -0
  76. data/spec/support/views/admin.erb +5 -0
  77. metadata +60 -12
  78. data/lib/osso/db/migrate/20200411144528_create_saml_providers.rb +0 -13
  79. data/lib/osso/db/migrate/20200413153029_add_oauth_client_reference_to_saml_providers.rb +0 -5
  80. data/lib/osso/db/migrate/20200501203026_drop_null_constraints_from_saml_provider.rb +0 -7
  81. data/lib/osso/db/migrate/20200501204047_drop_acs_url.rb +0 -5
  82. data/lib/osso/db/migrate/20200502135008_add_oauth_client_id_to_enterprise_account.rb +0 -5
  83. data/lib/osso/db/migrate/20200601131227_drop_null_constraint_from_saml_providers_provider.rb +0 -7
  84. data/spec/factories/saml_providers.rb +0 -46
  85. data/spec/models/saml_provider_spec.rb +0 -31
  86. data/spec/support/vcr_cassettes/okta_saml_callback.yml +0 -59
@@ -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.fetch('SESSION_SECRET')
10
+ use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
11
11
 
12
12
  error ActiveRecord::RecordNotFound do
13
13
  status 404
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/MethodLength
4
+
5
+ module Osso
6
+ module RouteMap
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ use Osso::Admin
10
+ use Osso::Auth
11
+ use Osso::Oauth
12
+
13
+ post '/graphql' do
14
+ enterprise_protected!
15
+
16
+ result = Osso::GraphQL::Schema.execute(
17
+ params[:query],
18
+ variables: params[:variables],
19
+ context: { scope: current_scope },
20
+ )
21
+
22
+ json result
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ # rubocop:enable Metrics/MethodLength
@@ -10,19 +10,19 @@ module Osso
10
10
  class EnterpriseAccount < ActiveRecord::Base
11
11
  belongs_to :oauth_client
12
12
  has_many :users
13
- has_many :saml_providers
13
+ has_many :identity_providers
14
14
 
15
15
  def single_provider?
16
- saml_providers.one?
16
+ identity_providers.one?
17
17
  end
18
18
 
19
19
  def provider
20
20
  return nil unless single_provider?
21
21
 
22
- saml_providers.first
22
+ identity_providers.first
23
23
  end
24
24
 
25
- alias saml_provider provider
25
+ alias identity_provider provider
26
26
  end
27
27
  end
28
28
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Models
5
+ # Base class for SAML Providers
6
+ class IdentityProvider < ActiveRecord::Base
7
+ NAME_FORMAT = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
8
+ belongs_to :enterprise_account
9
+ belongs_to :oauth_client
10
+ has_many :users
11
+ before_save :set_status
12
+
13
+
14
+ def name
15
+ service.titlecase
16
+ # raise(
17
+ # NoMethodError,
18
+ # '#name must be defined on each provider specific subclass',
19
+ # )
20
+ end
21
+
22
+ def saml_options
23
+ attributes.slice(
24
+ 'domain',
25
+ 'idp_cert',
26
+ 'idp_sso_target_url',
27
+ ).symbolize_keys
28
+ end
29
+
30
+ # def saml_options
31
+ # raise(
32
+ # NoMethodError,
33
+ # '#saml_options must be defined on each provider specific subclass',
34
+ # )
35
+ # end
36
+
37
+ def assertion_consumer_service_url
38
+ [
39
+ ENV.fetch('BASE_URL'),
40
+ 'auth',
41
+ 'saml',
42
+ id,
43
+ 'callback',
44
+ ].join('/')
45
+ end
46
+
47
+ alias acs_url assertion_consumer_service_url
48
+
49
+ def set_status
50
+ return if status != 'PENDING'
51
+
52
+ self.status = 'CONFIGURED' if sso_url && sso_cert
53
+ end
54
+ end
55
+ end
56
+ end
@@ -12,5 +12,5 @@ require_relative 'authorization_code'
12
12
  require_relative 'enterprise_account'
13
13
  require_relative 'oauth_client'
14
14
  require_relative 'redirect_uri'
15
- require_relative 'saml_provider'
15
+ require_relative 'identity_provider'
16
16
  require_relative 'user'
@@ -6,7 +6,7 @@ module Osso
6
6
  class OauthClient < ActiveRecord::Base
7
7
  has_many :access_tokens
8
8
  has_many :refresh_tokens
9
- has_many :saml_providers
9
+ has_many :identity_providers
10
10
  has_many :redirect_uris
11
11
 
12
12
  before_validation :setup, on: :create
@@ -29,4 +29,4 @@ module Osso
29
29
  end
30
30
  end
31
31
  end
32
- end
32
+ end
@@ -3,28 +3,27 @@
3
3
  module Osso
4
4
  module Models
5
5
  # Base class for SAML Providers
6
- class SamlProvider < ActiveRecord::Base
6
+ class IdentityProvider < ActiveRecord::Base
7
7
  NAME_FORMAT = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
8
- self.inheritance_column = :provider
9
8
  belongs_to :enterprise_account
10
9
  belongs_to :oauth_client
11
10
  has_many :users
12
11
 
13
12
  before_create :create_enterprise_account
14
13
 
15
- def name
16
- raise(
17
- NoMethodError,
18
- '#name must be defined on each provider specific subclass',
19
- )
20
- end
14
+ # def name
15
+ # raise(
16
+ # NoMethodError,
17
+ # '#name must be defined on each provider specific subclass',
18
+ # )
19
+ # end
21
20
 
22
- def saml_options
23
- raise(
24
- NoMethodError,
25
- '#saml_options must be defined on each provider specific subclass',
26
- )
27
- end
21
+ # def saml_options
22
+ # raise(
23
+ # NoMethodError,
24
+ # '#saml_options must be defined on each provider specific subclass',
25
+ # )
26
+ # end
28
27
 
29
28
  def assertion_consumer_service_url
30
29
  [
@@ -48,5 +47,3 @@ module Osso
48
47
  end
49
48
  end
50
49
  end
51
- require_relative 'saml_providers/azure_saml_provider'
52
- require_relative 'saml_providers/okta_saml_provider'
@@ -3,7 +3,7 @@
3
3
  module Osso
4
4
  module Models
5
5
  # Subclass for Azure / ADFS IDP instances
6
- class AzureSamlProvider < Models::SamlProvider
6
+ class AzureSamlProvider < Models::IdentityProvider
7
7
  def name
8
8
  'Azure'
9
9
  end
@@ -19,4 +19,4 @@ module Osso
19
19
  end
20
20
  end
21
21
  end
22
- end
22
+ end
@@ -3,7 +3,7 @@
3
3
  module Osso
4
4
  module Models
5
5
  # Subclass for Okta IDP instances
6
- class OktaSamlProvider < Models::SamlProvider
6
+ class OktaSamlProvider < Models::IdentityProvider
7
7
  def name
8
8
  'Okta'
9
9
  end
@@ -4,19 +4,19 @@ module Osso
4
4
  module Models
5
5
  class User < ActiveRecord::Base
6
6
  belongs_to :enterprise_account
7
- belongs_to :saml_provider
7
+ belongs_to :identity_provider
8
8
  has_many :authorization_codes, dependent: :delete_all
9
9
  has_many :access_tokens, dependent: :delete_all
10
10
 
11
11
  def oauth_client
12
- saml_provider.oauth_client
12
+ identity_provider.oauth_client
13
13
  end
14
14
 
15
15
  def as_json(*)
16
16
  {
17
17
  email: email,
18
18
  id: id,
19
- idp: saml_provider.name,
19
+ idp: identity_provider.name,
20
20
  }
21
21
  end
22
22
  end
@@ -6,37 +6,36 @@ module Osso
6
6
  class Admin < Sinatra::Base
7
7
  include AppConfig
8
8
  helpers Helpers::Auth
9
+ register Sinatra::Namespace
9
10
 
10
11
  before do
11
12
  chomp_token
12
13
  end
13
14
 
14
- get '/' do
15
- admin_protected!
15
+ namespace '/admin' do
16
+ get '' do
17
+ admin_protected!
16
18
 
17
- erb :'public/index'
18
- end
19
-
20
- get '/enterprise' do
21
- admin_protected!
19
+ erb :admin
20
+ end
22
21
 
23
- erb :admin
24
- end
22
+ get '/enterprise' do
23
+ admin_protected!
25
24
 
26
- get '/enterprise/:domain' do
27
- enterprise_protected!(params[:domain])
25
+ erb :admin
26
+ end
28
27
 
29
- @enterprise = Models::EnterpriseAccount.where(
30
- domain: params[:domain],
31
- ).first_or_create
28
+ get '/enterprise/:domain' do
29
+ enterprise_protected!(params[:domain])
32
30
 
33
- erb :admin
34
- end
31
+ erb :admin
32
+ end
35
33
 
36
- get '/config' do
37
- admin_protected!
34
+ get '/config' do
35
+ admin_protected!
38
36
 
39
- erb :admin
37
+ erb :admin
38
+ end
40
39
  end
41
40
  end
42
41
  end
@@ -8,6 +8,7 @@ require 'omniauth-saml'
8
8
  module Osso
9
9
  class Auth < Sinatra::Base
10
10
  include AppConfig
11
+ register Sinatra::Namespace
11
12
 
12
13
  UUID_REGEXP =
13
14
  /[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/.
@@ -24,41 +25,43 @@ module Osso
24
25
  identity_provider_id_regex: UUID_REGEXP,
25
26
  path_prefix: '/saml',
26
27
  callback_suffix: 'callback',
27
- ) do |saml_provider_id, _env|
28
- provider = Models::SamlProvider.find(saml_provider_id)
28
+ ) do |identity_provider_id, _env|
29
+ provider = Models::IdentityProvider.find(identity_provider_id)
29
30
  provider.saml_options
30
31
  end
31
32
  end
32
33
 
33
- # Enterprise users are sent here after authenticating against
34
- # their Identity Provider. We find or create a user record,
35
- # and then create an authorization code for that user. The user
36
- # is redirected back to your application with this code
37
- # as a URL query param, which you then exhange for an access token
38
- post '/saml/:id/callback' do
39
- provider = Models::SamlProvider.find(params[:id])
40
- oauth_client = provider.oauth_client
41
- redirect_uri = env['redirect_uri'] || oauth_client.default_redirect_uri.uri
34
+ namespace '/auth' do
35
+ # Enterprise users are sent here after authenticating against
36
+ # their Identity Provider. We find or create a user record,
37
+ # and then create an authorization code for that user. The user
38
+ # is redirected back to your application with this code
39
+ # as a URL query param, which you then exhange for an access token
40
+ post '/saml/:id/callback' do
41
+ provider = Models::IdentityProvider.find(params[:id])
42
+ oauth_client = provider.oauth_client
43
+ redirect_uri = env['redirect_uri'] || oauth_client.default_redirect_uri.uri
42
44
 
43
- attributes = env['omniauth.auth']&.
44
- extra&.
45
- response_object&.
46
- attributes
45
+ attributes = env['omniauth.auth']&.
46
+ extra&.
47
+ response_object&.
48
+ attributes
47
49
 
48
- user = Models::User.where(
49
- email: attributes[:email],
50
- idp_id: attributes[:id],
51
- ).first_or_create! do |new_user|
52
- new_user.enterprise_account_id = provider.enterprise_account_id
53
- new_user.saml_provider_id = provider.id
54
- end
50
+ user = Models::User.where(
51
+ email: attributes[:email],
52
+ idp_id: attributes[:id],
53
+ ).first_or_create! do |new_user|
54
+ new_user.enterprise_account_id = provider.enterprise_account_id
55
+ new_user.identity_provider_id = provider.id
56
+ end
55
57
 
56
- authorization_code = user.authorization_codes.create!(
57
- oauth_client: oauth_client,
58
- redirect_uri: redirect_uri,
59
- )
58
+ authorization_code = user.authorization_codes.create!(
59
+ oauth_client: oauth_client,
60
+ redirect_uri: redirect_uri,
61
+ )
60
62
 
61
- redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{session[:oauth_state]}")
63
+ redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{session[:oauth_state]}")
64
+ end
62
65
  end
63
66
  end
64
67
  end
@@ -5,53 +5,59 @@ require 'rack/oauth2'
5
5
  module Osso
6
6
  class Oauth < Sinatra::Base
7
7
  include AppConfig
8
- # Send your users here in order to being an authentication
9
- # flow. This flow follows the authorization grant oauth
10
- # spec with one exception - you must also pass the domain
11
- # of the user who wants to sign in.
12
- get '/authorize' do
13
- @enterprise = Models::EnterpriseAccount.
14
- includes(:saml_providers).
15
- find_by!(domain: params[:domain])
16
-
17
- Rack::OAuth2::Server::Authorize.new do |req, _res|
18
- client = Models::OauthClient.find_by!(identifier: req.client_id)
19
- req.verify_redirect_uri!(client.redirect_uri_values)
20
- end.call(env)
21
-
22
- if @enterprise.single_provider?
23
- session[:oauth_state] = params[:state]
24
- redirect "/auth/saml/#{@enterprise.provider.id}"
8
+ register Sinatra::Namespace
9
+ # rubocop:disable Metrics/BlockLength
10
+ namespace '/oauth' do
11
+ # Send your users here in order to being an authentication
12
+ # flow. This flow follows the authorization grant oauth
13
+ # spec with one exception - you must also pass the domain
14
+ # of the user who wants to sign in.
15
+ get '/authorize' do
16
+ @enterprise = Models::EnterpriseAccount.
17
+ includes(:identity_providers).
18
+ find_by!(domain: params[:domain])
19
+
20
+ Rack::OAuth2::Server::Authorize.new do |req, _res|
21
+ client = Models::OauthClient.find_by!(identifier: req.client_id)
22
+ req.verify_redirect_uri!(client.redirect_uri_values)
23
+ end.call(env)
24
+
25
+ if @enterprise.single_provider?
26
+ session[:oauth_state] = params[:state]
27
+ redirect "/auth/saml/#{@enterprise.provider.id}"
28
+ end
29
+
30
+ # TODO: multiple provider support
31
+ # erb :multiple_providers
32
+
33
+ rescue Rack::OAuth2::Server::Authorize::BadRequest => e
34
+ @error = e
35
+ return erb :error
25
36
  end
26
37
 
27
- erb :multiple_providers
28
-
29
- rescue Rack::OAuth2::Server::Authorize::BadRequest => e
30
- @error = e
31
- return erb :error
32
- end
33
-
34
- # Exchange an authorization code token for an access token.
35
- # In addition to the token, you must include all paramaters
36
- # required by Oauth spec: redirect_uri, client ID, and client secret
37
- post '/token' do
38
- Rack::OAuth2::Server::Token.new do |req, res|
39
- code = Models::AuthorizationCode.
40
- find_by_token!(params[:code])
41
- client = Models::OauthClient.find_by!(identifier: req.client_id)
42
- req.invalid_client! if client.secret != req.client_secret
43
- req.invalid_grant! if code.redirect_uri != req.redirect_uri
44
- res.access_token = code.access_token.to_bearer_token
45
- end.call(env)
46
- end
38
+ # Exchange an authorization code token for an access token.
39
+ # In addition to the token, you must include all paramaters
40
+ # required by Oauth spec: redirect_uri, client ID, and client secret
41
+ post '/token' do
42
+ Rack::OAuth2::Server::Token.new do |req, res|
43
+ code = Models::AuthorizationCode.
44
+ find_by_token!(params[:code])
45
+ client = Models::OauthClient.find_by!(identifier: req.client_id)
46
+ req.invalid_client! if client.secret != req.client_secret
47
+ req.invalid_grant! if code.redirect_uri != req.redirect_uri
48
+ res.access_token = code.access_token.to_bearer_token
49
+ end.call(env)
50
+ end
47
51
 
48
- # Use the access token to request a user profile
49
- get '/me' do
50
- json Models::AccessToken.
51
- includes(:user).
52
- valid.
53
- find_by_token!(params[:access_token]).
54
- user
52
+ # Use the access token to request a user profile
53
+ get '/me' do
54
+ json Models::AccessToken.
55
+ includes(:user).
56
+ valid.
57
+ find_by_token!(params[:access_token]).
58
+ user
59
+ end
55
60
  end
56
61
  end
57
62
  end
63
+ # rubocop:enable Metrics/BlockLength