osso 0.0.5.pre.lambda → 0.0.6

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.yml +6 -4
  3. data/.github/dependabot.yml +8 -0
  4. data/.github/workflows/automerge.yml +19 -0
  5. data/.rubocop.yml +4 -1
  6. data/Gemfile +1 -1
  7. data/Gemfile.lock +48 -27
  8. data/bin/annotate +3 -1
  9. data/db/schema.rb +40 -3
  10. data/lib/osso.rb +0 -1
  11. data/lib/osso/db/migrate/20201023142158_add_rodauth_tables.rb +47 -0
  12. data/lib/osso/db/migrate/20201105122026_add_token_index_to_access_tokens.rb +5 -0
  13. data/lib/osso/db/migrate/20201106154936_add_requested_to_authorization_codes_and_access_tokens.rb +6 -0
  14. data/lib/osso/db/migrate/20201109160851_add_sso_issuer_to_identity_providers.rb +12 -0
  15. data/lib/osso/db/migrate/20201110190754_remove_oauth_client_id_from_enterprise_accounts.rb +9 -0
  16. data/lib/osso/db/migrate/20201112160120_add_ping_to_identity_provider_service_enum.rb +28 -0
  17. data/lib/osso/error/account_configuration_error.rb +1 -0
  18. data/lib/osso/error/oauth_error.rb +6 -3
  19. data/lib/osso/graphql/mutation.rb +1 -0
  20. data/lib/osso/graphql/mutations.rb +1 -0
  21. data/lib/osso/graphql/mutations/create_enterprise_account.rb +0 -7
  22. data/lib/osso/graphql/mutations/create_identity_provider.rb +7 -6
  23. data/lib/osso/graphql/mutations/invite_admin_user.rb +43 -0
  24. data/lib/osso/graphql/query.rb +8 -0
  25. data/lib/osso/graphql/resolvers/enterprise_accounts.rb +2 -2
  26. data/lib/osso/graphql/types.rb +2 -2
  27. data/lib/osso/graphql/types/admin_user.rb +9 -0
  28. data/lib/osso/graphql/types/base_object.rb +1 -1
  29. data/lib/osso/graphql/types/identity_provider.rb +2 -0
  30. data/lib/osso/graphql/types/identity_provider_service.rb +2 -1
  31. data/lib/osso/lib/app_config.rb +1 -1
  32. data/lib/osso/lib/route_map.rb +0 -16
  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 +18 -4
  39. data/lib/osso/models/models.rb +1 -0
  40. data/lib/osso/models/oauth_client.rb +0 -1
  41. data/lib/osso/routes/admin.rb +39 -33
  42. data/lib/osso/routes/auth.rb +9 -9
  43. data/lib/osso/routes/oauth.rb +34 -16
  44. data/lib/osso/version.rb +1 -1
  45. data/lib/osso/views/admin.erb +5 -0
  46. data/lib/osso/views/error.erb +1 -0
  47. data/lib/osso/views/layout.erb +0 -0
  48. data/lib/osso/views/multiple_providers.erb +1 -0
  49. data/lib/osso/views/welcome.erb +0 -0
  50. data/lib/tasks/bootstrap.rake +25 -4
  51. data/osso-rb.gemspec +5 -0
  52. data/spec/factories/account.rb +24 -0
  53. data/spec/factories/enterprise_account.rb +11 -3
  54. data/spec/factories/identity_providers.rb +10 -2
  55. data/spec/factories/user.rb +4 -0
  56. data/spec/graphql/mutations/configure_identity_provider_spec.rb +1 -1
  57. data/spec/graphql/mutations/create_enterprise_account_spec.rb +0 -14
  58. data/spec/graphql/mutations/create_identity_provider_spec.rb +59 -8
  59. data/spec/graphql/query/identity_provider_spec.rb +2 -2
  60. data/spec/models/enterprise_account_spec.rb +18 -0
  61. data/spec/models/identity_provider_spec.rb +24 -3
  62. data/spec/routes/admin_spec.rb +7 -41
  63. data/spec/routes/auth_spec.rb +17 -18
  64. data/spec/routes/oauth_spec.rb +87 -5
  65. data/spec/spec_helper.rb +3 -3
  66. data/spec/support/views/layout.erb +1 -0
  67. metadata +98 -7
  68. data/lib/osso/helpers/auth.rb +0 -94
  69. data/lib/osso/helpers/helpers.rb +0 -8
  70. data/spec/helpers/auth_spec.rb +0 -269
@@ -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,9 +8,9 @@ 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
@@ -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
@@ -27,15 +27,36 @@ describe Osso::Models::IdentityProvider do
27
27
  describe '#acs_url_validator' do
28
28
  it 'returns a regex escaped string' do
29
29
  allow(subject).to receive(:acs_url).and_return(
30
- 'https://foo.com/auth/saml/callback'
30
+ 'https://foo.com/auth/saml/callback',
31
31
  )
32
32
 
33
33
  expect(subject.acs_url_validator).to eq(
34
- 'https://foo\\.com/auth/saml/callback'
34
+ 'https://foo\\.com/auth/saml/callback',
35
35
  )
36
36
  end
37
37
  end
38
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
39
60
 
40
61
  describe '#saml_options' do
41
62
  it 'returns the required args' do
@@ -44,7 +65,7 @@ describe Osso::Models::IdentityProvider do
44
65
  domain: subject.domain,
45
66
  idp_cert: subject.sso_cert,
46
67
  idp_sso_target_url: subject.sso_url,
47
- issuer: subject.domain,
68
+ issuer: subject.sso_issuer,
48
69
  )
49
70
  end
50
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
@@ -3,15 +3,16 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Osso::Oauth do
6
+ before do
7
+ described_class.set(:views, spec_views)
8
+ end
9
+
6
10
  let(:client) { create(:oauth_client) }
7
11
 
8
12
  describe 'get /oauth/authorize' do
9
13
  describe 'with a valid client ID and redirect URI' do
10
14
  describe 'for a domain that does not belong to an enterprise' do
11
- # TODO: better error handling and test
12
15
  it 'renders an error page' do
13
- described_class.set(:views, spec_views)
14
-
15
16
  create(:enterprise_with_okta, domain: 'foo.com')
16
17
 
17
18
  get(
@@ -59,7 +60,64 @@ describe Osso::Oauth do
59
60
  )
60
61
 
61
62
  expect(last_response).to be_ok
62
- expect(last_response.body).to eq("MULITPLE PROVIDERS")
63
+ expect(last_response.body).to eq('MULITPLE PROVIDERS')
64
+ end
65
+ end
66
+
67
+ describe "for an existing user's email address" do
68
+ it 'redirects to /auth/saml/:provider_id' do
69
+ enterprise = create(:enterprise_with_okta, oauth_client: client)
70
+ provider_id = enterprise.identity_providers.first.id
71
+ user = create(:user, email: "user@#{enterprise.domain}", identity_provider_id: provider_id)
72
+
73
+ get(
74
+ '/oauth/authorize',
75
+ email: user.email,
76
+ client_id: client.identifier,
77
+ response_type: 'code',
78
+ redirect_uri: client.redirect_uri_values.sample,
79
+ )
80
+
81
+ expect(last_response).to be_redirect
82
+ follow_redirect!
83
+ expect(last_request.url).to match("auth/saml/#{provider_id}")
84
+ end
85
+ end
86
+
87
+ describe "for a new user's email address belonging to an enterprise with one SAML provider" do
88
+ it 'redirects to /auth/saml/:provider_id' do
89
+ enterprise = create(:enterprise_with_okta, oauth_client: client)
90
+
91
+ get(
92
+ '/oauth/authorize',
93
+ email: "user@#{enterprise.domain}",
94
+ client_id: client.identifier,
95
+ response_type: 'code',
96
+ redirect_uri: client.redirect_uri_values.sample,
97
+ )
98
+
99
+ provider_id = enterprise.identity_providers.first.id
100
+
101
+ expect(last_response).to be_redirect
102
+ follow_redirect!
103
+ expect(last_request.url).to match("auth/saml/#{provider_id}")
104
+ end
105
+ end
106
+
107
+ describe "for a new user's email address belonging to an enterprise with multiple SAML providers" do
108
+ it 'renders the multiple providers screen' do
109
+ enterprise = create(:enterprise_with_multiple_providers, oauth_client: client)
110
+
111
+ get(
112
+ '/oauth/authorize',
113
+ email: "user@#{enterprise.domain}",
114
+ client_id: client.identifier,
115
+ response_type: 'code',
116
+ redirect_uri: client.redirect_uri_values.sample,
117
+ )
118
+
119
+ expect(last_response).to be_ok
120
+ expect(last_response.body).to eq('MULITPLE PROVIDERS')
63
121
  end
64
122
  end
65
123
  end
@@ -91,7 +149,7 @@ describe Osso::Oauth do
91
149
  end
92
150
 
93
151
  describe 'get /oauth/me' do
94
- describe 'with a valid unexpired access token' do
152
+ describe 'with a valid unexpired access token in params' do
95
153
  it 'returns the user' do
96
154
  user = create(:user)
97
155
  code = user.authorization_codes.valid.first
@@ -106,6 +164,30 @@ describe Osso::Oauth do
106
164
  email: user.email,
107
165
  id: user.id,
108
166
  idp: 'Okta',
167
+ requested: code.requested.symbolize_keys,
168
+ )
169
+ end
170
+ end
171
+
172
+ describe 'with a valid unexpired access token in headers' do
173
+ it 'returns the user' do
174
+ user = create(:user)
175
+ code = user.authorization_codes.valid.first
176
+
177
+ get(
178
+ '/oauth/me',
179
+ nil,
180
+ {
181
+ 'HTTP_AUTHORIZATION' => "Bearer: #{code.access_token.to_bearer_token}",
182
+ },
183
+ )
184
+
185
+ expect(last_response.status).to eq(200)
186
+ expect(last_json_response).to eq(
187
+ email: user.email,
188
+ id: user.id,
189
+ idp: 'Okta',
190
+ requested: code.requested.symbolize_keys,
109
191
  )
110
192
  end
111
193
  end