osso 0.0.3.15 → 0.0.3.20

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 (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