azure-directory 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d32a18884bf87b55f5efa53ac89c5ef4ad4ee986
4
+ data.tar.gz: 8e9aebe9a99a520b4ee6482afc67d63bc0d6d247
5
+ SHA512:
6
+ metadata.gz: f0b24aad5bc930941a09fdb4ea7d83648cffc6773d5394a4183a1c2d536cb0ef81cd3ac89a1c700b587fdc2dac458b104a89572c015cba597954e1a8d4886538
7
+ data.tar.gz: 855f22fc5e04d932497dff20d94f1b2a4148e04f07810007799fc86ce2770fa21273024a911a47385357179dd6630f882e2c02ad8db1a25fffba7675f613faf1
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ ._*
15
+ .DS_Store
16
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in azure-directory.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Omac
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,93 @@
1
+ # Azure Active Directory Graph API
2
+
3
+ ## Description
4
+
5
+ Simple Azure Active Directory Graph API wrapper for Ruby on Rails.
6
+
7
+ The API authentication protocol is a service to service call using OAuth2 client credentials. For more information go to
8
+ [Azure Documentation](https://msdn.microsoft.com/en-us/library/azure/dn645543.aspx).
9
+
10
+ * This library is in alpha. Future incompatible changes may be necessary. *
11
+
12
+ ## Install
13
+
14
+ Add the gem to the Gemfile
15
+
16
+ ```ruby
17
+ gem 'azure-directory'
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ First configure your API client in `config/initializers/azure_directory.rb`
23
+
24
+ ``` ruby
25
+ Azure::Directory.configure do
26
+
27
+ # OPTIONAL. Use a YAML file to store the requested access tokens. When the token is refreshed, this file will be updated.
28
+ use_yaml Rails.root.join('config', 'google_directory.yaml')
29
+
30
+ # Required attributes
31
+ client_id ''
32
+ client_secret ''
33
+ tenant_id ''
34
+ resource_id ''
35
+
36
+ end
37
+ ```
38
+
39
+ ### Multiple API clients using scopes
40
+
41
+ Specify a single or multiple scopes in the configuration file.
42
+
43
+ ``` ruby
44
+ Azure::Directory.configure do
45
+
46
+ scope :domain_one do
47
+ client_id ''
48
+ client_secret ''
49
+ # [...]
50
+ end
51
+
52
+ scope :domain_two do
53
+ client_id ''
54
+ client_secret ''
55
+ # [...]
56
+ end
57
+
58
+ end
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ``` ruby
64
+ azure = Azure::Directory::Client.new
65
+
66
+ azure.find_users
67
+
68
+ azure.create_user("email", "given_name", "family_name", "password")
69
+
70
+ azure.update_user("email", update_data)
71
+
72
+ azure.update_user_password("email", "new_password")
73
+
74
+ ```
75
+
76
+ ### Multiple Scopes
77
+
78
+ ``` ruby
79
+ domain_one = Azure::Directory::Client.new(:domain_one)
80
+ domain_one.find_users
81
+
82
+ domain_two = Azure::Directory::Client.new(:domain_two)
83
+ domain_two.find_users
84
+ ```
85
+
86
+ ## TO DO
87
+
88
+ * `use_active_model` for database token store
89
+ * Build the configuration generator
90
+ * Implement the Azure's REST API calls
91
+ * Abstract the API's Entities into ruby models [Entity Reference](https://msdn.microsoft.com/en-us/library/azure/dn151470.aspx)
92
+ * Better error handling
93
+ * Testing
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,29 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'azure/directory/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "azure-directory"
7
+ spec.version = Azure::Directory::VERSION
8
+ spec.authors = ["Omar Osorio"]
9
+ spec.email = ["omar@kioru.com"]
10
+ spec.homepage = "https://github.com/kiomac/azure-directory"
11
+
12
+ spec.summary = "Azure Active Directory Graph API client for Ruby on Rails"
13
+ spec.description = "Setup your Rails application with one or multiple clients for Azure AD Graph API using OAuth2 service-to-service calls."
14
+
15
+ spec.rdoc_options = ["--main", "README.md"]
16
+
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency 'oauth2', '~> 1.0'
24
+
25
+ spec.add_development_dependency 'rails', '~> 4.2'
26
+ spec.add_development_dependency "bundler", "~> 1.7"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency 'yard', '~> 0.8'
29
+ end
@@ -0,0 +1,242 @@
1
+ require 'azure/directory/version'
2
+ require 'azure/directory/config'
3
+ require 'oauth2'
4
+
5
+ module Azure
6
+ module Directory
7
+
8
+ GRAPH_API_VERSION = '1.5'
9
+
10
+ class Client
11
+
12
+ attr_reader :oauth, :oauth_token, :config
13
+
14
+ ##
15
+ # @param [Symbol] scope (:main) The scope to use with this client.
16
+ #
17
+ def initialize(scope = :main)
18
+ @config = Azure::Directory.configuration
19
+ @config = @config.using(scope) if @config.scope_name != scope
20
+
21
+ @oauth = OAuth2::Client.new( @config.client_id, @config.client_secret,
22
+ :site => 'https://login.windows.net/',
23
+ :authorize_url => "/#{@config.tenant_id}/oauth2/authorize",
24
+ :token_url => "/#{@config.tenant_id}/oauth2/token" )
25
+
26
+
27
+ if token_hash = @config.load_token
28
+ @oauth_token = OAuth2::AccessToken.from_hash(@oauth, token_hash)
29
+
30
+ else
31
+ fetch_access_token!
32
+ end
33
+
34
+ end
35
+
36
+
37
+ ##
38
+ # Do the service-to-service access token request and
39
+ # save it to the Token Store defined in the configuration.
40
+ #
41
+ # @return [OAuth2::AccessToken] a access token for the current session.
42
+ #
43
+ def fetch_access_token!
44
+ @oauth_token = oauth.get_token( :client_id => config.client_id,
45
+ :client_secret => config.client_secret,
46
+ :grant_type => 'client_credentials',
47
+ :response_type => 'client_credentials',
48
+ :resource => config.resource_id )
49
+
50
+ token_hash = { 'access_token' => oauth_token.token, 'token_type' => oauth_token.params['token_type'], 'expires_at' => oauth_token.expires_at }
51
+ config.save_token(token_hash)
52
+ oauth_token
53
+ end
54
+
55
+
56
+ ##
57
+ # Get all users from the active directory
58
+ #
59
+ # @return [Array]
60
+ #
61
+ # @see https://msdn.microsoft.com/en-us/library/azure/hh974483.aspx User
62
+ def find_users(params = nil)
63
+ users = get('/users', params)
64
+ users['value'] if users.is_a?(Hash)
65
+ end
66
+
67
+
68
+
69
+ ##
70
+ # Get user by email
71
+ #
72
+ # @return [Hash] The user's information or nil if not found
73
+ #
74
+ # @see https://msdn.microsoft.com/en-us/library/azure/hh974483.aspx User
75
+ def find_user_by_email(email, params = nil)
76
+ get("/users/#{email}", params)
77
+ end
78
+
79
+
80
+
81
+ ##
82
+ # Creates a unique user on the Active Directory
83
+ #
84
+ # @param [String] email User unique email inside the AD Domain.
85
+ # @param [String] given_name
86
+ # @param [String] family_name
87
+ # @param [String] password The password will set up with `forceChangePasswordNextLogin = true`by default.
88
+ # @param [Hash] params If you wish to add or override specific parameters from the Graph API.
89
+ #
90
+ # @option params [Boolean] 'accountEnabled' (true)
91
+ # @option params [String] 'displayName' Will concatenate given_name and family_name
92
+ # @option params [String] 'mailNickname' Username extracted from the email.
93
+ # @option params [String] 'passwordProfile' { "password" => password, "forceChangePasswordNextLogin" => true }
94
+ # @option params [String] 'userPrincipalName' email
95
+ # @option params [String] 'givenName' given_name
96
+ # @option params [String] 'surname' family_name
97
+ # @option params [String] 'usageLocation' 'US'
98
+ #
99
+ # @return [Hash] The user's information or nil if unsuccessful
100
+ #
101
+ # @see https://msdn.microsoft.com/en-us/library/azure/hh974483.aspx User
102
+ #
103
+ def create_user(email, given_name, family_name, password, params = {})
104
+ params = { 'accountEnabled' => true,
105
+ 'displayName' => "#{given_name} #{family_name}",
106
+ 'mailNickname' => email.split('@').first,
107
+ 'passwordProfile' => { "password" => password, "forceChangePasswordNextLogin" => true },
108
+ 'userPrincipalName' => email,
109
+ 'givenName' => given_name,
110
+ 'surname' => family_name,
111
+ 'usageLocation' => 'US'
112
+ }.merge(params)
113
+
114
+ post('users', params)
115
+ end
116
+
117
+
118
+
119
+ ##
120
+ # Updates the current user with specified parameters
121
+ #
122
+ # @param [String] params See the create_user method's params
123
+ #
124
+ # @return [Boolean] True if update was successful
125
+ #
126
+ def update_user(email, params = nil)
127
+ patch("users/#{email}", params) == :no_content
128
+ end
129
+
130
+
131
+
132
+ ##
133
+ # Updates the user's password
134
+ #
135
+ # @param [String] email
136
+ # @param [String] password A valid password
137
+ # @param [String] force_change_password_next_login True by default
138
+ #
139
+ # @return [Hash] The user's information or nil if unsuccessful
140
+ #
141
+ def update_user_password(email, password, force_change_password_next_login = true)
142
+ params = { 'passwordProfile' => {
143
+ 'password' => password,
144
+ 'forceChangePasswordNextLogin' => force_change_password_next_login } }
145
+
146
+ patch("users/#{email}", params) == :no_content
147
+ end
148
+
149
+
150
+ ##
151
+ # Obtain the SubscribedSkus.
152
+ #
153
+ def get_subscribed_skus
154
+ get('subscribedSkus')
155
+ end
156
+
157
+
158
+ ##
159
+ # Assignment of subscriptions for provisioned user account.
160
+ #
161
+ # @param [String] sku_part_number Using this name we get the skuId to do the proper assignment.
162
+ #
163
+ # @example
164
+ # assign_license('username@domain.com', 'STANDARDWOFFPACK_STUDENT')
165
+ #
166
+ def assign_license(email, sku_part_number)
167
+ skus = get('subscribedSkus')['value']
168
+ return nil unless sku = skus.detect{ |_sku| _sku['skuPartNumber'] == sku_part_number }
169
+
170
+ post("users/#{email}/assignLicense", { "addLicenses" => [ {"disabledPlans" => [], "skuId" => sku['skuId'] }], "removeLicenses" => [] })
171
+ end
172
+
173
+
174
+ ##
175
+ # Deletes an existing user by email
176
+ #
177
+ # @param [String] email User email
178
+ #
179
+ # @return [Boolean] True if the user was deleted
180
+ #
181
+ def delete_user(email)
182
+ delete("users/#{email}") == :no_content
183
+ end
184
+
185
+
186
+
187
+ private
188
+
189
+ def get(path, params = nil)
190
+ request(:get, path, params)
191
+ end
192
+
193
+ def post(path, params)
194
+ request(:post, path, nil, params)
195
+ end
196
+
197
+ def patch(path, params)
198
+ request(:patch, path, nil, params)
199
+ end
200
+
201
+ def delete(path)
202
+ request(:delete, path)
203
+ end
204
+
205
+ def request(method, path, params = nil, body = nil)
206
+ fetch_access_token! if oauth_token.expired?
207
+
208
+ response = oauth_token.request(method, graph_url(path), build_params(params, body).merge(:raise_errors => false) )
209
+ if response.error
210
+ unless (error = response.parsed).is_a?(Hash) and error['odata.error']['code'] == 'Request_ResourceNotFound'
211
+ Rails.logger.error("OAuth2 Error (#{response.status}): #{response.parsed}" )
212
+ end
213
+ return nil
214
+ end
215
+
216
+ case response.status
217
+ when 200, 201 then return response.parsed
218
+ when 204 then return :no_content
219
+ end
220
+
221
+ response
222
+
223
+ end
224
+
225
+
226
+ def graph_url(path)
227
+ "https://graph.windows.net/#{config.tenant_id}/#{path}"
228
+ end
229
+
230
+ def build_params(params = nil, body = nil)
231
+ params ||= {}
232
+ body = body.to_json if body and body.class.method_defined?(:to_json)
233
+
234
+ { :params => params.merge!( 'api-version' => GRAPH_API_VERSION ),
235
+ :body => body,
236
+ :headers => {'Content-Type' => 'application/json'} }
237
+ end
238
+ end
239
+
240
+
241
+ end
242
+ end
@@ -0,0 +1,148 @@
1
+ require 'yaml'
2
+
3
+ module Azure
4
+
5
+ module Directory
6
+
7
+ class MissingConfiguration < StandardError
8
+ def initialize
9
+ super('No configuration found for Azure Directory')
10
+ end
11
+ end
12
+
13
+ def self.configure(&block)
14
+ @config = Config::Builder.new(&block).build
15
+ end
16
+
17
+ def self.configuration
18
+ @config || (fail MissingConfiguration.new)
19
+ end
20
+
21
+
22
+
23
+ class Config
24
+
25
+ attr_reader :scope_name, :client_id, :client_secret, :tenant_id, :resource_id
26
+
27
+ def initialize(scope_name = :main)
28
+ @scope_name = scope_name
29
+ end
30
+
31
+ def using(scope)
32
+ @scopes[scope]
33
+ end
34
+
35
+
36
+ def save_token(token_hash)
37
+ token_hash = token_hash.slice('access_token', 'token_type', 'expires_at')
38
+ @token_store and @token_store.save(@scope_name, token_hash)
39
+ end
40
+
41
+ def load_token
42
+ @token_store and @token_store.load(@scope_name)
43
+ end
44
+
45
+
46
+ class Builder
47
+
48
+ def initialize(&block)
49
+ @config = @current_config = Config.new
50
+ @config.instance_variable_set('@scopes', { })
51
+ instance_eval(&block)
52
+ end
53
+
54
+ def build
55
+ @config
56
+ end
57
+
58
+ ##
59
+ # Use a YAML file to store the requested access tokens. When the token is refreshed, this file will be updated.
60
+ # You must declare this configuration attribute before any scope.
61
+ #
62
+ # @param [String] The YAML file path (keep this file secure).
63
+ #
64
+ def use_yaml( yaml_file )
65
+
66
+ File.exist?(yaml_file) || FileUtils.touch(yaml_file)
67
+ @token_store = YamlTokenStore.new( yaml_file )
68
+ @current_config.instance_variable_set('@token_store', @token_store)
69
+
70
+ end
71
+
72
+ ##
73
+ # OAuth: Application Client ID
74
+ #
75
+ def client_id( client_id )
76
+ @current_config.instance_variable_set('@client_id', client_id)
77
+ end
78
+
79
+ ##
80
+ # OAuth: Application Client Secret
81
+ #
82
+ def client_secret( client_secret )
83
+ @current_config.instance_variable_set('@client_secret', client_secret)
84
+ end
85
+
86
+ ##
87
+ # OAuth: Azure's Tenant ID.
88
+ #
89
+ # @param [String] tenant_id Tenant identifier (ID) of the Azure AD tenant that issued the token.
90
+ #
91
+ def tenant_id( tenant_id )
92
+ @current_config.instance_variable_set('@tenant_id', tenant_id)
93
+ end
94
+
95
+ ##
96
+ # Required Resource Access
97
+ #
98
+ # @param [String] Get the resourceAppId from the manifest of your application added to the Active Directory.
99
+ #
100
+ def resource_id( resource_id )
101
+ @current_config.instance_variable_set('@resource_id', resource_id)
102
+ end
103
+
104
+ ##
105
+ # Set a new configuration for a specific scope, in order to support multiple connections to different applications.
106
+ # Provide a block with the configuration parameters.
107
+ #
108
+ # @param [Symbol] scope_name Scope name
109
+ #
110
+ def scope( scope_name, &block )
111
+ scopes = @config.instance_variable_get('@scopes')
112
+ scopes[scope_name] = @current_config = Config.new(scope_name)
113
+
114
+ @current_config.instance_variable_set('@token_store', @token_store)
115
+
116
+ instance_eval(&block)
117
+ @current_config = @config
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+
125
+ class YamlTokenStore
126
+
127
+ def initialize(yaml_file)
128
+ @yaml_file = yaml_file
129
+ @yaml_data = YAML::load( yaml_file.open )
130
+ @yaml_data = {} unless @yaml_data.is_a?(Hash)
131
+ end
132
+
133
+ def save( scope_name, token_hash )
134
+ data = (@yaml_data[Rails.env.to_s] ||= {})
135
+ data[scope_name.to_s] = token_hash
136
+ File.open(@yaml_file, 'w') { |file| file.write( YAML::dump(@yaml_data) ) }
137
+ end
138
+
139
+ def load( scope_name )
140
+ data = @yaml_data[Rails.env.to_s] and data = data[scope_name.to_s] and data.slice('access_token', 'token_type', 'expires_at')
141
+ end
142
+
143
+ end
144
+
145
+ end
146
+
147
+
148
+ end
@@ -0,0 +1,5 @@
1
+ module Azure
2
+ module Directory
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: azure-directory
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Omar Osorio
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
83
+ description: Setup your Rails application with one or multiple clients for Azure AD
84
+ Graph API using OAuth2 service-to-service calls.
85
+ email:
86
+ - omar@kioru.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - azure-directory.gemspec
97
+ - lib/azure/directory.rb
98
+ - lib/azure/directory/config.rb
99
+ - lib/azure/directory/version.rb
100
+ homepage: https://github.com/kiomac/azure-directory
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options:
106
+ - "--main"
107
+ - README.md
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.2.2
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Azure Active Directory Graph API client for Ruby on Rails
126
+ test_files: []
127
+ has_rdoc: