sailpoint 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +35 -37
- data/lib/sailpoint.rb +54 -44
- data/lib/sailpoint/configuration.rb +108 -0
- data/lib/sailpoint/core_ext/blank.rb +55 -0
- data/lib/sailpoint/core_ext/escape_str.rb +27 -0
- data/lib/sailpoint/helpers.rb +26 -90
- data/lib/sailpoint/rest.rb +44 -23
- data/lib/sailpoint/scim.rb +44 -26
- data/lib/sailpoint/version.rb +4 -3
- metadata +21 -16
- data/lib/sailpoint/config.rb +0 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1220fcec1f06178a2d7a3ad3223c4f8a142c97a1c5c22a1a04cc7d0534fc2969
|
4
|
+
data.tar.gz: 0f4937b7f5a97782a0b2f0a86d7e09f621c196a33576c6149df973aa209ab9fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c16e91fc84ec90643c25af0731eb20762488bd55a8e729e19a557fff8bf0c6186b3daa23773c04b9866a360c7eb0e0de91d480c4ff2029fb56f95e7dc7dd091e
|
7
|
+
data.tar.gz: 8f8e3c65082bf64acc9311621dce1cc1c734a032be70cd86cdcd3705605d4e28d467caa46c54cb7c1b06f23a0bd7fd068ea8350b0ced08156bc9a2a584e5db96
|
data/README.md
CHANGED
@@ -46,24 +46,23 @@ If running from `irb` or wanting to call the IdentityIQ API from a ruby script u
|
|
46
46
|
require 'sailpoint'
|
47
47
|
|
48
48
|
# In order to make any API requests you need to specify the IdentityIQ API Host and set you API credentials
|
49
|
-
Sailpoint.
|
50
|
-
|
51
|
-
|
49
|
+
Sailpoint.configure do |config|
|
50
|
+
config.username = 'api_username'
|
51
|
+
config.password = 'api_password'
|
52
|
+
config.host = 'https://example.com'
|
53
|
+
end
|
52
54
|
```
|
53
55
|
|
54
|
-
## Getting Started
|
55
|
-
|
56
56
|
By default this will pull users from the REST API
|
57
57
|
If you want to pull from the SCIM API there are a number of ways to do this as well
|
58
58
|
|
59
59
|
```ruby
|
60
60
|
# First method
|
61
|
-
Sailpoint.get_user('sample_user'
|
61
|
+
Sailpoint.get_user('sample_user')
|
62
62
|
|
63
63
|
# Second method
|
64
64
|
# Note: When reassigning the API interface future queries will hit the new API endpoint unless specified
|
65
|
-
Sailpoint::
|
66
|
-
Sailpoint.get_user('sample_user')
|
65
|
+
Sailpoint::Rest.get_user('sample_user')
|
67
66
|
|
68
67
|
# Third method (and my personal favorite to use without assigning the interface)
|
69
68
|
Sailpoint::Scim.get_user('sample_user')
|
@@ -76,53 +75,47 @@ Lets first start by creating an initializer so you don't have to set the API con
|
|
76
75
|
```ruby
|
77
76
|
# config/initializers/sailpoint.rb
|
78
77
|
if defined?(Sailpoint)
|
79
|
-
Sailpoint
|
80
|
-
|
78
|
+
Sailpoint.configure do |config|
|
79
|
+
config.username = 'api_username'
|
80
|
+
config.password = 'api_password'
|
81
|
+
config.host = 'https://example.com'
|
82
|
+
end
|
81
83
|
end
|
84
|
+
```
|
82
85
|
|
83
86
|
# If you're using encrypted credentials
|
87
|
+
|
88
|
+
```ruby
|
84
89
|
if defined?(Sailpoint)
|
85
|
-
Sailpoint
|
86
|
-
|
90
|
+
Sailpoint.configure do |config|
|
91
|
+
config.username = Rails.application.credentials[:sailpoint][:username]
|
92
|
+
config.password = Rails.application.credentials[:sailpoint][:password]
|
93
|
+
config.host = 'https://example.com'
|
94
|
+
end
|
87
95
|
end
|
88
96
|
```
|
89
97
|
|
90
|
-
Now in your controller
|
98
|
+
Now in your controller or models you should be able to make an API request with the following command
|
91
99
|
|
92
100
|
```ruby
|
93
101
|
Sailpoint.get_user('sample_user')
|
94
102
|
```
|
95
103
|
|
96
|
-
# Misc
|
97
|
-
|
98
|
-
```ruby
|
99
|
-
Sailpoint::Config.set_credentials('api_username', 'api_password'); Sailpoint::Config.host = 'http://example.com/';
|
100
|
-
# Sailpoint::Config.interface = 'rest'
|
101
|
-
# Sailpoint::Config.interface = 'scim'
|
102
|
-
Sailpoint.get_user('username')
|
103
|
-
```
|
104
|
-
|
105
104
|
## General function calls
|
106
105
|
|
107
|
-
Listed below are the majority of the functions used throughout the library and their intended purpose.
|
108
|
-
|
109
106
|
* `Sailpoint.get_user(identity)` - Search the API resources for the specified user identity
|
110
|
-
* `Sailpoint.set_credentials(username, password)` - Assign the credentials for accessing the IdentityIQ API
|
111
|
-
* `Sailpoint.set_host(host)` - Assign the IdentityIQ API base URL
|
112
107
|
|
113
108
|
## Configuration
|
114
109
|
|
115
|
-
* `Sailpoint
|
116
|
-
* `Sailpoint
|
117
|
-
* `Sailpoint
|
118
|
-
* `Sailpoint
|
119
|
-
* `Sailpoint
|
120
|
-
* `Sailpoint
|
121
|
-
* `Sailpoint
|
122
|
-
* `Sailpoint
|
123
|
-
* `Sailpoint
|
124
|
-
* `Sailpoint::Config.url` - Returns the full API URL based on the `host+interface`
|
125
|
-
* `Sailpoint::Config.username` - If set, it returns the username credentials for API
|
110
|
+
* `Sailpoint.config.auth_header` - Returns the BasicAuth Header for creating and API request (if the username/password have been set)
|
111
|
+
* `Sailpoint.config.credentials` - A hash containing the API credentials when setting API requests headers
|
112
|
+
* `Sailpoint.config.hashed_credentials` - A Base64 encoded string for the API request
|
113
|
+
* `Sailpoint.config.host` - Returns the API base host
|
114
|
+
* `Sailpoint.config.interface` - Returns the specified API interface (REST || SCIM)
|
115
|
+
* `Sailpoint.config.interface_path` - Returns the API path dependant on the interface
|
116
|
+
* `Sailpoint.config.password` - Returns the API password specified
|
117
|
+
* `Sailpoint.config.url` - Returns the full API URL based on the `host+interface`
|
118
|
+
* `Sailpoint.config.username` - If set, it returns the username credentials for API
|
126
119
|
|
127
120
|
## Interface specific function calls
|
128
121
|
|
@@ -146,6 +139,11 @@ Listed below are the majority of the functions used throughout the library and t
|
|
146
139
|
* `Sailpoint::Rest.permitted_roles(identity)` - Get a users roles within the Organization
|
147
140
|
* `Sailpoint::Rest.ping` - Used to verify your credentials are valid and IdentityIQ reource is properly responding
|
148
141
|
|
142
|
+
```shell
|
143
|
+
# Rebuilding the gem to test in a required IRB term
|
144
|
+
gem uninstall sailpoint; rm -rf sailpoint-0.1.0.gem; gem build; gem install sailpoint
|
145
|
+
```
|
146
|
+
|
149
147
|
## API Documentation
|
150
148
|
|
151
149
|
* [IdentityNow](https://api.identitynow.com/)
|
data/lib/sailpoint.rb
CHANGED
@@ -1,61 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'sailpoint/version'
|
2
|
-
require 'sailpoint/
|
3
|
-
require 'sailpoint/
|
4
|
+
require 'sailpoint/core_ext/blank'
|
5
|
+
require 'sailpoint/core_ext/escape_str'
|
4
6
|
require 'sailpoint/rest'
|
5
7
|
require 'sailpoint/scim'
|
6
8
|
|
7
|
-
# Sailpoint Module to allow for easily accessing the Sailpoint API
|
8
9
|
module Sailpoint
|
10
|
+
require 'sailpoint/helpers' # To override defaults and adding global helper functions
|
11
|
+
require 'sailpoint/configuration'
|
12
|
+
|
9
13
|
# When generating a Standard Exception error
|
10
14
|
class Error < StandardError; end
|
15
|
+
class << self
|
16
|
+
def config
|
17
|
+
@config ||= Configuration.new
|
18
|
+
end
|
11
19
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
raise ArgumentError, 'Please specify a valid HOST/Interface before attemping a lookup.' unless valid_url?
|
18
|
-
raise ArgumentError, 'Valid credentials are required before attempting an API request.' unless valid_credentials?
|
19
|
-
raise ArgumentError, 'Invalid method type' unless valid_interface_type?(interface) || valid_interface_type?(Sailpoint::Config.interface)
|
20
|
-
|
21
|
-
if interface.nil?
|
22
|
-
Object.const_get("Sailpoint::#{Sailpoint::Config.interface.capitalize}").get_user(username)
|
23
|
-
elsif valid_interface_type?(interface)
|
24
|
-
Object.const_get("Sailpoint::#{interface.capitalize}").get_user(username)
|
20
|
+
# Used to memorize and create a Mutex to keep config in sync across running threads
|
21
|
+
#
|
22
|
+
# @return [Mutex]
|
23
|
+
def mutex
|
24
|
+
@mutex ||= Mutex.new
|
25
25
|
end
|
26
|
-
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
# If a valid username and URL have been supplied a lookup requests will be send to determine if the user exists in the specified interface
|
28
|
+
# @param username [String] - the username that we are going to valid exists in the IdentityIQ listing
|
29
|
+
# @return [Hash] - If a user is found, it will return all the that identities attributes
|
30
|
+
def get_user(username = '')
|
31
|
+
raise ArgumentError, 'An invalid user lookup was specified.' if username.blank?
|
32
|
+
raise ArgumentError, 'Please specify a valid HOST/Interface before attemping a lookup.' unless valid_url?
|
33
|
+
raise ArgumentError, 'Valid credentials are required before attempting an API request.' unless valid_credentials?
|
34
|
+
raise ArgumentError, 'Invalid interface type' unless valid_interface_type?(config.interface)
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
if config.interface.blank?
|
37
|
+
Sailpoint::Scim.get_user(username)
|
38
|
+
else
|
39
|
+
Object.const_get("Sailpoint::#{config.interface&.capitalize}").get_user(username)
|
40
|
+
end
|
41
|
+
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
# Used to verify if any credentails were supplied for the API request
|
44
|
+
# @return [Boolean] if credentials were supplied or not
|
45
|
+
def valid_credentials?
|
46
|
+
return false if Sailpoint.config.username.blank? && Sailpoint.config.password.blank?
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
+
!Sailpoint.config.hashed_credentials.blank?
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
# Used to verify if the specifed interface type is valid for the Sailpoint API
|
52
|
+
# @param interface [String] - A specified API interface endpoint, that can either be `Rest` or `Scim`
|
53
|
+
# @return [Boolean] - Returns weither the specifed interface is a a valid type allowed by the API.
|
54
|
+
def valid_interface_type?(interface = nil)
|
55
|
+
return false if interface.blank?
|
56
|
+
|
57
|
+
Sailpoint::Configuration::ALLOWED_INTERFACES.include?(interface)
|
58
|
+
end
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
+
# Used to verify if the URL string is blank or a URL was supplied
|
61
|
+
# @return [Boolean] - if a url for the API endpoint was supplied or not
|
62
|
+
def valid_url?
|
63
|
+
!Sailpoint.config.url.blank?
|
64
|
+
end
|
65
|
+
|
66
|
+
def configure
|
67
|
+
self.config ||= config
|
68
|
+
yield(config)
|
69
|
+
end
|
60
70
|
end
|
61
71
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'sailpoint/helpers'
|
5
|
+
|
6
|
+
# Used for setting you Sailpoint API configuration and credentials
|
7
|
+
module Sailpoint
|
8
|
+
# Used for setting API configuration before creating API Requests
|
9
|
+
# Configuration can include: <code>username, password, interface, host, url</code>
|
10
|
+
class Configuration
|
11
|
+
ALLOWED_INTERFACES = %w[rest scim].freeze
|
12
|
+
|
13
|
+
attr_accessor :password, :username
|
14
|
+
|
15
|
+
# Variables used for storing values for host= and interface=
|
16
|
+
attr_accessor :host_val, :interface_val
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
reload_config
|
20
|
+
end
|
21
|
+
|
22
|
+
def host=(val = nil)
|
23
|
+
self.host_val = trim_host(val)
|
24
|
+
end
|
25
|
+
|
26
|
+
def host
|
27
|
+
host_val
|
28
|
+
end
|
29
|
+
|
30
|
+
def interface=(val = nil)
|
31
|
+
val = val&.to_s&.strip unless val.blank?
|
32
|
+
self.interface_val = begin
|
33
|
+
if val.blank? || !ALLOWED_INTERFACES.include?(val)
|
34
|
+
'scim'
|
35
|
+
else
|
36
|
+
val
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def interface
|
42
|
+
interface_val
|
43
|
+
end
|
44
|
+
|
45
|
+
# SailPoints auth requires a Base64 string of (username:password)
|
46
|
+
# This is how most BasicAuth authentication methods work
|
47
|
+
# @return [String] - It will either return an empty string or a Base64.encoded hash for the the API credentials (BasicAuth requires Base64)
|
48
|
+
def hashed_credentials
|
49
|
+
return '' if username.blank? && password.blank?
|
50
|
+
|
51
|
+
Base64.encode64("#{username}:#{password}").strip
|
52
|
+
end
|
53
|
+
|
54
|
+
# Used for fetching the API interface_path based on the API interface specification
|
55
|
+
# @return [String] - Returns the API's interface path, based on interface type
|
56
|
+
def interface_path
|
57
|
+
return 'scim' if interface.blank? || !ALLOWED_INTERFACES.include?(interface)
|
58
|
+
|
59
|
+
interface
|
60
|
+
end
|
61
|
+
|
62
|
+
# Used for fetching the requesting users entire URL (Host+Interface)
|
63
|
+
# @return [String] - Returns the entire requesting URL (based on host and interface type)
|
64
|
+
def url
|
65
|
+
return '' if host.blank? || interface.blank?
|
66
|
+
|
67
|
+
full_host(interface)
|
68
|
+
end
|
69
|
+
|
70
|
+
def full_host(interface = '')
|
71
|
+
(interface.blank? ? [host, 'identityiq', interface_path].join('/') : [host, 'identityiq', interface].join('/'))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Used for fetching the API credentials when setting API requests headers
|
75
|
+
# @return [String] - Return a hash of the current API credentils (for validation purposes)
|
76
|
+
def credentials
|
77
|
+
{ username: username, password: password }.freeze
|
78
|
+
end
|
79
|
+
|
80
|
+
# Used for generating the API BasicAuth Header when creating an API request
|
81
|
+
# @return [String] - Return the API Authorization header for the making API requests
|
82
|
+
def auth_header
|
83
|
+
return {}.freeze if username.blank? && password.blank?
|
84
|
+
|
85
|
+
{ 'Authorization' => "Basic #{hashed_credentials}" }.freeze
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def reload_config
|
91
|
+
::Sailpoint.mutex.synchronize do
|
92
|
+
self.interface ||= 'scim'
|
93
|
+
self.host ||= nil
|
94
|
+
self.password ||= nil
|
95
|
+
self.username ||= nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Remove trailing forward slashes from the end of the Host, that way hosts and interfaces can be properly joined
|
100
|
+
# => I also did this because you'd get the same results if something supplied `http://example.com` or `https://example.com/`
|
101
|
+
# @return [String] - Returns a cleaned up and trimmed host with trailing slashs removed
|
102
|
+
def trim_host(str = nil)
|
103
|
+
return nil if str.blank?
|
104
|
+
|
105
|
+
str&.strip&.downcase&.gsub(%r{/?++$}, '')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless defined?(Object.blank?)
|
4
|
+
class Object
|
5
|
+
def blank?
|
6
|
+
respond_to?(:empty?) ? !!empty? : !self
|
7
|
+
end
|
8
|
+
|
9
|
+
def present?
|
10
|
+
!blank?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Used to override and give String the blank? validation similar to Rails
|
15
|
+
class String
|
16
|
+
# Used to determine if the object is nil or empty ('')
|
17
|
+
# @return [Boolean]
|
18
|
+
def blank?
|
19
|
+
strip&.empty? || strip&.length&.zero?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Similar to Rails imlemneation https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/activesupport/lib/active_support/core_ext/object/blank.rb#L56
|
24
|
+
class NilClass
|
25
|
+
# Used to determine if the object is blank? || empty?
|
26
|
+
# *Note:* If a nil value is specified it *should* always be blank? || empty?
|
27
|
+
# @return [Boolean]
|
28
|
+
def blank?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Used to override and give Number the blank? validation similar to Rails
|
34
|
+
class Numeric
|
35
|
+
# Used to determine if the object is blank? || empty?
|
36
|
+
#
|
37
|
+
# *Note:* If a object is declared a Numerica valye, it shouldn't ever be blank
|
38
|
+
# @return [Boolean]
|
39
|
+
def blank?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Used to override and give Array the blank? validation similar to Rails
|
45
|
+
class Array
|
46
|
+
# Used to determine if the object is blank? || empty?
|
47
|
+
# @return [Boolean]
|
48
|
+
alias blank? empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Used to override and give Hash the blank? validation similar to Rails
|
52
|
+
class Hash
|
53
|
+
alias blank? empty?
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
class Object
|
6
|
+
# In case anything other than a string is called with escape_str
|
7
|
+
# @return [String]
|
8
|
+
def escape_str
|
9
|
+
''
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class String
|
14
|
+
# Escape any special characters to make them URL safe
|
15
|
+
# @return [String]
|
16
|
+
def escape_str
|
17
|
+
CGI.escape(strip)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NilClass
|
22
|
+
# If the identity value was never decalred
|
23
|
+
# @return [String]
|
24
|
+
def escape_str
|
25
|
+
''
|
26
|
+
end
|
27
|
+
end
|
data/lib/sailpoint/helpers.rb
CHANGED
@@ -1,92 +1,28 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
class NilClass
|
28
|
-
# Used to determine if the object is blank? || empty?
|
29
|
-
# *Note:* If a nil value is specified it *should* always be blank? || empty?
|
30
|
-
# @return [true, false]
|
31
|
-
def blank?
|
32
|
-
true
|
33
|
-
end
|
34
|
-
|
35
|
-
# Used to determine if the object is not nil
|
36
|
-
# @return [true, false]
|
37
|
-
def present?
|
38
|
-
!blank?
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sailpoint
|
4
|
+
class Helpers
|
5
|
+
# Used to printout a [Red] message to STDOUT in case you want to print a message without causing an Exception.
|
6
|
+
# @param msg [String] - The excpetion message that should be printed out
|
7
|
+
def self.print_exception(msg = '')
|
8
|
+
puts "\e[31m#{msg}\e[0m"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used to generate an Exception error hen invalid credentails have been supplied
|
12
|
+
class AuthenticationException < StandardError
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
# Used to generate an ExceptionError message when Authenication has failed
|
16
|
+
# @param data [String] - The message in which you wish to send to STDOUT for the exception error
|
17
|
+
def initialize(data = 'An API Authentication error has occured.')
|
18
|
+
super
|
19
|
+
@data = data
|
20
|
+
end
|
21
|
+
|
22
|
+
# Specify the attribute in which to push to STDOUT when generating a Ruby ExceptionError
|
23
|
+
def message
|
24
|
+
@data
|
25
|
+
end
|
26
|
+
end
|
39
27
|
end
|
40
28
|
end
|
41
|
-
|
42
|
-
# Used to override and give Number the blank? validation similar to Rails
|
43
|
-
class Numeric
|
44
|
-
# Used to determine if the object is blank? || empty?
|
45
|
-
#
|
46
|
-
# *Note:* If a object is declared a Numerica valye, it shouldn't ever be blank
|
47
|
-
# @return [true, false]
|
48
|
-
def blank?
|
49
|
-
false
|
50
|
-
end
|
51
|
-
|
52
|
-
# Used to determine if the object is not nil
|
53
|
-
# @return [true, false]
|
54
|
-
def present?
|
55
|
-
!blank?
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Used to override and give Array the blank? validation similar to Rails
|
60
|
-
class Array
|
61
|
-
# Used to determine if the object is blank? || empty?
|
62
|
-
# @return [true, false]
|
63
|
-
alias_method :blank?, :empty?
|
64
|
-
end
|
65
|
-
|
66
|
-
# Used to override and give Hash the blank? validation similar to Rails
|
67
|
-
class Hash
|
68
|
-
alias_method :blank?, :empty?
|
69
|
-
|
70
|
-
# Used to determine if the object is not nil
|
71
|
-
# @return [true, false]
|
72
|
-
def present?
|
73
|
-
!blank?
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Used to generate an Exception error hen invalid credentails have been supplied
|
78
|
-
class AuthenticationException < StandardError
|
79
|
-
attr_reader :data
|
80
|
-
|
81
|
-
# Used to generate an ExceptionError message when Authenication has failed
|
82
|
-
# @param data [String] - The message in which you wish to send to STDOUT for the exception error
|
83
|
-
def initialize(data = 'An API Authentication error has occured.')
|
84
|
-
super
|
85
|
-
@data = data
|
86
|
-
end
|
87
|
-
|
88
|
-
# Specify the attribute in which to push to STDOUT when generating a Ruby ExceptionError
|
89
|
-
def message
|
90
|
-
@data
|
91
|
-
end
|
92
|
-
end
|
data/lib/sailpoint/rest.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'httparty'
|
2
4
|
require 'json'
|
3
5
|
|
@@ -5,12 +7,16 @@ require 'json'
|
|
5
7
|
module Sailpoint
|
6
8
|
# Used for created REST API calls to the organizations IdentityIQ source
|
7
9
|
class Rest
|
10
|
+
EMPTY_RESPONSE = '{}'
|
11
|
+
EMPTY_HASH = {}.freeze
|
12
|
+
|
8
13
|
# Used to verify if the supplied credentials are valid
|
9
14
|
# @return [Hash] - The responses body as a JSON hash
|
10
15
|
def self.authenticate
|
11
|
-
|
12
|
-
|
13
|
-
|
16
|
+
set_rest_interface
|
17
|
+
response = HTTParty.get([Sailpoint.config.url, 'authentication'].join('/'),
|
18
|
+
headers: Sailpoint.config.auth_header,
|
19
|
+
output: 'json', timeout: 10)
|
14
20
|
JSON.parse(response)
|
15
21
|
end
|
16
22
|
|
@@ -21,61 +27,76 @@ module Sailpoint
|
|
21
27
|
def self.check_roles(identity, roles)
|
22
28
|
# Values for both attributes are required in order to create the request
|
23
29
|
# And verify the user exists before attempting to create the request (this prevents IdentityIQ from making a long last query looking for a non-existant user)
|
24
|
-
return
|
30
|
+
return EMPTY_HASH if identity.blank? || roles.blank? || get_user(identity).empty?
|
25
31
|
|
32
|
+
set_rest_interface
|
26
33
|
# the roles attribute should either be 'Contractor,Assistant' or ['Contractor', 'Assistant']
|
27
34
|
roles = roles.join(',') if roles.is_a?(Array)
|
28
|
-
response = HTTParty.get([Sailpoint
|
29
|
-
headers: Sailpoint
|
30
|
-
output: 'json')
|
31
|
-
JSON.parse(response&.body ||
|
35
|
+
response = HTTParty.get([Sailpoint.config.url, "policies/checkRolePolicies?identity=#{identity&.escape_str}&roles=#{roles}"].join('/'),
|
36
|
+
headers: Sailpoint.config.auth_header,
|
37
|
+
output: 'json', timeout: 10)
|
38
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
32
39
|
end
|
33
40
|
|
34
41
|
# Used to fetch the specified user identiy from the REST API interface
|
35
42
|
# @param identity [String] - The user in which you are requesting data for
|
36
43
|
# @return [Hash] - If no user if found an empty hash will be returned. If a a user is found, their parsed JSON will be returned as a result
|
37
44
|
def self.get_identity(identity)
|
38
|
-
|
39
|
-
|
45
|
+
set_rest_interface
|
46
|
+
response = HTTParty.get([Sailpoint.config.url, 'identities', identity&.escape_str, 'managedIdentities'].join('/'),
|
47
|
+
headers: Sailpoint.config.auth_header,
|
40
48
|
output: 'json', timeout: 10)
|
41
|
-
return [] if response.code == '500'
|
49
|
+
return [].freeze if response.code == '500'
|
42
50
|
|
43
|
-
JSON.parse(response&.body ||
|
51
|
+
JSON.parse(response&.body || EMPTY_RESPONSE).first
|
44
52
|
end
|
45
53
|
|
46
54
|
# Used to fetch the specified users associated data
|
47
55
|
# @param identity [String] - The user in which you are requesting data for
|
48
56
|
# @return [Hash] - If no user if found an empty hash will be returned. If a a user is found, their parsed JSON will be returned as a result
|
49
57
|
def self.get_user(identity)
|
50
|
-
|
51
|
-
|
58
|
+
set_rest_interface
|
59
|
+
response = HTTParty.get([Sailpoint.config.url, 'identities', identity&.escape_str].join('/'),
|
60
|
+
headers: Sailpoint.config.auth_header,
|
52
61
|
output: 'json', timeout: 10)
|
53
|
-
raise AuthenticationException, 'Invalid credentials, please try again.' if response.code == 401
|
62
|
+
raise Sailpoint::Helpers::AuthenticationException, 'Invalid credentials, please try again.' if response.code == 401
|
54
63
|
|
55
|
-
JSON.parse(response&.body ||
|
64
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
56
65
|
end
|
57
66
|
|
58
67
|
# Get a users roles within the Organization
|
59
68
|
# @return [Hash] - The users roles associated within the Organization
|
60
|
-
def self.permitted_roles(identity)
|
69
|
+
def self.permitted_roles(identity = '')
|
61
70
|
# Before requesting a users roles we need to verify if the identiy matches a valid user first
|
62
|
-
return
|
71
|
+
return EMPTY_HASH if identity.blank? || get_user(identity).blank?
|
63
72
|
|
64
|
-
|
65
|
-
|
73
|
+
set_rest_interface
|
74
|
+
response = HTTParty.get([Sailpoint.config.url, "roles/assignablePermits/?roleMode=assignable&identity=#{identity&.escape_str}"].join('/'),
|
75
|
+
headers: Sailpoint.config.auth_header,
|
66
76
|
output: 'json', timeout: 10)
|
67
|
-
response_body = JSON.parse(response&.body ||
|
77
|
+
response_body = JSON.parse(response&.body || EMPTY_RESPONSE)
|
68
78
|
return response_body['objects'].map { |role| role['name'] } if response['status'].present? && response['status'] == 'success'
|
69
79
|
|
70
80
|
response_body
|
81
|
+
rescue
|
82
|
+
EMPTY_HASH
|
71
83
|
end
|
72
84
|
|
73
85
|
# Used to verify your credentials are valid and IdentityIQ reource is properly responding
|
74
86
|
# @return [Hash] - The head and body of the response
|
75
87
|
def self.ping
|
76
|
-
|
77
|
-
|
88
|
+
set_rest_interface
|
89
|
+
HTTParty.get([Sailpoint.config.url, 'ping'].join('/'),
|
90
|
+
headers: Sailpoint.config.auth_header,
|
78
91
|
output: 'json', timeout: 10)
|
92
|
+
rescue
|
93
|
+
false
|
79
94
|
end
|
95
|
+
|
96
|
+
def self.set_rest_interface
|
97
|
+
Sailpoint.config.interface = 'rest'
|
98
|
+
end
|
99
|
+
|
100
|
+
private_class_method :set_rest_interface
|
80
101
|
end
|
81
102
|
end
|
data/lib/sailpoint/scim.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'httparty'
|
2
4
|
require 'json'
|
3
5
|
|
@@ -5,81 +7,97 @@ require 'json'
|
|
5
7
|
module Sailpoint
|
6
8
|
# Used for created SCIM API calls to the organizations IdentityIQ source
|
7
9
|
class Scim
|
10
|
+
EMPTY_RESPONSE = '{}'
|
11
|
+
|
8
12
|
# Returns a massive list of all account entries in the IdeneityIQ sources
|
9
13
|
# @return [Hash] - A hashed list of all IdenityIQ accounts [Service and User accounts]
|
10
14
|
def self.accounts
|
11
|
-
|
12
|
-
|
15
|
+
set_scim_interface
|
16
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/Accounts'].join('/'),
|
17
|
+
headers: Sailpoint.config.auth_header,
|
13
18
|
output: 'json', timeout: 10)
|
14
|
-
JSON.parse(response&.body ||
|
19
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
15
20
|
end
|
16
21
|
|
17
22
|
# Used to fetch a list of all Applications and their associated attributes
|
18
23
|
# @return [Hash] - A hash of all avaialble applications and their associated MetaData attributes
|
19
24
|
def self.applications
|
20
|
-
|
21
|
-
|
25
|
+
set_scim_interface
|
26
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/Applications'].join('/'),
|
27
|
+
headers: Sailpoint.config.auth_header,
|
22
28
|
output: 'json', timeout: 10)
|
23
|
-
JSON.parse(response&.body ||
|
29
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
24
30
|
end
|
25
31
|
|
26
32
|
# Used to fetch the specified users associated data
|
27
33
|
# @return [Hash] - The users hashed data attributes
|
28
34
|
def self.get_user(identity)
|
29
|
-
|
30
|
-
|
35
|
+
set_scim_interface
|
36
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/Users', identity].join('/'),
|
37
|
+
headers: Sailpoint.config.auth_header,
|
31
38
|
output: 'json', timeout: 10)
|
32
39
|
# NOTE: If invalid credentials are supplied or the user could not be found response bodies contain a status code.
|
33
40
|
# => But if a a user if found, a status code isn't returned, but all of their data attributes are returned instead.
|
34
|
-
raise AuthenticationException, 'Invalid credentials, please try again.' if response.body['status'] && response.body['status'] == '401'
|
35
|
-
return [] if response.body && response.body['status'] && response.body['status'] == '404'
|
41
|
+
raise Sailpoint::Helpers::AuthenticationException, 'Invalid credentials, please try again.' if response.body['status'] && response.body['status'] == '401'
|
42
|
+
return [].freeze if response.body && response.body['status'] && response.body['status'] == '404'
|
36
43
|
|
37
|
-
JSON.parse(response&.body ||
|
44
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
38
45
|
end
|
39
46
|
|
40
47
|
# Fetch all resource types associated with the IdentityIQ API
|
41
48
|
# @return [Hash] - A hash of all resources types [Users, Applications, Accounts, Roles, etc.]
|
42
49
|
def self.resource_types
|
43
|
-
|
44
|
-
|
50
|
+
set_scim_interface
|
51
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/ResourceTypes'].join('/'),
|
52
|
+
headers: Sailpoint.config.auth_header,
|
45
53
|
output: 'json', timeout: 10)
|
46
|
-
JSON.parse(response&.body ||
|
54
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
47
55
|
end
|
48
56
|
|
49
57
|
# Fetch the schemas for all resources types assocaited with the API's returning data
|
50
58
|
# @return [Hash] - A hash of all all ResourceType Schemas
|
51
59
|
def self.schemas
|
52
|
-
|
53
|
-
|
60
|
+
set_scim_interface
|
61
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/Schemas'].join('/'),
|
62
|
+
headers: Sailpoint.config.auth_header,
|
54
63
|
output: 'json', timeout: 10)
|
55
|
-
JSON.parse(response&.body ||
|
64
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
56
65
|
end
|
57
66
|
|
58
67
|
# Fetch a list of all ServiceProviders associated with the data being served by the API
|
59
68
|
# @return [Hash] - A hashed list of SailPoint service providers associated with the IdentityIQ Instance
|
60
69
|
def self.service_providers
|
61
|
-
|
62
|
-
|
70
|
+
set_scim_interface
|
71
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/ServiceProviderConfig'].join('/'),
|
72
|
+
headers: Sailpoint.config.auth_header,
|
63
73
|
output: 'json', timeout: 10)
|
64
|
-
JSON.parse(response&.body ||
|
74
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
65
75
|
end
|
66
76
|
|
67
77
|
# Returns a list of all users from the associated organizations
|
68
78
|
# @return [Hash] - All users entries from the organizations sources
|
69
79
|
def self.users
|
70
|
-
|
71
|
-
|
80
|
+
set_scim_interface
|
81
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/Users'].join('/'),
|
82
|
+
headers: Sailpoint.config.auth_header,
|
72
83
|
output: 'json', timeout: 10)
|
73
|
-
JSON.parse(response&.body ||
|
84
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
74
85
|
end
|
75
86
|
|
76
87
|
# Returns a list of data attributes for the ResourceType -> Users
|
77
88
|
# @return [Hash] - A hash to describe the user schema attributes
|
78
89
|
def self.user_resource_types
|
79
|
-
|
80
|
-
|
90
|
+
set_scim_interface
|
91
|
+
response = HTTParty.get([Sailpoint.config.url, 'v2/ResourceTypes/User'].join('/'),
|
92
|
+
headers: Sailpoint.config.auth_header,
|
81
93
|
output: 'json', timeout: 10)
|
82
|
-
JSON.parse(response&.body ||
|
94
|
+
JSON.parse(response&.body || EMPTY_RESPONSE)
|
83
95
|
end
|
96
|
+
|
97
|
+
def self.set_scim_interface
|
98
|
+
Sailpoint.config.interface = 'scim'
|
99
|
+
end
|
100
|
+
|
101
|
+
private_class_method :set_scim_interface
|
84
102
|
end
|
85
103
|
end
|
data/lib/sailpoint/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sailpoint
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Hicks
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: '0.18'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: '0.18'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -50,29 +50,29 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
53
|
+
version: '13'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '
|
60
|
+
version: '13'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: rspec
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 3.
|
67
|
+
version: '3.9'
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 3.
|
75
|
-
description:
|
74
|
+
version: '3.9'
|
75
|
+
description: An helper for making API requests to a Sailpoint/IdentityIQ APIendpoint
|
76
76
|
email:
|
77
77
|
- tarellel@gmail.com
|
78
78
|
executables: []
|
@@ -86,16 +86,21 @@ files:
|
|
86
86
|
- bin/console
|
87
87
|
- bin/setup
|
88
88
|
- lib/sailpoint.rb
|
89
|
-
- lib/sailpoint/
|
89
|
+
- lib/sailpoint/configuration.rb
|
90
|
+
- lib/sailpoint/core_ext/blank.rb
|
91
|
+
- lib/sailpoint/core_ext/escape_str.rb
|
90
92
|
- lib/sailpoint/helpers.rb
|
91
93
|
- lib/sailpoint/rest.rb
|
92
94
|
- lib/sailpoint/scim.rb
|
93
95
|
- lib/sailpoint/version.rb
|
94
|
-
homepage:
|
96
|
+
homepage: https://github.com/tarellel/sailpoint
|
95
97
|
licenses:
|
96
98
|
- MIT
|
97
|
-
metadata:
|
98
|
-
|
99
|
+
metadata:
|
100
|
+
bug_tracker_uri: https://github.com/tarellel/sailpoint/issues
|
101
|
+
changelog_uri: https://github.com/tarellel/sailpoint/blob/master/CHANGELOG.md
|
102
|
+
source_code_uri: https://github.com/tarellel/sailpoint
|
103
|
+
post_install_message:
|
99
104
|
rdoc_options: []
|
100
105
|
require_paths:
|
101
106
|
- lib
|
@@ -110,8 +115,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
115
|
- !ruby/object:Gem::Version
|
111
116
|
version: '0'
|
112
117
|
requirements: []
|
113
|
-
rubygems_version: 3.
|
114
|
-
signing_key:
|
118
|
+
rubygems_version: 3.2.3
|
119
|
+
signing_key:
|
115
120
|
specification_version: 4
|
116
121
|
summary: A Sailpoint API helper
|
117
122
|
test_files: []
|
data/lib/sailpoint/config.rb
DELETED
@@ -1,105 +0,0 @@
|
|
1
|
-
require 'base64'
|
2
|
-
require 'sailpoint/helpers'
|
3
|
-
|
4
|
-
# Used for setting you Sailpoint API configuration and credentials
|
5
|
-
module Sailpoint
|
6
|
-
# Used for setting API configuration before creating API Requests
|
7
|
-
# Configuration can include: <code>username, password, interface, host, url</code>
|
8
|
-
class Config
|
9
|
-
attr_accessor :username, :password, :interface, :url
|
10
|
-
|
11
|
-
@username = nil
|
12
|
-
@password = nil
|
13
|
-
@interface = 'rest' # By default lets resort to using the Rest interface
|
14
|
-
@host = nil # Where the SailPoint server is hosted
|
15
|
-
|
16
|
-
class << self
|
17
|
-
attr_writer :username, :password, :interface, :host, :url
|
18
|
-
|
19
|
-
# Used to set the API requests BasicAuth credentails
|
20
|
-
# @param username [String] - Username for the API request
|
21
|
-
# @param password [String] - Password for the API request
|
22
|
-
def set_credentials(username, password)
|
23
|
-
@username = username unless username.nil?
|
24
|
-
@password = password unless password.nil?
|
25
|
-
end
|
26
|
-
|
27
|
-
# Used for setting if the interface type is Rest || SCIM
|
28
|
-
# @return [String] - Returns either the specified interface (Default: <code>rest</code>)
|
29
|
-
def interface
|
30
|
-
(@interface ||= 'rest')
|
31
|
-
end
|
32
|
-
|
33
|
-
# Used for fetching the API interface_path based on the API interface specification
|
34
|
-
# @return [String] - Returns the API's interface path, based on interface type
|
35
|
-
def interface_path
|
36
|
-
(@interface == 'scim' ? 'scim' : 'rest')
|
37
|
-
end
|
38
|
-
|
39
|
-
# Used to fetch the host for the API request
|
40
|
-
# @return [String] - If a valid host was specified, it will returned a trimmed string with trailing whitespaces and slashes removed
|
41
|
-
def host
|
42
|
-
return '' if @host.blank?
|
43
|
-
|
44
|
-
trimmed_host
|
45
|
-
end
|
46
|
-
|
47
|
-
# Used for fetching the requesting users entire URL (Host+Interface)
|
48
|
-
# @param interface [String] - used for when the user is specicically calling an API such as <code>Unmh::Sailpoint::Scim.get_user('brhicks')</code>
|
49
|
-
# @return [String] - Returns the entire requesting URL (based on host and interface type)
|
50
|
-
def url(interface = '')
|
51
|
-
return '' if @host.blank? || @interface.blank?
|
52
|
-
|
53
|
-
full_host(interface)
|
54
|
-
end
|
55
|
-
|
56
|
-
def full_host(interface = '')
|
57
|
-
interface.blank? ? [trimmed_host, 'identityiq', interface_path].join('/') : [trimmed_host, 'identityiq', interface].join('/')
|
58
|
-
end
|
59
|
-
|
60
|
-
# Used for fetching credentails username (if it has been set)
|
61
|
-
# @return [String] - The credentails username
|
62
|
-
def username
|
63
|
-
@username || ''
|
64
|
-
end
|
65
|
-
|
66
|
-
# Used for fetching the requesting users credentials password (if it has been set)
|
67
|
-
# @return [String] - The password for the API credentials
|
68
|
-
def password
|
69
|
-
@password || ''
|
70
|
-
end
|
71
|
-
|
72
|
-
# Used for fetching the API credentials when setting API requests headers
|
73
|
-
# @return [String] - Return a hash of the current API credentils (for validation purposes)
|
74
|
-
def credentials
|
75
|
-
{ username: @username, password: @password }
|
76
|
-
end
|
77
|
-
|
78
|
-
# Remove trailing forward slashes from the end of the Host, that way hosts and interfaces can be properly joined
|
79
|
-
# => I also did this because you'd get the same results if something supplied `http://example.com` or `https://example.com/`
|
80
|
-
# @return [String] - Returns a cleaned up and trimmed host with trailing slashs removed
|
81
|
-
def trimmed_host
|
82
|
-
return '' if @host.blank?
|
83
|
-
|
84
|
-
@host.strip.gsub!(%r{\/?++$}, '')
|
85
|
-
end
|
86
|
-
|
87
|
-
# SailPoints auth requires a Base64 string of (username:password)
|
88
|
-
# This is how most BasicAuth authentication methods work
|
89
|
-
# @return [String] - It will either return an empty string or a Base64.encoded hash for the the API credentials (BasicAuth requires Base64)
|
90
|
-
def hashed_credentials
|
91
|
-
return '' if @username.blank? && @password.blank?
|
92
|
-
|
93
|
-
Base64.encode64("#{@username}:#{@password}").strip
|
94
|
-
end
|
95
|
-
|
96
|
-
# Used for generating the API BasicAuth Header when creating an API request
|
97
|
-
# @return [String] - Return the API Authorization header for the making API requests
|
98
|
-
def auth_header
|
99
|
-
return '' if @username.blank? && @password.blank?
|
100
|
-
|
101
|
-
{ 'Authorization' => "Basic #{Sailpoint::Config.hashed_credentials}" }
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|