infinum_azure 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22a078df24ccf923263fb54f5527288599b6525afc08be03a02e831045abb22e
4
- data.tar.gz: 14122b63d4c84e3c4491f8d446092bcb8eb3712c6879df811cc814f311b74ebd
3
+ metadata.gz: 239fd243ff912422205387b16829115566d6cb14580b48bc229eddf4cbc6a9b0
4
+ data.tar.gz: 796c56fc53bfb09b5639fea39ec47fc1d7b8398df373bf77726690f3854a1d42
5
5
  SHA512:
6
- metadata.gz: d164d91027e483295911adad35c322442cc950a78a8ccb927c8c445c519172c58fc7b3682383d3e8e20bdec4ffbae77ac79ee6b77f086ea697e2d65a802bee68
7
- data.tar.gz: b6c10aa75e1f2395cb01527bf45cb3d7140db2125641b7ec7d21e8185e0af89abeabb9fa9d88bb909f6de56fde9bf13ae3ddc60d938e7e366478b876965a1ced
6
+ metadata.gz: '086ee18dff4f363beaa89c05d39a4a3330cf4cecbb8f8bcb63fe9de965372decd8cd545683b9afa7d798fc732af21094d5c45699ecefd8343f8a4ad6cc1b351a'
7
+ data.tar.gz: f8a7835a59f356ccd8b0ef1cfec8e658a73fb94316eb5c0bf0a7008971a770c74d138898c06509211a5d53f77fb734743644aca2f916f1b40bb5378eacb95c12
data/.rubocop.yml CHANGED
@@ -14,6 +14,9 @@ Style/Documentation:
14
14
  Style/SymbolArray:
15
15
  Enabled: false
16
16
 
17
+ Style/Lambda:
18
+ Enabled: false
19
+
17
20
  Metrics/BlockLength:
18
21
  Exclude:
19
22
  - '**/*.rake'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-06-15
4
+
5
+ - Slim down the gem (remove dependencies and transient dependencies):
6
+ - dry-configurable
7
+ - http
8
+ - responders
9
+ - Add `avatar_url`, `deactivated_at`, `provider_groups` and `employee` as selectable *resource_attributes*
10
+ - Add rake task `infinum_azure:migrate_users` for migrating users (updating `uid` values from Infinum ID to Infinum Azure)
11
+ - Update Readme
12
+
3
13
  ## [0.3.0] - 2023-04-21
4
14
 
5
15
  - Fix upserting new resource memoization
data/Gemfile.lock CHANGED
@@ -1,13 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- infinum_azure (0.3.0)
4
+ infinum_azure (0.4.0)
5
5
  bundler
6
6
  devise
7
- dry-configurable
8
- http
9
- omniauth-infinum_azure (>= 0.1.6, < 2.0)
10
- responders
7
+ omniauth-infinum_azure (>= 0.3.0, < 2.0)
11
8
 
12
9
  GEM
13
10
  remote: https://rubygems.org/
@@ -96,14 +93,6 @@ GEM
96
93
  warden (~> 1.2.3)
97
94
  diff-lcs (1.5.0)
98
95
  docile (1.4.0)
99
- domain_name (0.5.20190701)
100
- unf (>= 0.0.5, < 1.0.0)
101
- dry-configurable (1.0.1)
102
- dry-core (~> 1.0, < 2)
103
- zeitwerk (~> 2.6)
104
- dry-core (1.0.0)
105
- concurrent-ruby (~> 1.0)
106
- zeitwerk (~> 2.6)
107
96
  erubi (1.12.0)
108
97
  factory_bot (6.2.1)
109
98
  activesupport (>= 5.0.0)
@@ -116,24 +105,10 @@ GEM
116
105
  faraday-net_http (>= 2.0, < 3.1)
117
106
  ruby2_keywords (>= 0.0.4)
118
107
  faraday-net_http (3.0.2)
119
- ffi (1.15.5)
120
- ffi-compiler (1.0.1)
121
- ffi (>= 1.0.0)
122
- rake
123
108
  globalid (1.1.0)
124
109
  activesupport (>= 5.0)
125
110
  hashdiff (1.0.1)
126
111
  hashie (5.0.0)
127
- http (4.4.1)
128
- addressable (~> 2.3)
129
- http-cookie (~> 1.0)
130
- http-form_data (~> 2.2)
131
- http-parser (~> 1.2.0)
132
- http-cookie (1.0.5)
133
- domain_name (~> 0.5)
134
- http-form_data (2.3.0)
135
- http-parser (1.2.3)
136
- ffi-compiler (>= 1.0, < 2.0)
137
112
  i18n (1.12.0)
138
113
  concurrent-ruby (~> 1.0)
139
114
  jwt (2.7.0)
@@ -173,7 +148,7 @@ GEM
173
148
  hashie (>= 3.4.6)
174
149
  rack (>= 2.2.3)
175
150
  rack-protection
176
- omniauth-infinum_azure (0.1.6)
151
+ omniauth-infinum_azure (0.3.0)
177
152
  omniauth-oauth2
178
153
  omniauth-oauth2 (1.8.0)
179
154
  oauth2 (>= 1.4, < 3)
@@ -261,9 +236,6 @@ GEM
261
236
  timeout (0.3.2)
262
237
  tzinfo (2.0.6)
263
238
  concurrent-ruby (~> 1.0)
264
- unf (0.1.4)
265
- unf_ext
266
- unf_ext (0.0.8.2)
267
239
  version_gem (1.1.1)
268
240
  warden (1.2.9)
269
241
  rack (>= 2.0.9)
data/README.md CHANGED
@@ -29,8 +29,6 @@ Or install it yourself as:
29
29
  ## Dependencies
30
30
 
31
31
  * [Devise](https://github.com/plataformatec/devise)
32
- * [Dry configurable](https://github.com/dry-rb/dry-configurable)
33
- * [Http](https://github.com/httprb/http)
34
32
  * [Omniauth::InfinumAzure](https://github.com/infinum/ruby-infinum-azure-omniauth)
35
33
 
36
34
  ## Configuration
@@ -43,18 +41,36 @@ Or install it yourself as:
43
41
  InfinumAzure.configure do |config|
44
42
  config.service_name = 'Revisor'
45
43
  config.resource_name = 'User'
46
- config.resource_attributes = [:uid, :email, :first_name, :last_name]
44
+ config.resource_attributes = [:uid, :email, :first_name, :last_name, :avatar_url,
45
+ :deactivated_at, :provider_groups, :employee]
46
+
47
+ config.user_migration_scope = -> { resource_class.where(provider: 'infinum_id') }
48
+ config.user_migration_operation = > (record, resource) {
49
+ record.update_attribute(:provider, 'infinum_azure')
50
+ record.update_attribute(:uid, resource['uid'])
51
+ }
47
52
  end
48
53
  ```
49
54
 
50
55
  Configuration options:
51
- * Service name - name of application
52
- * Resource name - name of resource on whom authentication is being done
53
- * Resource attributes - attributes sent from InfinumAzure when user is created/updated that will be permitted
56
+ * service_name(mandatory) - name of application
57
+ * resource_name(mandatory) - name of resource on whom authentication is being done
58
+ * resource_attributes(optional) - attributes that will be permitted once the webhook controller receives the params from InfinumAzure
59
+ * user_migration_scope(optional) - a block that will be used to get the initial collection of resources (if blank, default is written above)
60
+ * user_migration_operation(optional) - a block that will be called for each resource from the above collection if a matching resource on InfinumAzure is found. The resource is a Hash containing the following properties:
61
+ * `uid` - string
62
+ * `first_name` - string || null
63
+ * `last_name` - string || null
64
+ * `email` - string
65
+ * `avatar_url` - string || null
66
+ * `groups` - string || null -> a comma separated list; if "employees" is present, the user is an employee
67
+ * `deactivated` - boolean
54
68
 
55
69
  ### Secrets
56
70
 
57
- Needed secrets:
71
+ Secrets should be kept in `config/secrets.yml` file.
72
+
73
+ Required ones are:
58
74
 
59
75
  ```ruby
60
76
  # config/secrets.yml
@@ -65,19 +81,44 @@ infinum_azure:
65
81
  tenant: 'InfinumAzure_tenant'
66
82
  ```
67
83
 
84
+ Optional ones are:
85
+
86
+ ```ruby
87
+ infinum_azure:
88
+ users_auth_url: 'InfinumAzure_users_auth_url_with_api_code' # required only if infinum_azure:migrate_users rake task is used
89
+ ```
90
+
68
91
  ## Usage
69
92
 
70
93
  1. Add columns to resource via migration.
71
94
 
72
- <b>Required columns:</b> email, uid and provider <br />
73
- <b>Optional columns:</b> name
95
+ <b>Required columns:</b>
96
+ * *email* _string_
97
+ * *uid* _string_
98
+ * *provider* _string_
99
+ * *remember_created_at* _datetime_
100
+ * *remember_token* _string_
101
+
102
+ <b>Optional columns:</b>
103
+ * *first_name* _string_
104
+ * *last_name* _string_
105
+ * *avatar_url* _string_
106
+ * *deactivated_at* _datetime_
107
+ * *provider_groups* _jsonb array_
108
+ * *employee* _boolean_
74
109
 
75
110
  2. Add following rows to resource model:
76
111
 
77
112
  ```ruby
78
- devise :omniauthable, omniauth_providers: [:infinum_azure]
113
+ devise :rememberable, :omniauthable, omniauth_providers: [:infinum_azure]
114
+
115
+ def remember_me
116
+ true
117
+ end
79
118
  ```
80
119
 
120
+ _NOTE_: The `#remember_me` method needs to always return *true* in order for users to stay logged in after they shut down their browsers. In case your app has a checkbox for `Remember me` on the login page next to the login button, you can override the return value.
121
+
81
122
  3. Use devise's method `#authenticate_user!` to authenticate users on API endpoints
82
123
 
83
124
  ```ruby
@@ -12,7 +12,7 @@ module InfinumAzure
12
12
  action = 'created'
13
13
  end
14
14
 
15
- InfinumAzure::AfterUpsertResource.call(resource, params[:user])
15
+ InfinumAzure::AfterUpsertResource.call(resource, normalized_azure_params)
16
16
 
17
17
  render json: { resource_name.underscore => action }
18
18
  end
@@ -26,9 +26,15 @@ module InfinumAzure
26
26
  end
27
27
 
28
28
  def user_params
29
- params.require(:user)
30
- .permit(InfinumAzure.resource_attributes)
31
- .merge(provider: InfinumAzure.provider)
29
+ normalized_azure_params
30
+ .slice(*InfinumAzure.resource_attributes)
31
+ .merge(provider: InfinumAzure.provider)
32
+ end
33
+
34
+ def normalized_azure_params
35
+ InfinumAzure::Resources::Params.normalize(
36
+ params.require(:user).permit!.to_h.symbolize_keys
37
+ )
32
38
  end
33
39
  end
34
40
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfinumAzure
4
+ module Resources
5
+ class Params
6
+ NORMALIZATIONS = {
7
+ uid: :propagate,
8
+ email: :propagate,
9
+ first_name: :propagate,
10
+ last_name: :propagate,
11
+ avatar_url: :propagate,
12
+ deactivated_at: {
13
+ procedure: ->(value) { [false, nil].include?(value) ? nil : Time.zone.now },
14
+ target_name: :deactivated
15
+ },
16
+ employee: {
17
+ procedure: ->(value) { value&.include?('employees') },
18
+ target_name: :groups
19
+ }
20
+ }.freeze
21
+
22
+ def self.normalize(payload)
23
+ new(payload).as_json
24
+ end
25
+
26
+ def initialize(params = {})
27
+ NORMALIZATIONS.each do |attribute, operation|
28
+ value = if operation == :propagate
29
+ params[attribute]
30
+ elsif operation.is_a?(Hash)
31
+ operation[:procedure].call(params[operation[:target_name]])
32
+ else
33
+ raise 'unsupported normalization'
34
+ end
35
+
36
+ instance_variable_set("@#{attribute}", value)
37
+ end
38
+ end
39
+
40
+ def as_json
41
+ NORMALIZATIONS.keys.reduce({}) do |hash, key|
42
+ hash[key] = instance_variable_get("@#{key}")
43
+
44
+ hash
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader(*NORMALIZATIONS.keys)
51
+ end
52
+ end
53
+ end
@@ -46,8 +46,5 @@ Gem::Specification.new do |spec|
46
46
 
47
47
  spec.add_dependency 'bundler'
48
48
  spec.add_dependency 'devise'
49
- spec.add_dependency 'dry-configurable'
50
- spec.add_dependency 'http'
51
- spec.add_dependency 'omniauth-infinum_azure', '>= 0.1.6', '< 2.0'
52
- spec.add_dependency 'responders'
49
+ spec.add_dependency 'omniauth-infinum_azure', '>= 0.3.0', '< 2.0'
53
50
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfinumAzure
4
+ class Config
5
+ Defaults.all_attributes.each do |attr, value|
6
+ attr_writer attr
7
+
8
+ define_method(attr) do
9
+ instance_variable_set("@#{attr}", instance_variable_get("@#{attr}") || value)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfinumAzure
4
+ module Defaults
5
+ REQUIRED = {
6
+ service_name: nil,
7
+ resource_name: nil
8
+ }.freeze
9
+ OPTIONAL = {
10
+ resource_attributes: [
11
+ :uid, :email, :first_name, :last_name, :avatar_url, :deactivated_at, :provider_groups, :employee
12
+ ],
13
+ user_migration_scope: -> { InfinumAzure.resource_class.where(provider: 'infinum_id') },
14
+ user_migration_operation: ->(record, resource) {
15
+ record.update_attribute(:provider, provider)
16
+ record.update_attribute(:uid, resource['uid'])
17
+ }
18
+ }.freeze
19
+
20
+ def self.all_attribute_names
21
+ REQUIRED.keys + OPTIONAL.keys
22
+ end
23
+
24
+ def self.all_attributes
25
+ REQUIRED.merge(OPTIONAL)
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InfinumAzure
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/infinum_azure.rb CHANGED
@@ -3,38 +3,58 @@
3
3
  require 'omniauth/infinum_azure'
4
4
  require 'infinum_azure/version'
5
5
  require 'infinum_azure/engine'
6
- require 'dry-configurable'
6
+ require 'infinum_azure/defaults'
7
+ require 'infinum_azure/config'
7
8
  require 'devise'
8
- require 'http'
9
9
 
10
10
  module InfinumAzure
11
- extend Dry::Configurable
11
+ Error = Class.new(StandardError)
12
12
 
13
- setting :service_name, reader: true
14
- setting :resource_name, default: 'User', reader: true
15
- setting :resource_attributes, default: [:uid, :email, :first_name, :last_name], reader: true
13
+ class << self
14
+ def configure
15
+ yield config if block_given?
16
16
 
17
- def self.provider
18
- to_s.underscore
19
- end
17
+ ensure_all_attributes_present!
18
+ end
20
19
 
21
- def self.resource_class
22
- resource_name.constantize
23
- end
20
+ def config
21
+ @config ||= Config.new
22
+ end
24
23
 
25
- def self.client_id
26
- dig_secret(:client_id)
27
- end
24
+ def ensure_all_attributes_present!
25
+ Defaults.all_attribute_names.each do |attribute|
26
+ raise Error, "InfinumAzure attribute '@#{attribute}' not set" if config.public_send(attribute).blank?
27
+ end
28
+ end
28
29
 
29
- def self.client_secret
30
- dig_secret(:client_secret)
31
- end
30
+ delegate(*Defaults.all_attribute_names, to: :config)
32
31
 
33
- def self.tenant
34
- dig_secret(:tenant)
35
- end
32
+ def provider
33
+ to_s.underscore
34
+ end
35
+
36
+ def resource_class
37
+ resource_name.constantize
38
+ end
39
+
40
+ def client_id
41
+ dig_secret(:client_id)
42
+ end
43
+
44
+ def client_secret
45
+ dig_secret(:client_secret)
46
+ end
47
+
48
+ def tenant
49
+ dig_secret(:tenant)
50
+ end
51
+
52
+ def users_auth_url
53
+ dig_secret(:users_auth_url)
54
+ end
36
55
 
37
- def self.dig_secret(key)
38
- Rails.application.secrets.dig(:infinum_azure, key)
56
+ def dig_secret(key)
57
+ Rails.application.secrets.dig(:infinum_azure, key)
58
+ end
39
59
  end
40
60
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'users/request'
4
+ require_relative 'users/migration'
5
+
6
+ namespace :infinum_azure do
7
+ desc 'Migrate users from InfinumID to InfinumAzure'
8
+ task migrate_users: :environment do
9
+ response = InfinumAzure::Users::Request.execute
10
+
11
+ if response.success?
12
+ process = InfinumAzure::Users::Migration.perform(response.body)
13
+
14
+ puts process.results
15
+ else
16
+ puts response.body
17
+ raise "couldn't connect to InfinumAzure AD B2C"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfinumAzure
4
+ module Users
5
+ class Migration
6
+ delegate :user_migration_scope, :user_migration_operation, to: InfinumAzure
7
+
8
+ def self.perform(response = {})
9
+ new(response).perform
10
+ end
11
+
12
+ attr_reader :response
13
+
14
+ def initialize(response)
15
+ @response = response
16
+ @users_updated_count = 0
17
+ @emails_not_found = []
18
+ end
19
+
20
+ def perform
21
+ user_migration_scope.call.each do |record|
22
+ resource = response.find { |object| object['email'] == record.email }
23
+ emails_not_found.push(record.email) && next if resource.nil?
24
+
25
+ user_migration_operation.call(record, resource)
26
+ self.users_updated_count += 1
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def results
33
+ <<~HEREDOC
34
+ Count of updated resources: #{users_updated_count}
35
+ Emails not found on Infinum Azure AD B2C: #{emails_not_found.size}
36
+ Emails:
37
+ #{emails_not_found.join("\n")}
38
+ HEREDOC
39
+ end
40
+
41
+ private
42
+
43
+ attr_accessor :users_updated_count, :emails_not_found
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require_relative 'response'
6
+
7
+ module InfinumAzure
8
+ module Users
9
+ class Request
10
+ URL = InfinumAzure.users_auth_url
11
+
12
+ def self.execute
13
+ raise 'infinum_azure_users_auth_url secret required for this rake task' if URL.blank?
14
+
15
+ uri = URI(URL)
16
+
17
+ Response.new(Net::HTTP.get_response(uri))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InfinumAzure
4
+ module Users
5
+ class Response
6
+ attr_reader :raw_response
7
+
8
+ def initialize(raw_response)
9
+ @raw_response = raw_response
10
+ end
11
+
12
+ def success?
13
+ raw_response.is_a?(Net::HTTPSuccess)
14
+ end
15
+
16
+ def body
17
+ @body ||= success? ? success_json['Value'] : error_json
18
+ end
19
+
20
+ private
21
+
22
+ def success_json
23
+ JSON.parse(raw_response.body)
24
+ end
25
+
26
+ def error_json
27
+ {
28
+ error: {
29
+ status: raw_response.code.to_i,
30
+ details: raw_response.body
31
+ }
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infinum_azure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marko Ćilimković
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-21 00:00:00.000000000 Z
11
+ date: 2023-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -220,41 +220,13 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
- - !ruby/object:Gem::Dependency
224
- name: dry-configurable
225
- requirement: !ruby/object:Gem::Requirement
226
- requirements:
227
- - - ">="
228
- - !ruby/object:Gem::Version
229
- version: '0'
230
- type: :runtime
231
- prerelease: false
232
- version_requirements: !ruby/object:Gem::Requirement
233
- requirements:
234
- - - ">="
235
- - !ruby/object:Gem::Version
236
- version: '0'
237
- - !ruby/object:Gem::Dependency
238
- name: http
239
- requirement: !ruby/object:Gem::Requirement
240
- requirements:
241
- - - ">="
242
- - !ruby/object:Gem::Version
243
- version: '0'
244
- type: :runtime
245
- prerelease: false
246
- version_requirements: !ruby/object:Gem::Requirement
247
- requirements:
248
- - - ">="
249
- - !ruby/object:Gem::Version
250
- version: '0'
251
223
  - !ruby/object:Gem::Dependency
252
224
  name: omniauth-infinum_azure
253
225
  requirement: !ruby/object:Gem::Requirement
254
226
  requirements:
255
227
  - - ">="
256
228
  - !ruby/object:Gem::Version
257
- version: 0.1.6
229
+ version: 0.3.0
258
230
  - - "<"
259
231
  - !ruby/object:Gem::Version
260
232
  version: '2.0'
@@ -264,24 +236,10 @@ dependencies:
264
236
  requirements:
265
237
  - - ">="
266
238
  - !ruby/object:Gem::Version
267
- version: 0.1.6
239
+ version: 0.3.0
268
240
  - - "<"
269
241
  - !ruby/object:Gem::Version
270
242
  version: '2.0'
271
- - !ruby/object:Gem::Dependency
272
- name: responders
273
- requirement: !ruby/object:Gem::Requirement
274
- requirements:
275
- - - ">="
276
- - !ruby/object:Gem::Version
277
- version: '0'
278
- type: :runtime
279
- prerelease: false
280
- version_requirements: !ruby/object:Gem::Requirement
281
- requirements:
282
- - - ">="
283
- - !ruby/object:Gem::Version
284
- version: '0'
285
243
  description:
286
244
  email:
287
245
  - marko.cilimkovic@infinum.hr
@@ -306,12 +264,19 @@ files:
306
264
  - app/models/infinum_azure/application_record.rb
307
265
  - app/services/infinum_azure/after_upsert_resource.rb
308
266
  - app/services/infinum_azure/resources/finder.rb
267
+ - app/services/infinum_azure/resources/params.rb
309
268
  - config/initializers/devise.rb
310
269
  - config/routes.rb
311
270
  - infinum_azure.gemspec
312
271
  - lib/infinum_azure.rb
272
+ - lib/infinum_azure/config.rb
273
+ - lib/infinum_azure/defaults.rb
313
274
  - lib/infinum_azure/engine.rb
314
275
  - lib/infinum_azure/version.rb
276
+ - lib/tasks/infinum_azure/user_migration.rake
277
+ - lib/tasks/infinum_azure/users/migration.rb
278
+ - lib/tasks/infinum_azure/users/request.rb
279
+ - lib/tasks/infinum_azure/users/response.rb
315
280
  homepage: https://github.com/infinum/rails-infinum-azure-engine
316
281
  licenses:
317
282
  - MIT