cloudinary 1.17.0 → 1.20.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e36bd9efd91c46b00fc0f52a3448ee076990a85b88a470e2cc8e7ccc610ed368
4
- data.tar.gz: dbf3cb94bc5cae2b502b2ca1bea558c93c11e0f77d91fe0577e019452c2acc93
3
+ metadata.gz: 9b641514503d45c3a6c4e8ba5ed87f0f5b8e521260eda789212543be5ccf77a1
4
+ data.tar.gz: 0f65e99aa08e446f01d783af5284a1265aaa4f28a1e95c284addd30beb36ec8c
5
5
  SHA512:
6
- metadata.gz: ba800e50f02feb8cd582a4ca42f6bdfe0fd77c6e36c9dd7e0c3314c9d944bafc451329c79276b02b417f04357aad5ff0ad71e32d3b433f36f2732bfbba70fa84
7
- data.tar.gz: de478592b7552f4aa1b6459effe907b692d22b334b63eb1dc02e7a3ca5e0e5779c7fe82306d3211935df1964f085fdba0e3e6f159716a0055dc45e370be8f6c4
6
+ metadata.gz: 191f627013b5f97fca44e748fd1c4631f9af58a69680b02fdc6b4604e2ddf31216542fcb88e2d1daa825c0ff22a138f2c7d0b41d4895a69cc1b7eaa42a1ab16a
7
+ data.tar.gz: 8a5abe09f562e188e1e6d43d05f462939c62c4070ac50e93c3acaf876998e651aa2fc50775a10c5fb324d388fac9d599c5fe0e60480cdbbebeb7eedf699057e8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,64 @@
1
+ 1.20.0 / 2021-03-26
2
+ ==================
3
+
4
+ New functionality and features
5
+ ------------------------------
6
+
7
+ * Add support for `download_backedup_asset` helper method
8
+ * Add support for `filename_override` upload parameter
9
+ * Add support for `SHA-256` algorithm in auth signatures
10
+
11
+ Other Changes
12
+ -------------
13
+
14
+ * Fix `type` parameter support in ActiveStorage service
15
+ * Fix expression normalization in advanced cases
16
+ * Add test for context metadata as user variables
17
+ * Improve validation of auth token generation
18
+
19
+
20
+ 1.19.0 / 2021-03-05
21
+ ==================
22
+
23
+ New functionality and features
24
+ ------------------------------
25
+
26
+ * Add Account Provisioning API
27
+ * Add support for `api_proxy` parameter
28
+ * Add support for `date` parameter in `usage` Admin API
29
+
30
+ Other Changes
31
+ -------------
32
+
33
+ * Fix direct upload of raw files
34
+ * Improve unit testing of add-ons
35
+ * Change test for `eval` upload parameter
36
+ * Bump vulnerable version of rubyzip
37
+ * Fix `cloudinary.gemspec` glob issue
38
+
39
+ 1.18.1 / 2020-09-30
40
+ ===================
41
+
42
+ * Update embedded `jquery.cloudinary.js` to fix ES5 compatibility issue
43
+
44
+ 1.18.0 / 2020-09-27
45
+ ===================
46
+
47
+ New functionality and features
48
+ ------------------------------
49
+ * Add `download_folder` helper
50
+ * Add support for `sources` in `video` tag
51
+ * Add structured metadata to Admin and Upload API
52
+
53
+ Other Changes
54
+ -------------
55
+ * Fix download of a raw file in ActiveStorage
56
+ * Update embedded `jquery.cloudinary.js` to fix ES5 compatibility issue
57
+
58
+ 1.17.1 / 2020-08-25
59
+ ===================
60
+
61
+ * Fix options handling issue in SassC
1
62
 
2
63
  1.17.0 / 2020-08-21
3
64
  ===================
data/cloudinary.gemspec CHANGED
@@ -15,12 +15,17 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.rubyforge_project = "cloudinary"
17
17
 
18
- s.files = (`git ls-files`.split("\n") - `git ls-files {test,spec,features,samples}/*`.split("\n")) + Dir.glob("vendor/assets/javascripts/*/*") + Dir.glob("vendor/assets/html/*")
18
+ s.files = `git ls-files`.split("\n").select { |f| !f.start_with?("test", "spec", "features", "samples") } + Dir.glob("vendor/assets/javascripts/*/*") + Dir.glob("vendor/assets/html/*")
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  s.add_dependency "aws_cf_signer"
23
- s.add_dependency "rest-client"
23
+
24
+ if RUBY_VERSION >= "2.0.0"
25
+ s.add_dependency "rest-client", ">= 2.0.0"
26
+ else
27
+ s.add_dependency "rest-client"
28
+ end
24
29
 
25
30
  s.add_development_dependency "actionpack"
26
31
  s.add_development_dependency "nokogiri"
@@ -39,7 +44,7 @@ Gem::Specification.new do |s|
39
44
  s.add_development_dependency "railties", "<= 4.2.7" if RUBY_VERSION <= "1.9.3"
40
45
  s.add_development_dependency "rspec-rails"
41
46
 
42
- s.add_development_dependency "rubyzip", "<=1.2.0" # support testing Ruby 1.9
47
+ s.add_development_dependency "rubyzip"
43
48
 
44
49
  if RUBY_VERSION <= "2.4.0"
45
50
  s.add_development_dependency "simplecov", "<= 0.17.1" # support testing Ruby 1.9
@@ -1,5 +1,6 @@
1
1
  require 'active_storage/blob_key'
2
2
  require 'cloudinary/helper'
3
+ require 'net/http'
3
4
 
4
5
  unless ActiveStorage::Blob.method_defined? :original_key
5
6
  class ActiveStorage::Blob
@@ -57,7 +58,7 @@ module ActiveStorage
57
58
  url = Cloudinary::Utils.cloudinary_url(
58
59
  public_id(key),
59
60
  resource_type: resource_type(nil, key),
60
- format: ext_for_file(filename, content_type),
61
+ format: ext_for_file(key, filename, content_type),
61
62
  **@options.merge(options.symbolize_keys)
62
63
  )
63
64
 
@@ -71,6 +72,14 @@ module ActiveStorage
71
72
  instrument :url, key: key do |payload|
72
73
  options = {:resource_type => resource_type(nil, key)}.merge(@options.merge(options.symbolize_keys))
73
74
  options[:public_id] = public_id_internal(key)
75
+ # Provide file format for raw files, since js client does not include original file name.
76
+ #
77
+ # When the file is uploaded from the server, the request includes original filename. That allows Cloudinary
78
+ # to identify file extension and append it to the public id of the file (raw files include file extension
79
+ # in their public id, opposed to transformable assets (images/video) that use only basename). When uploading
80
+ # through direct upload (client side js), filename is missing, and that leads to inconsistent/broken URLs.
81
+ # To avoid that, we explicitly pass file format in options.
82
+ options[:format] = ext_for_file(key) if options[:resource_type] == "raw"
74
83
  options[:context] = {active_storage_key: key}
75
84
  options.delete(:file)
76
85
  payload[:url] = api_uri("upload", options)
@@ -86,7 +95,12 @@ module ActiveStorage
86
95
 
87
96
  def delete(key)
88
97
  instrument :delete, key: key do
89
- Cloudinary::Uploader.destroy public_id(key), resource_type: resource_type(nil, key)
98
+ options = {
99
+ resource_type: resource_type(nil, key),
100
+ type: @options[:type]
101
+ }.compact
102
+
103
+ Cloudinary::Uploader.destroy public_id(key), **options
90
104
  end
91
105
  end
92
106
 
@@ -98,7 +112,12 @@ module ActiveStorage
98
112
  def exist?(key)
99
113
  instrument :exist, key: key do |payload|
100
114
  begin
101
- Cloudinary::Api.resource public_id(key), resource_type: resource_type(nil, key)
115
+ options = {
116
+ resource_type: resource_type(nil, key),
117
+ type: @options[:type]
118
+ }.compact
119
+
120
+ Cloudinary::Api.resource public_id(key), **options
102
121
  true
103
122
  rescue Cloudinary::Api::NotFound => e
104
123
  false
@@ -107,8 +126,7 @@ module ActiveStorage
107
126
  end
108
127
 
109
128
  def download(key, &block)
110
- url = Cloudinary::Utils.unsigned_download_url(public_id(key), resource_type: resource_type(nil, key))
111
- uri = URI(url)
129
+ uri = URI(url(key))
112
130
  if block_given?
113
131
  instrument :streaming_download, key: key do
114
132
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
@@ -169,12 +187,18 @@ module ActiveStorage
169
187
  #
170
188
  # It does the best effort when original filename does not include extension, but we know the mime-type.
171
189
  #
190
+ # @param [ActiveStorage::BlobKey] key The blob key with attributes.
172
191
  # @param [ActiveStorage::Filename] filename The original filename.
173
192
  # @param [string] content_type The content type of the file.
174
193
  #
175
194
  # @return [string] The extension of the filename.
176
- def ext_for_file(filename, content_type)
195
+ def ext_for_file(key, filename = nil, content_type = nil)
196
+ if filename.blank?
197
+ options = key.respond_to?(:attributes) ? key.attributes : {}
198
+ filename = ActiveStorage::Filename.new(options[:filename]) if options.has_key?(:filename)
199
+ end
177
200
  ext = filename.respond_to?(:extension_without_delimiter) ? filename.extension_without_delimiter : nil
201
+
178
202
  return ext unless ext.blank?
179
203
 
180
204
  # Raw files are not convertible, no extension guessing for them
@@ -194,6 +218,8 @@ module ActiveStorage
194
218
  end
195
219
 
196
220
  def content_type_to_resource_type(content_type)
221
+ return 'image' if content_type.nil?
222
+
197
223
  type, subtype = content_type.split('/')
198
224
  case type
199
225
  when 'video', 'audio'
data/lib/cloudinary.rb CHANGED
@@ -16,7 +16,12 @@ require "cloudinary/missing"
16
16
  module Cloudinary
17
17
  autoload :Utils, 'cloudinary/utils'
18
18
  autoload :Uploader, 'cloudinary/uploader'
19
+ autoload :BaseConfig, "cloudinary/base_config"
20
+ autoload :Config, "cloudinary/config"
21
+ autoload :AccountConfig, "cloudinary/account_config"
22
+ autoload :BaseApi, "cloudinary/base_api"
19
23
  autoload :Api, "cloudinary/api"
24
+ autoload :AccountApi, "cloudinary/account_api"
20
25
  autoload :Downloader, "cloudinary/downloader"
21
26
  autoload :Blob, "cloudinary/blob"
22
27
  autoload :PreloadedFile, "cloudinary/preloaded_file"
@@ -58,64 +63,42 @@ module Cloudinary
58
63
  "ept" => "eps"
59
64
  }
60
65
 
61
- @@config = nil
62
-
66
+ # Cloudinary config
67
+ #
68
+ # @param [Hash] new_config If +new_config+ is passed, Config will be updated with it
69
+ # @yieldparam [OpenStruct] Config can be updated in the block
70
+ #
71
+ # @return [OpenStruct]
63
72
  def self.config(new_config=nil)
64
- first_time = @@config.nil?
65
- @@config ||= OpenStruct.new((YAML.load(ERB.new(IO.read(config_dir.join("cloudinary.yml"))).result)[config_env] rescue {}))
66
-
67
- config_from_env if first_time
73
+ @@config ||= make_new_config(Config)
68
74
 
69
- set_config(new_config) if new_config
70
- yield(@@config) if block_given?
75
+ @@config.update(new_config) if new_config
76
+ yield @@config if block_given?
71
77
 
72
78
  @@config
73
79
  end
74
80
 
75
- def self.config_from_url(url)
76
- @@config ||= OpenStruct.new
77
- return unless url && !url.empty?
78
- uri = URI.parse(url)
79
- if !uri.scheme || "cloudinary" != uri.scheme.downcase
80
- raise(CloudinaryException,
81
- "Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
82
- end
83
- set_config(
84
- "cloud_name" => uri.host,
85
- "api_key" => uri.user,
86
- "api_secret" => uri.password,
87
- "private_cdn" => !uri.path.blank?,
88
- "secure_distribution" => uri.path[1..-1]
89
- )
90
- uri.query.to_s.split("&").each do
91
- |param|
92
- key, value = param.split("=")
93
- if isNestedKey? key
94
- putNestedKey key, value
95
- else
96
- set_config(key => Utils.smart_unescape(value))
97
- end
98
- end
99
- end
81
+ # Cloudinary account config
82
+ #
83
+ # @param [Hash] new_config If +new_config+ is passed, Account Config will be updated with it
84
+ # @yieldparam [OpenStruct] Account config can be updated in the block
85
+ #
86
+ # @return [OpenStruct]
87
+ def self.account_config(new_config=nil)
88
+ @@account_config ||= make_new_config(AccountConfig)
100
89
 
101
- def self.putNestedKey(key, value)
102
- chain = key.split(/[\[\]]+/).reject { |i| i.empty? }
103
- outer = @@config
104
- lastKey = chain.pop()
105
- chain.each do |innerKey|
106
- inner = outer[innerKey]
107
- if inner.nil?
108
- inner = OpenStruct.new
109
- outer[innerKey] = inner
110
- end
111
- outer = inner
112
- end
113
- outer[lastKey] = value
90
+ @@account_config.update(new_config) if new_config
91
+ yield @@account_config if block_given?
92
+
93
+ @@account_config
114
94
  end
115
95
 
96
+ def self.config_from_url(url)
97
+ config.load_from_url(url)
98
+ end
116
99
 
117
- def self.isNestedKey?(key)
118
- /\w+\[\w+\]/ =~ key
100
+ def self.config_from_account_url(url)
101
+ account_config.load_from_url(url)
119
102
  end
120
103
 
121
104
  def self.app_root
@@ -129,22 +112,6 @@ module Cloudinary
129
112
 
130
113
  private
131
114
 
132
- def self.config_from_env
133
- # Heroku support
134
- if ENV["CLOUDINARY_CLOUD_NAME"]
135
- config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
136
- config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
137
- config_keys.each do |full_key|
138
- conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
139
- conf_val = ENV[full_key]
140
- conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
141
- set_config(conf_key => conf_val)
142
- end
143
- elsif ENV["CLOUDINARY_URL"]
144
- config_from_url(ENV["CLOUDINARY_URL"])
145
- end
146
- end
147
-
148
115
  def self.config_env
149
116
  return ENV["CLOUDINARY_ENV"] if ENV["CLOUDINARY_ENV"]
150
117
  return Rails.env if defined? Rails::env
@@ -159,6 +126,20 @@ module Cloudinary
159
126
  def self.set_config(new_config)
160
127
  new_config.each{|k,v| @@config.send(:"#{k}=", v) if !v.nil?}
161
128
  end
129
+
130
+ # Builds config from yaml file, extends it with specific module and loads configuration from environment variable
131
+ #
132
+ # @param [Module] config_module Config is extended with this module after being built
133
+ #
134
+ # @return [OpenStruct]
135
+ def self.make_new_config(config_module)
136
+ OpenStruct.new((YAML.load(ERB.new(IO.read(config_dir.join("cloudinary.yml"))).result)[config_env] rescue {})).tap do |config|
137
+ config.extend(config_module)
138
+ config.load_config_from_env
139
+ end
140
+ end
141
+
142
+ private_class_method :make_new_config
162
143
  end
163
144
  # Prevent require loop if included after Rails is already initialized.
164
145
  require "cloudinary/helper" if defined?(::ActionView::Base)
@@ -0,0 +1,231 @@
1
+ class Cloudinary::AccountApi
2
+ extend Cloudinary::BaseApi
3
+
4
+ # Creates a new sub-account. Any users that have access to all sub-accounts will also automatically have access to the
5
+ # new sub-account.
6
+ # @param [String] name The display name as shown in the management console
7
+ # @param [String] cloud_name A case-insensitive cloud name comprised of alphanumeric and underscore characters.
8
+ # Generates an error if the specified cloud name is not unique across all Cloudinary accounts.
9
+ # Note: Once created, the name can only be changed for accounts with fewer than 1000 assets.
10
+ # @param [Object] custom_attributes Any custom attributes you want to associate with the sub-account
11
+ # @param [Boolean] enabled Whether to create the account as enabled (default is enabled)
12
+ # @param [String] base_account ID of sub-account from which to copy settings
13
+ # @param [Object] options additional options
14
+ def self.create_sub_account(name, cloud_name = nil, custom_attributes = {}, enabled = nil, base_account = nil, options = {})
15
+ params = {
16
+ name: name,
17
+ cloud_name: cloud_name,
18
+ custom_attributes: custom_attributes,
19
+ enabled: enabled,
20
+ base_sub_account_id: base_account
21
+ }
22
+
23
+ call_account_api(:post, 'sub_accounts', params, options.merge(content_type: :json))
24
+ end
25
+
26
+ # Updates the specified details of the sub-account.
27
+ # @param [String] sub_account_id The ID of the sub-account.
28
+ # @param [String] name The display name as shown in the management console
29
+ # @param [String] cloud_name A case-insensitive cloud name comprised of alphanumeric and underscore characters.
30
+ # Generates an error if the specified cloud name is not unique across all Cloudinary accounts.
31
+ # Note: Once created, the name can only be changed for accounts with fewer than 1000 assets.
32
+ # @param [Object] custom_attributes Any custom attributes you want to associate with the sub-account, as a map/hash
33
+ # of key/value pairs.
34
+ # @param [Boolean] enabled Whether the sub-account is enabled.
35
+ # @param [Object] options additional options
36
+ def self.update_sub_account(sub_account_id, name = nil, cloud_name = nil, custom_attributes = nil, enabled = nil, options = {})
37
+ params = {
38
+ name: name,
39
+ cloud_name: cloud_name,
40
+ custom_attributes: custom_attributes,
41
+ enabled: enabled
42
+ }
43
+
44
+ call_account_api(:put, ['sub_accounts', sub_account_id], params, options.merge(content_type: :json))
45
+ end
46
+
47
+ # Lists sub-accounts.
48
+ # @param [Boolean] enabled Whether to only return enabled sub-accounts (true) or disabled accounts (false).
49
+ # Default: all accounts are returned (both enabled and disabled).
50
+ # @param [Array<String>] ids A list of up to 100 sub-account IDs. When provided, other parameters are ignored.
51
+ # @param [String] prefix Returns accounts where the name begins with the specified case-insensitive string.
52
+ # @param [Object] options additional options
53
+ def self.sub_accounts(enabled = nil, ids = [], prefix = nil, options = {})
54
+ params = {
55
+ enabled: enabled,
56
+ ids: ids,
57
+ prefix: prefix
58
+ }
59
+
60
+ call_account_api(:get, 'sub_accounts', params, options.merge(content_type: :json))
61
+ end
62
+
63
+ # Retrieves the details of the specified sub-account.
64
+ # @param [String] sub_account_id The ID of the sub-account.
65
+ # @param [Object] options additional options
66
+ def self.sub_account(sub_account_id, options = {})
67
+ call_account_api(:get, ['sub_accounts', sub_account_id], {}, options.merge(content_type: :json))
68
+ end
69
+
70
+ # Deletes the specified sub-account. Supported only for accounts with fewer than 1000 assets.
71
+ # @param [String] sub_account_id The ID of the sub-account.
72
+ # @param [Object] options additional options
73
+ def self.delete_sub_account(sub_account_id, options = {})
74
+ call_account_api(:delete, ['sub_accounts', sub_account_id], {}, options)
75
+ end
76
+
77
+ # Creates a new user in the account.
78
+ # @param [String] name The name of the user.
79
+ # @param [String] email A unique email address, which serves as the login name and notification address.
80
+ # @param [String] role The role to assign. Possible values: master_admin, admin, billing, technical_admin, reports,
81
+ # media_library_admin, media_library_user
82
+ # @param [Array<String>] sub_account_ids The list of sub-account IDs that this user can access.
83
+ # Note: This parameter is ignored if the role is specified as master_admin.
84
+ # @param [Object] options additional options
85
+ def self.create_user(name, email, role, sub_account_ids = [], options = {})
86
+ params = {
87
+ name: name,
88
+ email: email,
89
+ role: role,
90
+ sub_account_ids: sub_account_ids
91
+ }
92
+
93
+ call_account_api(:post, 'users', params, options.merge(content_type: :json))
94
+ end
95
+
96
+ # Deletes an existing user.
97
+ # @param [String] user_id The ID of the user to delete.
98
+ # @param [Object] options additional options
99
+ def self.delete_user(user_id, options = {})
100
+ call_account_api(:delete, ['users', user_id], {}, options)
101
+ end
102
+
103
+ # Updates the details of the specified user.
104
+ # @param [String] user_id The ID of the user to update.
105
+ # @param [String] name The name of the user.
106
+ # @param [String] email A unique email address, which serves as the login name and notification address.
107
+ # @param [String] role The role to assign. Possible values: master_admin, admin, billing, technical_admin, reports,
108
+ # media_library_admin, media_library_user
109
+ # @param [Array<String>] sub_account_ids The list of sub-account IDs that this user can access.
110
+ # Note: This parameter is ignored if the role is specified as master_admin.
111
+ # @param [Object] options additional options
112
+ def self.update_user(user_id, name = nil, email = nil, role = nil, sub_account_ids = nil, options = {})
113
+ params = {
114
+ name: name,
115
+ email: email,
116
+ role: role,
117
+ sub_account_ids: sub_account_ids
118
+ }
119
+
120
+ call_account_api(:put, ['users', user_id], params, options.merge(content_type: :json))
121
+ end
122
+
123
+ # Returns the user with the specified ID.
124
+ # @param [String] user_id The ID of the user.
125
+ # @param [Object] options additional options
126
+ def self.user(user_id, options = {})
127
+ call_account_api(:get, ['users', user_id], {}, options.merge(content_type: :json))
128
+ end
129
+
130
+ # Lists users in the account.
131
+ # @param [Boolean] pending Whether to only return pending users. Default: all users
132
+ # @param [Array<String>] user_ids A list of up to 100 user IDs. When provided, other parameters are ignored.
133
+ # @param [String] prefix Returns users where the name or email address begins with the specified case-insensitive string.
134
+ # @param [String] sub_account_id Only returns users who have access to the specified account.
135
+ # @param [Object] options additional options
136
+ def self.users(pending = nil, user_ids = [], prefix = nil, sub_account_id = nil, options = {})
137
+ params = if user_ids && user_ids.count > 0
138
+ {
139
+ ids: user_ids
140
+ }
141
+ else
142
+ {
143
+ prefix: prefix,
144
+ sub_account_id: sub_account_id,
145
+ pending: pending
146
+ }
147
+ end
148
+
149
+ call_account_api(:get, 'users', params, options.merge(content_type: :json))
150
+ end
151
+
152
+ # Creates a new user group.
153
+ # @param [String] name The name for the user group.
154
+ # @param [Object] options additional options
155
+ def self.create_user_group(name, options = {})
156
+ params = {
157
+ name: name
158
+ }
159
+
160
+ call_account_api(:post, 'user_groups', params, options.merge(content_type: :json))
161
+ end
162
+
163
+ # Updates the specified user group.
164
+ # @param [String] group_id The ID of the user group to update.
165
+ # @param [String] name The name for the user group.
166
+ # @param [Object] options additional options
167
+ def self.update_user_group(group_id, name, options = {})
168
+ params = {
169
+ name: name
170
+ }
171
+
172
+ call_account_api(:put, ['user_groups', group_id], params, options.merge(content_type: :json))
173
+ end
174
+
175
+ # Adds a user to a group with the specified ID.
176
+ # @param [String] group_id The ID of the user group.
177
+ # @param [String] user_id The ID of the user.
178
+ # @param [Object] options additional options
179
+ def self.add_user_to_group(group_id, user_id, options = {})
180
+ call_account_api(:post, ['user_groups', group_id, 'users', user_id], {}, options.merge(content_type: :json))
181
+ end
182
+
183
+ # Removes a user from a group with the specified ID.
184
+ # @param [String] group_id The ID of the user group.
185
+ # @param [String] user_id The ID of the user.
186
+ # @param [Object] options additional options
187
+ def self.remove_user_from_group(group_id, user_id, options = {})
188
+ call_account_api(:delete, ['user_groups', group_id, 'users', user_id], {}, options.merge(content_type: :json))
189
+ end
190
+
191
+ # Deletes the user group with the specified ID.
192
+ # @param [String] group_id The ID of the user group to delete.
193
+ # @param [Object] options additional options
194
+ def self.delete_user_group(group_id, options = {})
195
+ call_account_api(:delete, ['user_groups', group_id], {}, options)
196
+ end
197
+
198
+ # Lists user groups in the account.
199
+ # @param [Object] options additional options
200
+ def self.user_groups(options = {})
201
+ call_account_api(:get, 'user_groups', {}, options.merge(content_type: :json))
202
+ end
203
+
204
+ # Retrieves the details of the specified user group.
205
+ # @param [String] group_id The ID of the user group to retrieve.
206
+ # @param [Object] options additional options
207
+ def self.user_group(group_id, options = {})
208
+ call_account_api(:get, ['user_groups', group_id], {}, options.merge(content_type: :json))
209
+ end
210
+
211
+ # Lists users in the specified user group.
212
+ # @param [String] group_id The ID of the user group.
213
+ # @param [Object] options additional options
214
+ def self.user_group_users(group_id, options = {})
215
+ call_account_api(:get, ['user_groups', group_id, 'users'], {}, options.merge(content_type: :json))
216
+ end
217
+
218
+ def self.call_account_api(method, uri, params, options)
219
+ account_id = options[:account_id] || Cloudinary.account_config.account_id || raise('Must supply account_id')
220
+ api_key = options[:provisioning_api_key] || Cloudinary.account_config.provisioning_api_key || raise('Must supply provisioning api_key')
221
+ api_secret = options[:provisioning_api_secret] || Cloudinary.account_config.provisioning_api_secret || raise('Must supply provisioning api_secret')
222
+
223
+ params.reject! { |_, v| v.nil? }
224
+
225
+ call_cloudinary_api(method, uri, api_key, api_secret, params, options) do |cloudinary, inner_uri|
226
+ [cloudinary, 'v1_1', 'provisioning', 'accounts', account_id, inner_uri]
227
+ end
228
+ end
229
+
230
+ private_class_method :call_account_api
231
+ end