identikey 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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