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.
- checksums.yaml +4 -4
- data/.buildkite/pipeline.yml +9 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +10 -2
- data/README.md +3 -2
- data/db/schema.rb +9 -1
- data/lib/osso/db/migrate/20200826201852_create_app_config.rb +11 -0
- data/lib/osso/graphql/mutation.rb +7 -0
- data/lib/osso/graphql/mutations.rb +1 -0
- data/lib/osso/graphql/mutations/base_mutation.rb +18 -5
- data/lib/osso/graphql/mutations/configure_identity_provider.rb +8 -10
- data/lib/osso/graphql/mutations/create_enterprise_account.rb +7 -0
- data/lib/osso/graphql/mutations/create_identity_provider.rb +14 -5
- data/lib/osso/graphql/mutations/create_oauth_client.rb +1 -3
- data/lib/osso/graphql/mutations/delete_enterprise_account.rb +9 -11
- data/lib/osso/graphql/mutations/delete_oauth_client.rb +1 -3
- data/lib/osso/graphql/mutations/regenerate_oauth_credentials.rb +1 -3
- data/lib/osso/graphql/mutations/set_redirect_uris.rb +2 -4
- data/lib/osso/graphql/mutations/update_app_config.rb +29 -0
- data/lib/osso/graphql/query.rb +14 -0
- data/lib/osso/graphql/resolvers.rb +1 -0
- data/lib/osso/graphql/resolvers/base_resolver.rb +25 -0
- data/lib/osso/graphql/resolvers/enterprise_account.rb +1 -11
- data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
- data/lib/osso/graphql/resolvers/oauth_clients.rb +2 -2
- data/lib/osso/graphql/types.rb +2 -1
- data/lib/osso/graphql/types/admin_user.rb +22 -0
- data/lib/osso/graphql/types/app_config.rb +22 -0
- data/lib/osso/graphql/types/base_object.rb +22 -0
- data/lib/osso/graphql/types/enterprise_account.rb +0 -5
- data/lib/osso/graphql/types/identity_provider.rb +0 -6
- data/lib/osso/graphql/types/oauth_client.rb +2 -4
- data/lib/osso/graphql/types/redirect_uri.rb +2 -4
- data/lib/osso/helpers/auth.rb +40 -18
- data/lib/osso/lib/route_map.rb +2 -2
- data/lib/osso/models/app_config.rb +33 -0
- data/lib/osso/models/models.rb +1 -0
- data/lib/osso/models/oauth_client.rb +3 -2
- data/lib/osso/models/redirect_uri.rb +0 -11
- data/lib/osso/routes/admin.rb +8 -2
- data/lib/osso/routes/auth.rb +29 -12
- data/lib/osso/routes/oauth.rb +25 -18
- data/lib/osso/version.rb +1 -1
- data/lib/tasks/bootstrap.rake +2 -0
- data/spec/graphql/mutations/configure_identity_provider_spec.rb +17 -4
- data/spec/graphql/mutations/create_enterprise_account_spec.rb +53 -4
- data/spec/graphql/mutations/create_identity_provider_spec.rb +18 -6
- data/spec/graphql/mutations/create_oauth_client_spec.rb +10 -3
- data/spec/graphql/mutations/delete_enterprise_account_spec.rb +18 -4
- data/spec/graphql/mutations/delete_oauth_client_spec.rb +8 -4
- data/spec/graphql/query/enterprise_account_spec.rb +21 -6
- data/spec/graphql/query/enterprise_accounts_spec.rb +4 -2
- data/spec/graphql/query/identity_provider_spec.rb +16 -6
- data/spec/graphql/query/oauth_clients_spec.rb +10 -7
- data/spec/helpers/auth_spec.rb +97 -0
- data/spec/routes/auth_spec.rb +18 -0
- data/spec/routes/oauth_spec.rb +5 -2
- data/spec/spec_helper.rb +3 -0
- data/spec/support/views/error.erb +0 -0
- metadata +10 -3
- 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 <
|
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 <
|
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:
|
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 <
|
6
|
+
class OAuthClients < BaseResolver
|
7
7
|
type [Types::OauthClient], null: true
|
8
8
|
|
9
9
|
def resolve
|
10
|
-
|
10
|
+
Osso::Models::OauthClient.all
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/lib/osso/graphql/types.rb
CHANGED
@@ -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?(
|
28
|
-
|
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?(
|
18
|
-
|
15
|
+
def self.authorized?(_object, context)
|
16
|
+
context[:scope] == 'admin'
|
19
17
|
end
|
20
18
|
end
|
21
19
|
end
|
data/lib/osso/helpers/auth.rb
CHANGED
@@ -3,10 +3,21 @@
|
|
3
3
|
module Osso
|
4
4
|
module Helpers
|
5
5
|
module Auth
|
6
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
36
|
-
payload, _args = decode(token)
|
41
|
+
private
|
37
42
|
|
38
|
-
|
39
|
-
|
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
|
data/lib/osso/lib/route_map.rb
CHANGED
@@ -11,12 +11,12 @@ module Osso
|
|
11
11
|
use Osso::Oauth
|
12
12
|
|
13
13
|
post '/graphql' do
|
14
|
-
|
14
|
+
token_protected!
|
15
15
|
|
16
16
|
result = Osso::GraphQL::Schema.execute(
|
17
17
|
params[:query],
|
18
18
|
variables: params[:variables],
|
19
|
-
context:
|
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
|
+
#
|
data/lib/osso/models/models.rb
CHANGED
@@ -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
|
26
|
-
self.secret
|
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
|
data/lib/osso/routes/admin.rb
CHANGED
@@ -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
|
-
|
23
|
+
internal_protected!
|
18
24
|
|
19
25
|
erb :admin
|
20
26
|
end
|
21
27
|
|
22
28
|
get '/enterprise' do
|
23
|
-
|
29
|
+
token_protected!
|
24
30
|
|
25
31
|
erb :admin
|
26
32
|
end
|
data/lib/osso/routes/auth.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
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
|
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
|
-
|
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
|