osso 0.0.3.15 → 0.0.3.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +9 -0
  3. data/.rubocop.yml +1 -0
  4. data/Gemfile +1 -0
  5. data/Gemfile.lock +10 -2
  6. data/README.md +3 -2
  7. data/db/schema.rb +9 -1
  8. data/lib/osso/db/migrate/20200826201852_create_app_config.rb +11 -0
  9. data/lib/osso/graphql/mutation.rb +7 -0
  10. data/lib/osso/graphql/mutations.rb +1 -0
  11. data/lib/osso/graphql/mutations/base_mutation.rb +18 -5
  12. data/lib/osso/graphql/mutations/configure_identity_provider.rb +8 -10
  13. data/lib/osso/graphql/mutations/create_enterprise_account.rb +7 -0
  14. data/lib/osso/graphql/mutations/create_identity_provider.rb +14 -5
  15. data/lib/osso/graphql/mutations/create_oauth_client.rb +1 -3
  16. data/lib/osso/graphql/mutations/delete_enterprise_account.rb +9 -11
  17. data/lib/osso/graphql/mutations/delete_oauth_client.rb +1 -3
  18. data/lib/osso/graphql/mutations/regenerate_oauth_credentials.rb +1 -3
  19. data/lib/osso/graphql/mutations/set_redirect_uris.rb +2 -4
  20. data/lib/osso/graphql/mutations/update_app_config.rb +29 -0
  21. data/lib/osso/graphql/query.rb +14 -0
  22. data/lib/osso/graphql/resolvers.rb +1 -0
  23. data/lib/osso/graphql/resolvers/base_resolver.rb +25 -0
  24. data/lib/osso/graphql/resolvers/enterprise_account.rb +1 -11
  25. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
  26. data/lib/osso/graphql/resolvers/oauth_clients.rb +2 -2
  27. data/lib/osso/graphql/types.rb +2 -1
  28. data/lib/osso/graphql/types/admin_user.rb +22 -0
  29. data/lib/osso/graphql/types/app_config.rb +22 -0
  30. data/lib/osso/graphql/types/base_object.rb +22 -0
  31. data/lib/osso/graphql/types/enterprise_account.rb +0 -5
  32. data/lib/osso/graphql/types/identity_provider.rb +0 -6
  33. data/lib/osso/graphql/types/oauth_client.rb +2 -4
  34. data/lib/osso/graphql/types/redirect_uri.rb +2 -4
  35. data/lib/osso/helpers/auth.rb +40 -18
  36. data/lib/osso/lib/route_map.rb +2 -2
  37. data/lib/osso/models/app_config.rb +33 -0
  38. data/lib/osso/models/models.rb +1 -0
  39. data/lib/osso/models/oauth_client.rb +3 -2
  40. data/lib/osso/models/redirect_uri.rb +0 -11
  41. data/lib/osso/routes/admin.rb +8 -2
  42. data/lib/osso/routes/auth.rb +29 -12
  43. data/lib/osso/routes/oauth.rb +25 -18
  44. data/lib/osso/version.rb +1 -1
  45. data/lib/tasks/bootstrap.rake +2 -0
  46. data/spec/graphql/mutations/configure_identity_provider_spec.rb +17 -4
  47. data/spec/graphql/mutations/create_enterprise_account_spec.rb +53 -4
  48. data/spec/graphql/mutations/create_identity_provider_spec.rb +18 -6
  49. data/spec/graphql/mutations/create_oauth_client_spec.rb +10 -3
  50. data/spec/graphql/mutations/delete_enterprise_account_spec.rb +18 -4
  51. data/spec/graphql/mutations/delete_oauth_client_spec.rb +8 -4
  52. data/spec/graphql/query/enterprise_account_spec.rb +21 -6
  53. data/spec/graphql/query/enterprise_accounts_spec.rb +4 -2
  54. data/spec/graphql/query/identity_provider_spec.rb +16 -6
  55. data/spec/graphql/query/oauth_clients_spec.rb +10 -7
  56. data/spec/helpers/auth_spec.rb +97 -0
  57. data/spec/routes/auth_spec.rb +18 -0
  58. data/spec/routes/oauth_spec.rb +5 -2
  59. data/spec/spec_helper.rb +3 -0
  60. data/spec/support/views/error.erb +0 -0
  61. metadata +10 -3
  62. data/lib/osso/graphql/types/user.rb +0 -17
@@ -3,22 +3,12 @@
3
3
  module Osso
4
4
  module GraphQL
5
5
  module Resolvers
6
- class EnterpriseAccount < ::GraphQL::Schema::Resolver
6
+ class EnterpriseAccount < BaseResolver
7
7
  type Types::EnterpriseAccount, null: false
8
8
 
9
9
  def resolve(args)
10
- return unless admin? || enterprise_authorized?(args[:domain])
11
-
12
10
  Osso::Models::EnterpriseAccount.find_by(domain: args[:domain])
13
11
  end
14
-
15
- def admin?
16
- context[:scope] == :admin
17
- end
18
-
19
- def enterprise_authorized?(domain)
20
- context[:scope] == domain
21
- end
22
12
  end
23
13
  end
24
14
  end
@@ -3,11 +3,11 @@
3
3
  module Osso
4
4
  module GraphQL
5
5
  module Resolvers
6
- class EnterpriseAccounts < ::GraphQL::Schema::Resolver
6
+ class EnterpriseAccounts < BaseResolver
7
7
  type Types::EnterpriseAccount.connection_type, null: true
8
8
 
9
9
  def resolve(sort_column: nil, sort_order: nil)
10
- return Array(Osso::Models::EnterpriseAccount.find_by(domain: context[:scope])) if context[:scope] != :admin
10
+ return Array(Osso::Models::EnterpriseAccount.find_by(domain: context_domain)) unless internal_authorized?
11
11
 
12
12
  accounts = Osso::Models::EnterpriseAccount
13
13
 
@@ -3,11 +3,11 @@
3
3
  module Osso
4
4
  module GraphQL
5
5
  module Resolvers
6
- class OAuthClients < ::GraphQL::Schema::Resolver
6
+ class OAuthClients < BaseResolver
7
7
  type [Types::OauthClient], null: true
8
8
 
9
9
  def resolve
10
- return Osso::Models::OauthClient.all if context[:scope] == :admin
10
+ Osso::Models::OauthClient.all
11
11
  end
12
12
  end
13
13
  end
@@ -9,6 +9,8 @@ require_relative 'types/base_connection'
9
9
  require_relative 'types/base_object'
10
10
  require_relative 'types/base_enum'
11
11
  require_relative 'types/base_input_object'
12
+ require_relative 'types/admin_user'
13
+ require_relative 'types/app_config'
12
14
  require_relative 'types/identity_provider_service'
13
15
  require_relative 'types/identity_provider_status'
14
16
  require_relative 'types/identity_provider'
@@ -16,4 +18,3 @@ require_relative 'types/enterprise_account'
16
18
  require_relative 'types/redirect_uri'
17
19
  require_relative 'types/redirect_uri_input'
18
20
  require_relative 'types/oauth_client'
19
- require_relative 'types/user'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module Osso
6
+ module GraphQL
7
+ module Types
8
+ class AdminUser < Types::BaseObject
9
+ description 'An Admin User of Osso'
10
+
11
+ field :id, ID, null: false
12
+ field :email, String, null: false
13
+ field :scope, String, null: false
14
+ field :oauth_client_id, ID, null: true
15
+
16
+ def self.authorized?(_object, _context)
17
+ true
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql'
4
+
5
+ module Osso
6
+ module GraphQL
7
+ module Types
8
+ class AppConfig < Types::BaseObject
9
+ description 'Configuration values for your application'
10
+
11
+ field :id, ID, null: false
12
+ field :name, String, null: true
13
+ field :logo_url, String, null: true
14
+ field :contact_email, String, null: true
15
+
16
+ def self.authorized?(_object, context)
17
+ admin_authorized?(context)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,6 +10,28 @@ module Osso
10
10
 
11
11
  field :created_at, ::GraphQL::Types::ISO8601DateTime, null: false
12
12
  field :updated_at, ::GraphQL::Types::ISO8601DateTime, null: false
13
+
14
+ def self.admin_authorized?(context)
15
+ context[:scope] == 'admin'
16
+ end
17
+
18
+ def self.internal_authorized?(context)
19
+ %w[admin internal].include?(context[:scope])
20
+ end
21
+
22
+ def self.enterprise_authorized?(context, domain)
23
+ return false unless domain
24
+
25
+ context[:email].split('@')[1] == domain
26
+ end
27
+
28
+ def self.authorized?(object, context)
29
+ # we first receive the payload object as a hash, but can depend on the
30
+ # return type to hide the actual objects non-admins shouldn't see
31
+ return true if object.class == Hash
32
+
33
+ internal_authorized?(context) || enterprise_authorized?(context, object&.domain)
34
+ end
13
35
  end
14
36
  end
15
37
  end
@@ -9,7 +9,6 @@ module Osso
9
9
  description 'An Account for a company that wishes to use SAML via Osso'
10
10
  implements ::GraphQL::Types::Relay::Node
11
11
 
12
- global_id_field :gid
13
12
  field :id, ID, null: false
14
13
  field :name, String, null: false
15
14
  field :domain, String, null: false
@@ -23,10 +22,6 @@ module Osso
23
22
  def identity_providers
24
23
  object.identity_providers
25
24
  end
26
-
27
- def self.authorized?(object, context)
28
- super && (context[:scope] == :admin || object.domain == context[:scope])
29
- end
30
25
  end
31
26
  end
32
27
  end
@@ -7,9 +7,7 @@ module Osso
7
7
  module Types
8
8
  class IdentityProvider < Types::BaseObject
9
9
  description 'Represents a SAML based IDP instance for an EnterpriseAccount'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :enterprise_account_id, ID, null: false
15
13
  field :service, Types::IdentityProviderService, null: true
@@ -23,10 +21,6 @@ module Osso
23
21
  def documentation_pdf_url
24
22
  ENV['BASE_URL'] + '/identity_provider/documentation/' + @object.id
25
23
  end
26
-
27
- def self.authorized?(object, context)
28
- super && (context[:scope] == :admin || object.domain == context[:scope])
29
- end
30
24
  end
31
25
  end
32
26
  end
@@ -7,9 +7,7 @@ module Osso
7
7
  module Types
8
8
  class OauthClient < Types::BaseObject
9
9
  description 'An OAuth client used to consume Osso SAML users'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :name, String, null: false
15
13
  field :client_id, String, null: false
@@ -24,8 +22,8 @@ module Osso
24
22
  object.secret
25
23
  end
26
24
 
27
- def self.authorized?(object, context)
28
- super && context[:scope] == :admin
25
+ def self.authorized?(_object, context)
26
+ admin_authorized?(context)
29
27
  end
30
28
  end
31
29
  end
@@ -7,15 +7,13 @@ module Osso
7
7
  module Types
8
8
  class RedirectUri < Types::BaseObject
9
9
  description 'An allowed redirect URI for an OauthClient'
10
- implements ::GraphQL::Types::Relay::Node
11
10
 
12
- global_id_field :gid
13
11
  field :id, ID, null: false
14
12
  field :uri, String, null: false
15
13
  field :primary, Boolean, null: false
16
14
 
17
- def self.authorized?(object, context)
18
- super && context[:scope] == :admin
15
+ def self.authorized?(_object, context)
16
+ context[:scope] == 'admin'
19
17
  end
20
18
  end
21
19
  end
@@ -3,10 +3,21 @@
3
3
  module Osso
4
4
  module Helpers
5
5
  module Auth
6
- attr_accessor :current_scope
6
+ END_USER_SCOPE = 'end-user'
7
+ INTERNAL_SCOPE = 'internal'
8
+ ADMIN_SCOPE = 'admin'
9
+
10
+ attr_accessor :current_user
11
+
12
+ def token_protected!
13
+ decode(token)
14
+ rescue JWT::DecodeError
15
+ halt 401
16
+ end
7
17
 
8
18
  def enterprise_protected!(domain = nil)
9
19
  return if admin_authorized?
20
+ return if internal_authorized?
10
21
  return if enterprise_authorized?(domain)
11
22
 
12
23
  halt 401 if request.post?
@@ -14,33 +25,42 @@ module Osso
14
25
  redirect ENV['JWT_URL']
15
26
  end
16
27
 
17
- # use client id in payload to restrict customer
18
- # users from accessing dev?
19
- def enterprise_authorized?(_domain)
20
- payload, _args = decode(token)
21
-
22
- @current_scope = payload['scope']
28
+ def internal_protected!
29
+ return if admin_authorized?
30
+ return if internal_authorized?
23
31
 
24
- true
25
- rescue JWT::DecodeError
26
- false
32
+ redirect ENV['JWT_URL']
27
33
  end
28
34
 
29
35
  def admin_protected!
30
- return if admin_authorized?
36
+ return true if admin_authorized?
31
37
 
32
38
  redirect ENV['JWT_URL']
33
39
  end
34
40
 
35
- def admin_authorized?
36
- payload, _args = decode(token)
41
+ private
37
42
 
38
- if payload['scope'] == 'admin'
39
- @current_scope = :admin
40
- return true
41
- end
43
+ def enterprise_authorized?(domain)
44
+ decode(token)
42
45
 
46
+ @current_user[:scope] == END_USER_SCOPE &&
47
+ @current_user[:email].split('@')[1] == domain
48
+ rescue JWT::DecodeError
43
49
  false
50
+ end
51
+
52
+ def internal_authorized?
53
+ decode(token)
54
+
55
+ @current_user[:scope] == INTERNAL_SCOPE
56
+ rescue JWT::DecodeError
57
+ false
58
+ end
59
+
60
+ def admin_authorized?
61
+ decode(token)
62
+
63
+ @current_user[:scope] == ADMIN_SCOPE
44
64
  rescue JWT::DecodeError
45
65
  false
46
66
  end
@@ -60,12 +80,14 @@ module Osso
60
80
  end
61
81
 
62
82
  def decode(token)
63
- JWT.decode(
83
+ payload, _args = JWT.decode(
64
84
  token,
65
85
  ENV['JWT_HMAC_SECRET'],
66
86
  true,
67
87
  { algorithm: 'HS256' },
68
88
  )
89
+
90
+ @current_user = payload.symbolize_keys
69
91
  end
70
92
  end
71
93
  end
@@ -11,12 +11,12 @@ module Osso
11
11
  use Osso::Oauth
12
12
 
13
13
  post '/graphql' do
14
- enterprise_protected!
14
+ token_protected!
15
15
 
16
16
  result = Osso::GraphQL::Schema.execute(
17
17
  params[:query],
18
18
  variables: params[:variables],
19
- context: { scope: current_scope },
19
+ context: current_user.symbolize_keys,
20
20
  )
21
21
 
22
22
  json result
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osso
4
+ module Models
5
+ class AppConfig < ::ActiveRecord::Base
6
+ validate :limit_to_one, on: :create
7
+
8
+ def self.find
9
+ first
10
+ end
11
+
12
+ private
13
+
14
+ def limit_to_one
15
+ return if Osso::Models::AppConfig.count.zero?
16
+
17
+ errors[:base] << 'AppConfig already exists'
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # == Schema Information
24
+ #
25
+ # Table name: app_configs
26
+ #
27
+ # id :uuid not null, primary key
28
+ # contact_email :string
29
+ # logo_url :string
30
+ # name :string
31
+ # created_at :datetime not null
32
+ # updated_at :datetime not null
33
+ #
@@ -10,6 +10,7 @@ module Osso
10
10
  end
11
11
 
12
12
  require_relative 'access_token'
13
+ require_relative 'app_config'
13
14
  require_relative 'authorization_code'
14
15
  require_relative 'enterprise_account'
15
16
  require_relative 'oauth_client'
@@ -5,6 +5,7 @@ module Osso
5
5
  module Models
6
6
  class OauthClient < ActiveRecord::Base
7
7
  has_many :access_tokens
8
+ has_many :enterprise_accounts
8
9
  has_many :refresh_tokens
9
10
  has_many :identity_providers
10
11
  has_many :redirect_uris
@@ -22,8 +23,8 @@ module Osso
22
23
  end
23
24
 
24
25
  def generate_secrets
25
- self.identifier = SecureRandom.hex(16)
26
- self.secret = SecureRandom.hex(32)
26
+ self.identifier ||= SecureRandom.hex(16)
27
+ self.secret ||= SecureRandom.hex(32)
27
28
  end
28
29
  end
29
30
  end
@@ -4,17 +4,6 @@ module Osso
4
4
  module Models
5
5
  class RedirectUri < ActiveRecord::Base
6
6
  belongs_to :oauth_client
7
-
8
- # TODO
9
- # before_validation :set_primary, on: :creaet, :update
10
-
11
- private
12
-
13
- def set_primary
14
- if primary_was.true? && primary.false?
15
-
16
- end
17
- end
18
7
  end
19
8
  end
20
9
  end
@@ -13,14 +13,20 @@ module Osso
13
13
  end
14
14
 
15
15
  namespace '/admin' do
16
+ get '/login' do
17
+ token_protected!
18
+
19
+ erb :admin
20
+ end
21
+
16
22
  get '' do
17
- admin_protected!
23
+ internal_protected!
18
24
 
19
25
  erb :admin
20
26
  end
21
27
 
22
28
  get '/enterprise' do
23
- admin_protected!
29
+ token_protected!
24
30
 
25
31
  erb :admin
26
32
  end
@@ -14,10 +14,6 @@ module Osso
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
- def self.internal_redirect?(env)
18
- env['HTTP_REFERER']&.match(env['SERVER_NAME'])
19
- end
20
-
21
17
  use OmniAuth::Builder do
22
18
  OmniAuth::MultiProvider.register(
23
19
  self,
@@ -26,21 +22,24 @@ module Osso
26
22
  path_prefix: '/auth/saml',
27
23
  callback_suffix: 'callback',
28
24
  ) do |identity_provider_id, _env|
29
- provider = Models::IdentityProvider.find(identity_provider_id)
30
- provider.saml_options
25
+ Models::IdentityProvider.find(identity_provider_id).
26
+ saml_options
31
27
  end
32
28
  end
33
29
 
34
- namespace '/auth' do
30
+ namespace '/auth' do # rubocop:disable Metrics/BlockLength
31
+ get '/failure' do
32
+ @error = params[:message]
33
+ erb :error
34
+ end
35
35
  # Enterprise users are sent here after authenticating against
36
36
  # their Identity Provider. We find or create a user record,
37
37
  # and then create an authorization code for that user. The user
38
38
  # is redirected back to your application with this code
39
- # as a URL query param, which you then exhange for an access token
39
+ # as a URL query param, which you then exchange for an access token.
40
40
  post '/saml/:id/callback' do
41
41
  provider = Models::IdentityProvider.find(params[:id])
42
- oauth_client = provider.oauth_client
43
- redirect_uri = env['redirect_uri'] || oauth_client.primary_redirect_uri.uri
42
+ @oauth_client = provider.oauth_client
44
43
 
45
44
  attributes = env['omniauth.auth']&.
46
45
  extra&.
@@ -56,11 +55,29 @@ module Osso
56
55
  end
57
56
 
58
57
  authorization_code = user.authorization_codes.create!(
59
- oauth_client: oauth_client,
58
+ oauth_client: @oauth_client,
60
59
  redirect_uri: redirect_uri,
61
60
  )
62
61
 
63
- redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{session[:oauth_state]}")
62
+ # Mark IDP as active
63
+
64
+ redirect(redirect_uri + "?code=#{CGI.escape(authorization_code.token)}&state=#{provider_state}")
65
+ end
66
+
67
+ def redirect_uri
68
+ return @oauth_client.primary_redirect_uri.uri if valid_idp_initiated_flow
69
+
70
+ session[:osso_oauth_redirect_uri]
71
+ end
72
+
73
+ def provider_state
74
+ return @provider_state = 'IDP_INITIATED' if valid_idp_initiated_flow
75
+
76
+ session.delete(:osso_oauth_state)
77
+ end
78
+
79
+ def valid_idp_initiated_flow
80
+ !session[:osso_oauth_redirect_uri] && !session[:osso_oauth_state]
64
81
  end
65
82
  end
66
83
  end