identikey 0.3.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.
@@ -0,0 +1,211 @@
1
+ require 'identikey/base'
2
+ require 'identikey/administration/session'
3
+ require 'identikey/administration/session_query'
4
+ require 'identikey/administration/digipass'
5
+ require 'identikey/administration/user'
6
+
7
+ module Identikey
8
+ # This class wraps the Administration API wsdl, that contains dozens of
9
+ # methods. It is currently monolithic.
10
+ #
11
+ # It's the lower level into the Administration API, while its models are
12
+ # wrapped in separate clasess.
13
+ #
14
+ class Administration < Base
15
+ client wsdl: './sdk/wsdl/administration.wsdl'
16
+
17
+ operations :logon, :logoff, :sessionalive,
18
+ :admin_session_query, :user_execute,
19
+ :digipass_execute, :digipassappl_execute
20
+
21
+ def logon(username:, password:, domain:)
22
+ resp = super(message: {
23
+ attributeSet: {
24
+ attributes: typed_attributes_list_from(
25
+ CREDFLD_DOMAIN: domain,
26
+ CREDFLD_PASSWORD: password,
27
+ CREDFLD_USERID: username,
28
+ CREDFLD_PASSWORD_FORMAT: Unsigned(0)
29
+ )
30
+ }
31
+ })
32
+
33
+ parse_response resp, :logon_response
34
+ end
35
+
36
+ def logoff(session_id:)
37
+ resp = super(message: {
38
+ attributeSet: {
39
+ attributes: typed_attributes_list_from(
40
+ CREDFLD_SESSION_ID: session_id
41
+ )
42
+ }
43
+ })
44
+
45
+ parse_response resp, :logoff_response
46
+ end
47
+
48
+ def sessionalive(session_id:)
49
+ resp = super(message: {
50
+ attributeSet: {
51
+ attributes: typed_attributes_list_from(
52
+ CREDFLD_SESSION_ID: session_id
53
+ )
54
+ }
55
+ })
56
+
57
+ parse_response resp, :sessionalive_response
58
+ end
59
+
60
+ def admin_session_query(session_id:)
61
+ attributes = [ ]
62
+
63
+ # These doesn't seem to work as described by the WSDL.
64
+ # if q_idx
65
+ # attributes.push(attributeID: 'ADMINSESSIONFLD_SESSION_IDX',
66
+ # value: { '@xsi:type': 'xsd:string', content!: q_idx})
67
+ # end
68
+
69
+ # if q_location
70
+ # attributes.push(attributeID: 'ADMINSESSIONFLD_LOCATION',
71
+ # value: { '@xsi:type': 'xsd:string', content!: q_location})
72
+ # end
73
+
74
+ # if q_username
75
+ # attributes.push(attributeID: 'ADMINSESSIONFLD_LOGIN_NAME',
76
+ # value: { '@xsi:type': 'xsd:string', content!: q_username})
77
+ # end
78
+
79
+ resp = super(message: {
80
+ sessionID: session_id,
81
+ attributeSet: {
82
+ attributes: attributes
83
+ }
84
+ # fieldSet: { ... }
85
+ # queryOptions: { ... }
86
+ })
87
+
88
+ parse_response resp, :admin_session_query_response
89
+ end
90
+
91
+ def user_execute(session_id:, cmd:, attributes: [])
92
+ resp = super(message: {
93
+ sessionID: session_id,
94
+ cmd: cmd,
95
+ attributeSet: {
96
+ attributes: attributes
97
+ }
98
+ })
99
+
100
+ parse_response resp, :user_execute_response
101
+ end
102
+
103
+ def user_execute_VIEW(session_id:, username:, domain:)
104
+ user_execute(
105
+ session_id: session_id,
106
+ cmd: 'USERCMD_VIEW',
107
+ attributes: typed_attributes_list_from(
108
+ USERFLD_USERID: username,
109
+ USERFLD_DOMAIN: domain
110
+ )
111
+ )
112
+ end
113
+
114
+ def user_execute_CREATE(session_id:, attributes:)
115
+ user_execute(
116
+ session_id: session_id,
117
+ cmd: 'USERCMD_CREATE',
118
+ attributes: typed_attributes_list_from(attributes)
119
+ )
120
+ end
121
+
122
+ def user_execute_UPDATE(session_id:, attributes:)
123
+ user_execute(
124
+ session_id: session_id,
125
+ cmd: 'USERCMD_UPDATE',
126
+ attributes: typed_attributes_list_from(attributes)
127
+ )
128
+ end
129
+
130
+ def user_execute_DELETE(session_id:, username:, domain:)
131
+ user_execute(
132
+ session_id: session_id,
133
+ cmd: 'USERCMD_DELETE',
134
+ attributes: typed_attributes_list_from(
135
+ USERFLD_USERID: username,
136
+ USERFLD_DOMAIN: domain
137
+ )
138
+ )
139
+ end
140
+
141
+ def digipass_execute(session_id:, cmd:, attributes: [])
142
+ resp = super(message: {
143
+ sessionID: session_id,
144
+ cmd: cmd,
145
+ attributeSet: {
146
+ attributes: attributes
147
+ }
148
+ })
149
+
150
+ parse_response resp, :digipass_execute_response
151
+ end
152
+
153
+ def digipass_execute_VIEW(session_id:, serial_no:)
154
+ digipass_execute(
155
+ session_id: session_id,
156
+ cmd: 'DIGIPASSCMD_VIEW',
157
+ attributes: typed_attributes_list_from(
158
+ DIGIPASSFLD_SERNO: serial_no
159
+ )
160
+ )
161
+ end
162
+
163
+ def digipass_execute_UNASSIGN(session_id:, serial_no:)
164
+ digipass_execute(
165
+ session_id: session_id,
166
+ cmd: 'DIGIPASSCMD_UNASSIGN',
167
+ attributes: typed_attributes_list_from(
168
+ DIGIPASSFLD_SERNO: serial_no
169
+ )
170
+ )
171
+ end
172
+
173
+ def digipass_execute_ASSIGN(session_id:, serial_no:, username:, domain:, grace_period: 0)
174
+ digipass_execute(
175
+ session_id: session_id,
176
+ cmd: 'DIGIPASSCMD_ASSIGN',
177
+ attributes: typed_attributes_list_from(
178
+ DIGIPASSFLD_SERNO: serial_no,
179
+ DIGIPASSFLD_ASSIGNED_USERID: username,
180
+ DIGIPASSFLD_DOMAIN: domain,
181
+ DIGIPASSFLD_GRACE_PERIOD_DAYS: grace_period
182
+ )
183
+ )
184
+ end
185
+
186
+ def digipassappl_execute(session_id:, cmd:, attributes:)
187
+ resp = super(message: {
188
+ sessionID: session_id,
189
+ cmd: cmd,
190
+ attributeSet: {
191
+ attributes: attributes
192
+ }
193
+ })
194
+
195
+ parse_response resp, :digipassappl_execute_response
196
+ end
197
+
198
+ def digipassappl_execute_TEST_OTP(session_id:, serial_no:, appl:, otp:)
199
+ digipassappl_execute(
200
+ session_id: session_id,
201
+ cmd: 'DIGIPASSAPPLCMD_TEST_OTP',
202
+ attributes: typed_attributes_list_from(
203
+ DIGIPASSAPPLFLD_SERNO: serial_no,
204
+ DIGIPASSAPPLFLD_APPL_NAME: appl,
205
+ DIGIPASSAPPLFLD_RESPONSE: otp
206
+ )
207
+ )
208
+ end
209
+
210
+ end
211
+ end
@@ -0,0 +1,56 @@
1
+ require 'identikey/base'
2
+
3
+ module Identikey
4
+ class Authentication < Base
5
+ client wsdl: './sdk/wsdl/authentication.wsdl'
6
+
7
+ operations :auth_user
8
+
9
+ def auth_user(user, domain, otp)
10
+ resp = super(message: {
11
+ credentialAttributeSet: {
12
+ attributes: typed_attributes_list_from(
13
+ CREDFLD_COMPONENT_TYPE: 'Administration Program',
14
+ CREDFLD_USERID: user,
15
+ CREDFLD_DOMAIN: domain,
16
+ CREDFLD_PASSWORD_FORMAT: Unsigned(0),
17
+ CREDFLD_PASSWORD: otp
18
+ )
19
+ }
20
+ })
21
+
22
+ parse_response resp, :auth_user_response
23
+ end
24
+
25
+ def self.valid_otp?(user, domain, otp)
26
+ status, result, _ = new.auth_user(user, domain, otp)
27
+ return otp_validated_ok?(status, result)
28
+ end
29
+
30
+ def self.validate!(user, domain, otp)
31
+ status, result, _ = new.auth_user(user, domain, otp)
32
+
33
+ if otp_validated_ok?(status, result)
34
+ return true
35
+ else
36
+ error_message = result['CREDFLD_STATUS_MESSAGE']
37
+ raise Identikey::Error, "OTP Validation error (#{status}): #{error_message}"
38
+ end
39
+ end
40
+
41
+ # Given an authentication status and result message, returns true if
42
+ # that defines a successful OTP validation or not.
43
+ #
44
+ # For all cases, except where the OTP is "push", Identikey returns a
45
+ # status that is != than `STAT_SUCCESS`. But when the OTP is "push",
46
+ # then Identikey returns a `STAT_SUCCESS` with a "password is wrong"
47
+ # message in the `CREDFLD_STATUS_MESSAGE`.
48
+ #
49
+ # This method checks for both cases.. Success means a `STAT_SUCCESS`
50
+ # and nothing in the `CREDFLD_STATUS_MESSAGE`.
51
+ #
52
+ def self.otp_validated_ok?(status, result)
53
+ status == 'STAT_SUCCESS' && !result.key?('CREDFLD_STATUS_MESSAGE')
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,272 @@
1
+ module Identikey
2
+
3
+ class Base
4
+ extend Savon::Model
5
+
6
+ def self.configure(&block)
7
+ self.client.globals.instance_eval(&block)
8
+
9
+ # Work around a sillyness in Savon
10
+ if client.globals[:wsdl] != client.wsdl.document
11
+ client.wsdl.document = client.globals[:wsdl]
12
+ end
13
+ end
14
+
15
+ def self.client(options = nil)
16
+ return super() unless options
17
+
18
+ options = DEFAULTS.merge(options)
19
+ options = process_identikey_filters(options)
20
+
21
+ super options
22
+ end
23
+
24
+ def self.default_user_agent_header
25
+ {'User-Agent' => "ruby/identikey #{Identikey::VERSION}"}
26
+ end
27
+
28
+ # Loops over the filters option content and adds Identikey
29
+ # specific parameter filtering.
30
+ #
31
+ # Due to faulty design in the Identikey SOAP endpoint, the
32
+ # parameter filters require context-dependant logic as all
33
+ # attributes are passed in `<attributeID>` elements, while
34
+ # all values are passed in `<value>` elements.
35
+ #
36
+ # Identikey attributes to filter out are specified in the
37
+ # `filters` option with the `identikey:` prefix.
38
+ #
39
+ # Example, filter out the `CREDFLD_PASSWORD` field from
40
+ # the logs (done by default):
41
+ #
42
+ # configure do
43
+ # filters [ 'identikey:CREDFLD_PASSWORD' ]
44
+ # end
45
+ #
46
+ def self.process_identikey_filters(options)
47
+ filters = options[:filters] || []
48
+
49
+ options[:filters] = filters.map do |filter|
50
+ if filter.to_s =~ /^identikey:(.+)/
51
+ filter = identikey_filter_proc_for($1)
52
+ end
53
+
54
+ filter
55
+ end
56
+
57
+ return options
58
+ end
59
+
60
+ def self.identikey_filter_proc_for(attribute)
61
+ lambda do |document|
62
+ document.xpath("//attributeID[text()='#{attribute}']/../value").each do |node|
63
+ node.content = '***FILTERED***'
64
+ end
65
+ end
66
+ end
67
+
68
+ DEFAULTS = {
69
+ endpoint: 'https://localhost:8888/',
70
+
71
+ ssl_version: :TLSv1_2,
72
+ ssl_verify_mode: :none,
73
+
74
+ headers: default_user_agent_header,
75
+
76
+ encoding: 'UTF-8',
77
+
78
+ logger: Logger.new('log/identikey.log'),
79
+ log_level: :debug,
80
+ log: true,
81
+ pretty_print_xml: true,
82
+
83
+ filters: [
84
+ 'identikey:CREDFLD_PASSWORD',
85
+ 'identikey:CREDFLD_STATIC_PASSWORD',
86
+ 'identikey:CREDFLD_SESSION_ID'
87
+ ]
88
+ }.freeze
89
+
90
+ def endpoint
91
+ self.class.client.globals[:endpoint]
92
+ end
93
+
94
+ def wsdl
95
+ self.class.client.globals[:wsdl]
96
+ end
97
+
98
+ protected
99
+
100
+ # Parse the generic response types that the API returns.
101
+ #
102
+ # The returned attributes (up to now...) are always:
103
+ #
104
+ # - The given root element, whose name is derived from the SOAP command
105
+ # that was invoked
106
+ # - The :results element, containing:
107
+ # - :result_codes, containing :status_code_enum that is the operation
108
+ # result code
109
+ # - :result_attribute, that may either contain a single attributes list
110
+ # or multiple ones.
111
+ # - :error_stack, a list of error that occurred
112
+ #
113
+ # The returned value is a three-elements array, containing:
114
+ #
115
+ # [Response code, Attribute(s) list, Errors list]
116
+ #
117
+ # The response code is a string from the IDENTIKEY Authentication Server
118
+ # Error Codes table.
119
+ #
120
+ # The attributes list is an Hash when a single object's attributes were
121
+ # requested, or is an Array of Hashes when the response contains a list
122
+ # of objects.
123
+ #
124
+ # The attributes list may be nil.
125
+ #
126
+ # The errors list is an array of strings containing error descriptions.
127
+ # The strings themselves contain the error code, albeit in different
128
+ # formats. TODO maybe create a separate class for errors, that includes
129
+ # the error code.
130
+ #
131
+ # TODO refactor and split in separate methods
132
+ #
133
+ def parse_response(resp, root_element)
134
+ body = resp.body
135
+
136
+ if body.size.zero?
137
+ raise Identikey::Error, "Empty response received"
138
+ end
139
+
140
+ unless body.key?(root_element)
141
+ raise Identikey::Error, "Expected response to have #{root_element}, found #{body.keys.join(', ')}"
142
+ end
143
+
144
+ # The root results element
145
+ #
146
+ root = body[root_element]
147
+
148
+ # ... that the authentication API wraps with another element
149
+ #
150
+ results_key = root_element.to_s.sub(/_response$/, '_results').to_sym
151
+ if root.keys.size == 1 && root.key?(results_key)
152
+ root = root[results_key]
153
+ end
154
+
155
+ # The results element
156
+ #
157
+ unless root.key?(:results)
158
+ raise Identikey::Error, "Results element not found below #{root_element}"
159
+ end
160
+
161
+ results = root[:results]
162
+
163
+ # Result code
164
+ #
165
+ unless results.key?(:result_codes)
166
+ raise Identikey::Error, "Result codes not found below #{root_element}"
167
+ end
168
+
169
+ result_code = results[:result_codes][:status_code_enum] || 'STAT_UNKNOWN'
170
+
171
+ # Result attributes
172
+ #
173
+ unless results.key?(:result_attribute)
174
+ raise Identikey::Error, "Result attribute not found below #{root_element}"
175
+ end
176
+
177
+ results_attr = results[:result_attribute]
178
+
179
+ result_attributes = if results_attr.key?(:attributes)
180
+ entries = [ results_attr[:attributes] ].flatten
181
+ parse_attributes entries
182
+
183
+ elsif results_attr.key?(:attribute_list)
184
+ # This attribute may contain a single entry or multiple ones. Lists of
185
+ # a single element are returned as a single attributes set.. but the
186
+ # caller expects a list so we return the single element in an Array.
187
+ #
188
+ entries = [ results_attr[:attribute_list] ].flatten
189
+ entries.inject([]) do |a, entry|
190
+ a.push parse_attributes(entry[:attributes])
191
+ end
192
+ else
193
+ nil
194
+ end
195
+
196
+ # Errors
197
+ #
198
+ errors = if results[:error_stack].key?(:errors)
199
+ parse_errors results[:error_stack][:errors]
200
+ else
201
+ nil
202
+ end
203
+
204
+ return result_code, result_attributes, errors
205
+ end
206
+
207
+ def parse_attributes(attributes)
208
+ attributes.inject({}) do |h, attribute|
209
+ h.update(attribute.fetch(:attribute_id) => attribute.fetch(:value))
210
+ end
211
+ end
212
+
213
+ def parse_errors(errors)
214
+ case errors
215
+ when Array
216
+ errors.map { |e| e.fetch(:error_desc) }
217
+ when Hash
218
+ errors.fetch(:error_desc)
219
+ end
220
+ end
221
+
222
+ # Converts and hash keyed by attribute name into an array of hashes
223
+ # whose keys are the attribute name as attributeID and the value as
224
+ # a Gyoku-compatible hash with the xsd:type annotation. The type is
225
+ # inferred from the Ruby value type and the contents are serialized
226
+ # as a string formatted as per the XSD DTD definition.
227
+ #
228
+ # <rant>
229
+ # This code should not exist, because defining argument types is what
230
+ # WSDL is for. However, in the braindead web services implementation
231
+ # of Vasco there are infinite protocols that accept a variable number
232
+ # of attributes and their types are defined only in the documentation
233
+ # and in server code, making WSDL (and SOAP) only an annoynace rather
234
+ # than an aid.
235
+ # </rant>
236
+ #
237
+ def typed_attributes_list_from(hash)
238
+ hash.map do |name, value|
239
+ type, value = case value
240
+
241
+ when Unsigned
242
+ [ 'xsd:unsignedInt', value.to_s ]
243
+
244
+ when Integer
245
+ [ 'xsd:int', value.to_s ]
246
+
247
+ when DateTime, Time
248
+ [ 'xsd:datetime', value.utc.iso8601 ]
249
+
250
+ when TrueClass, FalseClass
251
+ [ 'xsd:boolean', value.to_s ]
252
+
253
+ when Symbol, String
254
+ [ 'xsd:string', value.to_s ]
255
+
256
+ when NilClass
257
+ next
258
+
259
+ else
260
+ raise Identikey::Error, "#{name} type #{value.class} is unsupported"
261
+ end
262
+
263
+ { attributeID: name.to_s,
264
+ value: { '@xsi:type': type, content!: value } }
265
+ end.compact
266
+ end
267
+
268
+ # protected
269
+
270
+ end
271
+
272
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # Wrapper for an integer immediate value, that is used only as an annotation
3
+ # for typed_attributes_list_from() in order to generate the correct XSD type
4
+ # from an Object's class.
5
+ #
6
+ class Unsigned < BasicObject
7
+ def initialize(value)
8
+ @int = ::Kernel::Integer(value)
9
+
10
+ if @int < 0
11
+ raise ArgumentError, "Invalid input syntax for Unsigned integer: #{value}"
12
+ end
13
+ end
14
+
15
+ def class
16
+ ::Unsigned
17
+ end
18
+
19
+ def method_missing(meth, *args, &block)
20
+ @int.public_send(meth, *args, &block)
21
+ end
22
+ end
23
+
24
+ module Kernel
25
+ def Unsigned(value)
26
+ ::Unsigned.new(value)
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Identikey
2
+ VERSION = "0.3.0"
3
+ end
data/lib/identikey.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'savon'
2
+
3
+ require 'identikey/version'
4
+ require 'identikey/unsigned'
5
+ require 'identikey/authentication'
6
+ require 'identikey/administration'
7
+
8
+ module Identikey
9
+ class Error < StandardError; end
10
+ end
data/log/.keep ADDED
File without changes