osso 0.0.5.pre.zeta → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +4 -2
  3. data/.rubocop.yml +4 -1
  4. data/Gemfile.lock +48 -32
  5. data/LICENSE +21 -23
  6. data/bin/annotate +3 -1
  7. data/db/schema.rb +41 -3
  8. data/lib/osso/db/migrate/20200929154117_add_users_count_to_identity_providers_and_enterprise_accounts.rb +6 -0
  9. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  10. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  11. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  12. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  13. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  14. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  15. data/lib/osso/error/account_configuration_error.rb +1 -0
  16. data/lib/osso/error/oauth_error.rb +6 -3
  17. data/lib/osso/graphql/mutation.rb +2 -0
  18. data/lib/osso/graphql/mutations.rb +2 -0
  19. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  20. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  21. data/lib/osso/graphql/mutations/delete_identity_provider.rb +24 -0
  22. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  23. data/lib/osso/graphql/query.rb +8 -0
  24. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +3 -3
  25. data/lib/osso/graphql/types.rb +2 -2
  26. data/lib/osso/graphql/types/admin_user.rb +9 -0
  27. data/lib/osso/graphql/types/base_object.rb +1 -1
  28. data/lib/osso/graphql/types/enterprise_account.rb +1 -0
  29. data/lib/osso/graphql/types/identity_provider.rb +3 -0
  30. data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
  31. data/lib/osso/helpers/auth.rb +1 -1
  32. data/lib/osso/lib/route_map.rb +0 -15
  33. data/lib/osso/lib/saml_handler.rb +5 -0
  34. data/lib/osso/models/access_token.rb +4 -2
  35. data/lib/osso/models/account.rb +34 -0
  36. data/lib/osso/models/authorization_code.rb +2 -1
  37. data/lib/osso/models/enterprise_account.rb +3 -1
  38. data/lib/osso/models/identity_provider.rb +23 -5
  39. data/lib/osso/models/models.rb +1 -0
  40. data/lib/osso/models/oauth_client.rb +0 -1
  41. data/lib/osso/models/user.rb +2 -2
  42. data/lib/osso/routes/admin.rb +39 -33
  43. data/lib/osso/routes/auth.rb +9 -9
  44. data/lib/osso/routes/oauth.rb +35 -17
  45. data/lib/osso/version.rb +1 -1
  46. data/lib/osso/views/admin.erb +5 -0
  47. data/lib/osso/views/error.erb +1 -0
  48. data/lib/osso/views/layout.erb +0 -0
  49. data/lib/osso/views/multiple_providers.erb +1 -0
  50. data/lib/osso/views/welcome.erb +0 -0
  51. data/lib/tasks/bootstrap.rake +18 -4
  52. data/osso-rb.gemspec +5 -0
  53. data/spec/factories/account.rb +24 -0
  54. data/spec/factories/enterprise_account.rb +11 -3
  55. data/spec/factories/identity_providers.rb +10 -2
  56. data/spec/factories/user.rb +4 -0
  57. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  58. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  59. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  60. data/spec/graphql/query/identity_provider_spec.rb +3 -2
  61. data/spec/models/enterprise_account_spec.rb +18 -0
  62. data/spec/models/identity_provider_spec.rb +35 -1
  63. data/spec/routes/admin_spec.rb +7 -41
  64. data/spec/routes/auth_spec.rb +17 -18
  65. data/spec/routes/oauth_spec.rb +88 -5
  66. data/spec/spec_helper.rb +3 -3
  67. data/spec/support/views/layout.erb +1 -0
  68. data/spec/support/views/multiple_providers.erb +1 -0
  69. metadata +92 -5
  70. data/spec/helpers/auth_spec.rb +0 -97
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB = Sequel.postgres(extensions: :activerecord_connection)
4
+
5
+ FactoryBot.define do
6
+ factory :account, class: Osso::Models::Account do
7
+ id { SecureRandom.uuid }
8
+ email { Faker::Internet.email }
9
+ end
10
+
11
+ factory :verified_account, parent: :account do
12
+ transient do
13
+ password { SecureRandom.urlsafe_base64(8) }
14
+ end
15
+ status_id { 2 }
16
+
17
+ after :create do |account|
18
+ DB[:account_password_hashes].insert(
19
+ id: account.id,
20
+ password_hash: BCrypt::Password.create('secret', cost: BCrypt::Engine::MIN_COST).to_s,
21
+ )
22
+ end
23
+ end
24
+ end
@@ -2,19 +2,22 @@
2
2
 
3
3
  FactoryBot.define do
4
4
  factory :enterprise_account, class: Osso::Models::EnterpriseAccount do
5
+ transient do
6
+ oauth_client { create(:oauth_client) }
7
+ end
5
8
  id { SecureRandom.uuid }
6
9
  name { Faker::Company.name }
7
10
  domain { Faker::Internet.domain_name }
8
- oauth_client
9
11
  end
10
12
 
11
13
  factory :enterprise_with_okta, parent: :enterprise_account do
12
- after :create do |enterprise|
14
+ after :create do |enterprise, evaluator|
13
15
  create(
14
16
  :configured_identity_provider,
15
17
  service: 'OKTA',
16
18
  domain: enterprise.domain,
17
19
  enterprise_account_id: enterprise.id,
20
+ oauth_client: evaluator.oauth_client,
18
21
  )
19
22
  end
20
23
  end
@@ -31,12 +34,16 @@ FactoryBot.define do
31
34
  end
32
35
 
33
36
  factory :enterprise_with_multiple_providers, parent: :enterprise_account do
34
- after :create do |enterprise|
37
+ transient do
38
+ oauth_client { nil }
39
+ end
40
+ after :create do |enterprise, evaluator|
35
41
  create(
36
42
  :configured_identity_provider,
37
43
  service: 'OKTA',
38
44
  domain: enterprise.domain,
39
45
  enterprise_account_id: enterprise.id,
46
+ oauth_client: evaluator.oauth_client,
40
47
  )
41
48
 
42
49
  create(
@@ -44,6 +51,7 @@ FactoryBot.define do
44
51
  service: 'AZURE',
45
52
  domain: enterprise.domain,
46
53
  enterprise_account_id: enterprise.id,
54
+ oauth_client: evaluator.oauth_client,
47
55
  )
48
56
  end
49
57
  end
@@ -21,6 +21,13 @@ FactoryBot.define do
21
21
  end
22
22
  end
23
23
 
24
+ factory :ping_identity_provider, parent: :identity_provider do
25
+ service { 'PING' }
26
+ sso_url do
27
+ 'https://auth.pingone.com/42cd503f-f0ba-47c7-a5b5-e69e9d8fab47/saml20/idp/sso'
28
+ end
29
+ end
30
+
24
31
  factory :configured_identity_provider, parent: :identity_provider do
25
32
  status { 'CONFIGURED' }
26
33
  sso_cert do
@@ -55,15 +62,16 @@ end
55
62
  # Table name: identity_providers
56
63
  #
57
64
  # id :uuid not null, primary key
58
- # service :string
65
+ # service :enum
59
66
  # domain :string not null
60
67
  # sso_url :string
61
68
  # sso_cert :text
62
69
  # enterprise_account_id :uuid
63
70
  # oauth_client_id :uuid
64
- # status :enum default("PENDING")
71
+ # status :enum default("pending")
65
72
  # created_at :datetime
66
73
  # updated_at :datetime
74
+ # users_count :integer default(0)
67
75
  #
68
76
  # Indexes
69
77
  #
@@ -12,6 +12,10 @@ FactoryBot.define do
12
12
  :authorization_code,
13
13
  user: user,
14
14
  redirect_uri: user.oauth_client.redirect_uri_values.sample,
15
+ requested: [
16
+ { domain: user.email.split('@')[1], email: nil },
17
+ { domain: nil, email: user.email },
18
+ ].sample,
15
19
  )
16
20
  end
17
21
  end
@@ -81,7 +81,7 @@ describe Osso::GraphQL::Schema do
81
81
  end
82
82
 
83
83
  it 'does not configure an identity provider' do
84
- expect(subject.dig('errors')).to_not be_empty
84
+ expect(subject['errors']).to_not be_empty
85
85
  end
86
86
  end
87
87
  end
@@ -47,7 +47,6 @@ describe Osso::GraphQL::Schema do
47
47
  input: {
48
48
  name: Faker::Company.name,
49
49
  domain: domain,
50
- oauthClientId: oauth_client.id,
51
50
  },
52
51
  }
53
52
  end
@@ -57,10 +56,6 @@ describe Osso::GraphQL::Schema do
57
56
  expect(subject.dig('data', 'createEnterpriseAccount', 'enterpriseAccount', 'domain')).
58
57
  to eq(domain)
59
58
  end
60
-
61
- it 'attaches the Enterprise Account to the correct OAuth Client' do
62
- expect { subject }.to change { oauth_client.enterprise_accounts.count }.by(1)
63
- end
64
59
  end
65
60
 
66
61
  describe 'for an internal scoped user' do
@@ -68,7 +63,6 @@ describe Osso::GraphQL::Schema do
68
63
  {
69
64
  scope: 'internal',
70
65
  email: 'user@saasco.com',
71
- oauth_client_id: oauth_client.identifier,
72
66
  }
73
67
  end
74
68
 
@@ -77,10 +71,6 @@ describe Osso::GraphQL::Schema do
77
71
  expect(subject.dig('data', 'createEnterpriseAccount', 'enterpriseAccount', 'domain')).
78
72
  to eq(domain)
79
73
  end
80
-
81
- it 'attaches the Enterprise Account to the correct OAuth Client' do
82
- expect { subject }.to change { oauth_client.enterprise_accounts.count }.by(1)
83
- end
84
74
  end
85
75
 
86
76
  describe 'for an email scoped user' do
@@ -97,10 +87,6 @@ describe Osso::GraphQL::Schema do
97
87
  expect(subject.dig('data', 'createEnterpriseAccount', 'enterpriseAccount', 'domain')).
98
88
  to eq(domain)
99
89
  end
100
-
101
- it 'attaches the Enterprise Account to the correct OAuth Client' do
102
- expect { subject }.to change { oauth_client.enterprise_accounts.count }.by(1)
103
- end
104
90
  end
105
91
  describe 'for the wrong email scoped user' do
106
92
  let(:current_context) do
@@ -5,6 +5,7 @@ require 'spec_helper'
5
5
  describe Osso::GraphQL::Schema do
6
6
  describe 'CreateIdentityProvider' do
7
7
  let(:enterprise_account) { create(:enterprise_account) }
8
+ let(:oauth_client) { create(:oauth_client) }
8
9
  let(:mutation) do
9
10
  <<~GRAPHQL
10
11
  mutation CreateIdentityProvider($input: CreateIdentityProviderInput!) {
@@ -34,7 +35,15 @@ describe Osso::GraphQL::Schema do
34
35
  { scope: 'admin' }
35
36
  end
36
37
  describe 'without a service' do
37
- let(:variables) { { input: { enterpriseAccountId: enterprise_account.id } } }
38
+ let(:variables) do
39
+ {
40
+ input:
41
+ {
42
+ enterpriseAccountId: enterprise_account.id,
43
+ oauthClientId: oauth_client.id,
44
+ },
45
+ }
46
+ end
38
47
 
39
48
  it 'creates an identity provider' do
40
49
  expect { subject }.to change { enterprise_account.identity_providers.count }.by(1)
@@ -44,8 +53,16 @@ describe Osso::GraphQL::Schema do
44
53
  end
45
54
 
46
55
  describe 'with a service' do
47
- let(:variables) { { input: { enterpriseAccountId: enterprise_account.id, service: 'OKTA' } } }
48
-
56
+ let(:variables) do
57
+ {
58
+ input:
59
+ {
60
+ enterpriseAccountId: enterprise_account.id,
61
+ service: 'OKTA',
62
+ oauthClientId: oauth_client.id,
63
+ },
64
+ }
65
+ end
49
66
  it 'creates an identity provider for given service ' do
50
67
  expect { subject }.to change { enterprise_account.identity_providers.count }.by(1)
51
68
  expect(subject.dig('data', 'createIdentityProvider', 'identityProvider', 'service')).
@@ -65,8 +82,16 @@ describe Osso::GraphQL::Schema do
65
82
  let(:enterprise_account) { create(:enterprise_account, domain: domain) }
66
83
 
67
84
  describe 'without a service' do
68
- let(:variables) { { input: { enterpriseAccountId: enterprise_account.id } } }
69
-
85
+ let(:variables) do
86
+ {
87
+ input:
88
+ {
89
+ enterpriseAccountId: enterprise_account.id,
90
+ oauthClientId: oauth_client.id,
91
+ },
92
+ }
93
+ end
94
+
70
95
  it 'creates an identity provider' do
71
96
  expect { subject }.to change { enterprise_account.identity_providers.count }.by(1)
72
97
  expect(subject.dig('data', 'createIdentityProvider', 'identityProvider', 'domain')).
@@ -75,7 +100,16 @@ describe Osso::GraphQL::Schema do
75
100
  end
76
101
 
77
102
  describe 'with a service' do
78
- let(:variables) { { input: { enterpriseAccountId: enterprise_account.id, service: 'OKTA' } } }
103
+ let(:variables) do
104
+ {
105
+ input:
106
+ {
107
+ enterpriseAccountId: enterprise_account.id,
108
+ oauthClientId: oauth_client.id,
109
+ service: 'OKTA',
110
+ },
111
+ }
112
+ end
79
113
 
80
114
  it 'creates an identity provider for given service ' do
81
115
  expect { subject }.to change { enterprise_account.identity_providers.count }.by(1)
@@ -97,7 +131,15 @@ describe Osso::GraphQL::Schema do
97
131
  let(:target_account) { create(:enterprise_account) }
98
132
 
99
133
  describe 'without a service' do
100
- let(:variables) { { input: { enterpriseAccountId: target_account.id, domain: domain } } }
134
+ let(:variables) do
135
+ {
136
+ input:
137
+ {
138
+ enterpriseAccountId: target_account.id,
139
+ oauthClientId: oauth_client.id,
140
+ },
141
+ }
142
+ end
101
143
 
102
144
  it 'does not creates a identity provider' do
103
145
  expect { subject }.to_not(change { Osso::Models::IdentityProvider.count })
@@ -105,7 +147,16 @@ describe Osso::GraphQL::Schema do
105
147
  end
106
148
 
107
149
  describe 'with a service' do
108
- let(:variables) { { input: { enterpriseAccountId: target_account.id, service: 'OKTA', domain: domain } } }
150
+ let(:variables) do
151
+ {
152
+ input:
153
+ {
154
+ enterpriseAccountId: target_account.id,
155
+ service: 'OKTA',
156
+ oauthClientId: oauth_client.id,
157
+ },
158
+ }
159
+ end
109
160
 
110
161
  it 'does not creates a identity provider' do
111
162
  expect { subject }.to_not(change { Osso::Models::IdentityProvider.count })
@@ -8,13 +8,14 @@ describe Osso::GraphQL::Schema do
8
8
  let(:domain) { Faker::Internet.domain_name }
9
9
  let(:variables) { { id: id } }
10
10
  let(:query) do
11
- <<~GRAPHQL
11
+ <<-GRAPHQL
12
12
  query IdentityProvider($id: ID!) {
13
- identityProvider(id: $id) {
13
+ identityProvider(id: $id) {
14
14
  id
15
15
  service
16
16
  domain
17
17
  acsUrl
18
+ acsUrlValidator
18
19
  ssoCert
19
20
  ssoUrl
20
21
  status
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Osso::Models::EnterpriseAccount do
6
+ describe 'validates_domain' do
7
+ it 'it returns false for an invalid domain' do
8
+ customer = described_class.new(
9
+ name: 'foo',
10
+ domain: ' foo.com',
11
+ )
12
+
13
+ customer.save
14
+
15
+ expect(customer.errors[:domain]).to include('is invalid')
16
+ end
17
+ end
18
+ end
@@ -24,6 +24,40 @@ describe Osso::Models::IdentityProvider do
24
24
  end
25
25
  end
26
26
 
27
+ describe '#acs_url_validator' do
28
+ it 'returns a regex escaped string' do
29
+ allow(subject).to receive(:acs_url).and_return(
30
+ 'https://foo.com/auth/saml/callback',
31
+ )
32
+
33
+ expect(subject.acs_url_validator).to eq(
34
+ 'https://foo\\.com/auth/saml/callback',
35
+ )
36
+ end
37
+ end
38
+
39
+ describe '#sso_issuer' do
40
+ it 'returns a url unique to self' do
41
+ ENV['HEROKU_APP_NAME'] = nil
42
+ ENV['BASE_URL'] = 'https://example.com'
43
+
44
+ expect(subject.sso_issuer).to eq(
45
+ "#{subject.domain}/#{subject.oauth_client_id}",
46
+ )
47
+ end
48
+
49
+ it 'returns a uri with protocol when required' do
50
+ ENV['HEROKU_APP_NAME'] = nil
51
+ ENV['BASE_URL'] = 'https://example.com'
52
+
53
+ idp = create(:ping_identity_provider)
54
+
55
+ expect(idp.sso_issuer).to eq(
56
+ "https://#{idp.domain}/#{idp.oauth_client_id}",
57
+ )
58
+ end
59
+ end
60
+
27
61
  describe '#saml_options' do
28
62
  it 'returns the required args' do
29
63
  expect(subject.saml_options).
@@ -31,7 +65,7 @@ describe Osso::Models::IdentityProvider do
31
65
  domain: subject.domain,
32
66
  idp_cert: subject.sso_cert,
33
67
  idp_sso_target_url: subject.sso_url,
34
- issuer: subject.domain,
68
+ issuer: subject.sso_issuer,
35
69
  )
36
70
  end
37
71
  end
@@ -3,56 +3,22 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Osso::Admin do
6
- let(:jwt_url) { 'https://foo.com/jwt' }
7
- let(:jwt_hmac_secret) { SecureRandom.hex(32) }
8
-
9
- before do
10
- ENV['JWT_URL'] = jwt_url
11
- ENV['JWT_HMAC_SECRET'] = jwt_hmac_secret
12
- described_class.set(:views, spec_views)
13
- end
14
-
15
6
  describe 'get /admin' do
16
- it 'redirects to JWT_URL without a session or token' do
7
+ it 'redirects to /login without a session' do
17
8
  get('/admin')
18
9
 
19
10
  expect(last_response).to be_redirect
20
11
  follow_redirect!
21
- expect(last_request.url).to eq(jwt_url)
22
- end
23
-
24
- it 'redirects to JWT_URL with an invalid token' do
25
- get('/admin', token: SecureRandom.hex(32))
26
-
27
- expect(last_response).to be_redirect
28
-
29
- follow_redirect!
30
-
31
- expect(last_request.url).to eq(jwt_url)
12
+ expect(last_request.url).to match('/login')
32
13
  end
33
14
 
34
- it 'chomps the token and redirects to request path with valid token' do
35
- token = JWT.encode(
36
- { email: 'admin@saas.com', scope: 'admin' },
37
- jwt_hmac_secret,
38
- 'HS256',
39
- )
40
-
41
- get('/admin', { admin_token: token })
42
-
43
- expect(last_response).to be_redirect
44
- follow_redirect!
45
- expect(last_request.url).to match('/admin')
46
- end
15
+ xit 'renders the admin page for a valid session token' do
16
+ password = SecureRandom.urlsafe_base64(16)
17
+ account = create(:verified_account, password: password)
47
18
 
48
- it 'renders the admin page for a valid session token' do
49
- token = JWT.encode(
50
- { email: 'admin@saas.com', scope: 'admin' },
51
- jwt_hmac_secret,
52
- 'HS256',
53
- )
19
+ post('/login', { email: account.email, password: password })
54
20
 
55
- get('/admin', {}, 'rack.session' => { admin_token: token })
21
+ get('/admin')
56
22
 
57
23
  expect(last_response).to be_ok
58
24
  end
@@ -182,29 +182,28 @@ describe Osso::Auth do
182
182
  it 'raises an error when email is missing' do
183
183
  mock_saml_omniauth(email: nil, id: SecureRandom.uuid)
184
184
 
185
-
186
- response = post(
187
- "/auth/saml/#{azure_provider.id}/callback",
188
- nil,
189
- {
190
- 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
191
- },
192
- )
193
-
194
- expect(response.body).to eq('Osso::Error::MissingSamlEmailAttributeError')
195
- end
185
+ response = post(
186
+ "/auth/saml/#{azure_provider.id}/callback",
187
+ nil,
188
+ {
189
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
190
+ },
191
+ )
192
+
193
+ expect(response.body).to eq('Osso::Error::MissingSamlEmailAttributeError')
194
+ end
196
195
 
197
196
  it 'raises an error when id is missing' do
198
197
  mock_saml_omniauth(email: Faker::Internet.email, id: nil)
199
198
 
200
199
  response = post(
201
- "/auth/saml/#{azure_provider.id}/callback",
202
- nil,
203
- {
204
- 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
205
- },
206
- )
207
-
200
+ "/auth/saml/#{azure_provider.id}/callback",
201
+ nil,
202
+ {
203
+ 'omniauth.auth' => OmniAuth.config.mock_auth[:saml],
204
+ },
205
+ )
206
+
208
207
  expect(response.body).to eq('Osso::Error::MissingSamlIdAttributeError')
209
208
  end
210
209
  end