sp-job 0.2.3 → 0.3.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/configure +40 -0
- data/lib/sp-job.rb +21 -2
- data/lib/sp/job/back_burner.rb +350 -68
- data/lib/sp/job/broker.rb +18 -16
- data/lib/sp/job/broker_http_client.rb +80 -20
- data/lib/sp/job/broker_oauth2_client.rb +12 -4
- data/lib/sp/job/common.rb +876 -62
- data/lib/sp/job/configure/configure.rb +640 -0
- data/lib/sp/job/curl_http_client.rb +100 -0
- data/lib/sp/job/easy_http_client.rb +94 -0
- data/lib/sp/job/http_client.rb +51 -0
- data/lib/sp/job/job_db_adapter.rb +38 -36
- data/lib/sp/job/jsonapi_error.rb +31 -74
- data/lib/sp/job/jwt.rb +55 -5
- data/lib/sp/job/mail_queue.rb +9 -2
- data/lib/sp/job/manticore_http_client.rb +94 -0
- data/lib/sp/job/pg_connection.rb +90 -10
- data/lib/sp/job/query_params.rb +45 -0
- data/lib/sp/job/rfc822.rb +13 -0
- data/lib/sp/job/session.rb +239 -0
- data/lib/sp/job/unique_file.rb +37 -1
- data/lib/sp/job/uploaded_image_converter.rb +27 -19
- data/lib/sp/job/worker.rb +51 -1
- data/lib/sp/job/worker_thread.rb +22 -7
- data/lib/sp/jsonapi.rb +24 -0
- data/lib/sp/jsonapi/adapters/base.rb +177 -0
- data/lib/sp/jsonapi/adapters/db.rb +26 -0
- data/lib/sp/jsonapi/adapters/raw_db.rb +96 -0
- data/lib/sp/jsonapi/exceptions.rb +54 -0
- data/lib/sp/jsonapi/model/base.rb +31 -0
- data/lib/sp/jsonapi/model/concerns/attributes.rb +91 -0
- data/lib/sp/jsonapi/model/concerns/model.rb +39 -0
- data/lib/sp/jsonapi/model/concerns/persistence.rb +212 -0
- data/lib/sp/jsonapi/model/concerns/serialization.rb +57 -0
- data/lib/sp/jsonapi/parameters.rb +54 -0
- data/lib/sp/jsonapi/service.rb +96 -0
- data/lib/tasks/configure.rake +2 -496
- data/sp-job.gemspec +3 -2
- metadata +24 -2
data/lib/sp/job/jwt.rb
CHANGED
@@ -25,11 +25,61 @@ module SP
|
|
25
25
|
module Job
|
26
26
|
class JWTHelper
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
# encode & sign jwt
|
29
|
+
def self.encode(key:, payload:)
|
30
|
+
rsa_private = OpenSSL::PKey::RSA.new( File.read( key ) )
|
31
|
+
return JWT.encode payload, rsa_private, 'RS256', { :typ => "JWT" }
|
32
|
+
end #self.encodeJWT
|
33
|
+
|
34
|
+
# key: Path of the private key to be used on encoding
|
35
|
+
# jwt_validity: Must be set in hours
|
36
|
+
# tube: Name of the tube
|
37
|
+
# ttr: Job max execution time in seconds
|
38
|
+
# payload: Data to be used on the job
|
39
|
+
def self.jobify(key:, jwt_validity: 24, tube:, ttr: 8600, payload:)
|
40
|
+
# UTC timestamp
|
41
|
+
now = Time.now.getutc.to_i
|
42
|
+
# Expire
|
43
|
+
exp_offset = jwt_validity * 60 * 60
|
44
|
+
exp = now + exp_offset
|
45
|
+
# Issued At
|
46
|
+
iat = now
|
47
|
+
# Not before
|
48
|
+
nbf = now
|
49
|
+
|
50
|
+
job_payload = { tube: tube }
|
51
|
+
job_payload.merge!(payload)
|
52
|
+
|
53
|
+
self.encode(key: key, payload: {
|
54
|
+
action: 'job',
|
55
|
+
exp: exp, # Data de expiração
|
56
|
+
iat: iat, # Issued at
|
57
|
+
nbf: nbf, # Not before
|
58
|
+
job: {
|
59
|
+
tube: tube,
|
60
|
+
ttr: ttr,
|
61
|
+
payload: job_payload
|
62
|
+
}
|
63
|
+
})
|
64
|
+
end
|
65
|
+
|
66
|
+
# Submit a jwt for a job
|
67
|
+
def self.submit (url:, jwt:)
|
68
|
+
response = HttpClient.get_klass.post(
|
69
|
+
url: url,
|
70
|
+
headers: {
|
71
|
+
'Content-Type' => 'application/text'
|
72
|
+
},
|
73
|
+
body: jwt,
|
74
|
+
expect: {
|
75
|
+
code: 200,
|
76
|
+
content: {
|
77
|
+
type: 'application/json'
|
78
|
+
}
|
79
|
+
}
|
80
|
+
)
|
81
|
+
response
|
82
|
+
end
|
33
83
|
|
34
84
|
end # end class 'JWT'
|
35
85
|
end # module Job
|
data/lib/sp/job/mail_queue.rb
CHANGED
@@ -44,11 +44,18 @@ module SP
|
|
44
44
|
body: job[:body],
|
45
45
|
template: job[:template],
|
46
46
|
to: job[:to],
|
47
|
+
cc: job[:cc],
|
47
48
|
reply_to: job[:reply_to],
|
48
49
|
subject: job[:subject],
|
49
|
-
attachments: job[:attachments]
|
50
|
+
attachments: job[:attachments],
|
51
|
+
session: {
|
52
|
+
user_id: job[:user_id],
|
53
|
+
entity_id: job[:entity_id],
|
54
|
+
role_mask: job[:role_mask],
|
55
|
+
module_mask: job[:module_mask]
|
56
|
+
}
|
50
57
|
)
|
51
|
-
logger.info "
|
58
|
+
logger.info "mail - to: #{job[:to]} cc: #{job[:cc]} subject: #{job[:subject]}"
|
52
59
|
end
|
53
60
|
|
54
61
|
def self.on_failure (e, job)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011-2016 Cloudware S.A. All rights reserved.
|
3
|
+
#
|
4
|
+
# This file is part of sp-job.
|
5
|
+
#
|
6
|
+
# sp-job is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# sp-job is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with sp-job. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
# encoding: utf-8
|
20
|
+
#
|
21
|
+
|
22
|
+
#
|
23
|
+
# A helper class to do HTTP request without session management.
|
24
|
+
#
|
25
|
+
|
26
|
+
require 'manticore'
|
27
|
+
require_relative 'easy_http_client'
|
28
|
+
|
29
|
+
module SP
|
30
|
+
module Job
|
31
|
+
class ManticoreHTTPClient < EasyHttpClient
|
32
|
+
def self.post(url:, headers:, body:, expect:, conn_options: {})
|
33
|
+
conn_options[:connection_timeout] ||= 10
|
34
|
+
conn_options[:request_timeout] ||= 60
|
35
|
+
client = ::Manticore::Client.new(socket_timeout: conn_options[:connection_timeout], request_timeout: conn_options[:request_timeout])
|
36
|
+
nr = self.normalize_response(response: client.post(url, body: body, headers: headers))
|
37
|
+
|
38
|
+
# compare status code
|
39
|
+
if nr[:code] != expect[:code]
|
40
|
+
if 401 == nr[:code]
|
41
|
+
raise ::SP::Job::JSONAPI::Error.new(status: nr[:code], code: 'A01', detail: nil)
|
42
|
+
else
|
43
|
+
raise ::SP::Job::JSONAPI::Error.new(status: nr[:code], code: 'B01', detail: nil)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# compare content-type
|
48
|
+
if nr[:content][:type] != expect[:content][:type]
|
49
|
+
raise ::SP::Job::JSONAPI::Error.new(status: 500, code: 'I01', detail: "Unexpected 'Content-Type': #{nr[:content][:type]}, expected #{expect[:content][:type]}!")
|
50
|
+
end
|
51
|
+
|
52
|
+
# done
|
53
|
+
nr
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.get(url:)
|
57
|
+
client = ::Manticore::Client.new
|
58
|
+
self.normalize_response(response: client.get(url))
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.delete(url:, headers:)
|
62
|
+
client = ::Manticore::Client.new
|
63
|
+
self.normalize_response(response: client.delete(url, headers: headers))
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def self.normalize_response(response:)
|
69
|
+
o = {
|
70
|
+
code: response.code,
|
71
|
+
body: response.body,
|
72
|
+
description: http_reason(code: response.code),
|
73
|
+
content: {
|
74
|
+
type: nil,
|
75
|
+
length: 0
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
response.headers.each do |key, value|
|
80
|
+
case key
|
81
|
+
when 'content-type'
|
82
|
+
o[:content][:type] = value
|
83
|
+
when 'content-length'
|
84
|
+
o[:content][:length] = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
o
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/sp/job/pg_connection.rb
CHANGED
@@ -31,6 +31,7 @@ module SP
|
|
31
31
|
# Public Attributes
|
32
32
|
#
|
33
33
|
attr_accessor :connection
|
34
|
+
attr_reader :config
|
34
35
|
|
35
36
|
#
|
36
37
|
# Prepare database connection configuration.
|
@@ -58,6 +59,7 @@ module SP
|
|
58
59
|
@treshold = new_min.to_i
|
59
60
|
end
|
60
61
|
end
|
62
|
+
@transaction_open = false
|
61
63
|
end
|
62
64
|
|
63
65
|
#
|
@@ -87,7 +89,7 @@ module SP
|
|
87
89
|
# @param args all the args for the query
|
88
90
|
# @return query result.
|
89
91
|
#
|
90
|
-
def
|
92
|
+
def execp (query, *args)
|
91
93
|
@mutex.synchronize {
|
92
94
|
if nil == @connection
|
93
95
|
_connect()
|
@@ -95,7 +97,24 @@ module SP
|
|
95
97
|
_check_life_span()
|
96
98
|
unless @id_cache.has_key? query
|
97
99
|
id = "p#{Digest::MD5.hexdigest(query)}"
|
98
|
-
|
100
|
+
begin
|
101
|
+
@connection.prepare(id, query)
|
102
|
+
rescue PG::DuplicatePstatement => ds
|
103
|
+
tmp_debug_str = ""
|
104
|
+
@id_cache.each do | k, v |
|
105
|
+
if v == id || k == query
|
106
|
+
tmp_debug_str += "#{v}: #{k}\n"
|
107
|
+
break
|
108
|
+
end
|
109
|
+
end
|
110
|
+
if 0 == tmp_debug_str.length
|
111
|
+
tmp_debug_str = "~~~\nAll Entries:\n" + @id_cache.to_s
|
112
|
+
else
|
113
|
+
tmp_debug_str = "~~~\nCached Entry:\n#{tmp_debug_str}"
|
114
|
+
end
|
115
|
+
tmp_debug_str += "~~~\nNew Entry: #{id}:#{query}\n"
|
116
|
+
raise "#{ds.message}\n#{tmp_debug_str}"
|
117
|
+
end
|
99
118
|
@id_cache[query] = id
|
100
119
|
else
|
101
120
|
id = @id_cache[query]
|
@@ -105,14 +124,21 @@ module SP
|
|
105
124
|
end
|
106
125
|
|
107
126
|
#
|
108
|
-
# Execute a
|
127
|
+
# Execute a normal SQL statement.
|
109
128
|
#
|
110
|
-
# @param query
|
129
|
+
# @param query the SQL query with data binding
|
130
|
+
# @param args all the args for the query
|
131
|
+
# @return query result.
|
111
132
|
#
|
112
|
-
def
|
133
|
+
def exec (query, *args)
|
113
134
|
@mutex.synchronize {
|
114
|
-
|
115
|
-
|
135
|
+
if nil == @connection
|
136
|
+
_connect()
|
137
|
+
end
|
138
|
+
_check_life_span()
|
139
|
+
if args.length > 0
|
140
|
+
@connection.exec(sprintf(query, *args))
|
141
|
+
else
|
116
142
|
@connection.exec(query)
|
117
143
|
end
|
118
144
|
}
|
@@ -134,20 +160,71 @@ module SP
|
|
134
160
|
@config[:conn_str]
|
135
161
|
end
|
136
162
|
|
163
|
+
#
|
164
|
+
# Call this to open a transaction
|
165
|
+
#
|
166
|
+
def begin
|
167
|
+
@mutex.synchronize {
|
168
|
+
if nil == @connection
|
169
|
+
_connect()
|
170
|
+
end
|
171
|
+
_check_life_span()
|
172
|
+
r = @connection.exec("BEGIN;")
|
173
|
+
if PG::PGRES_COMMAND_OK != r.result_status
|
174
|
+
raise "Unable to BEGIN a new transaction!"
|
175
|
+
end
|
176
|
+
@transaction_open = true
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Call this to commit the currently open transaction
|
182
|
+
#
|
183
|
+
def commit
|
184
|
+
@mutex.synchronize {
|
185
|
+
if nil != @connection && true == @transaction_open
|
186
|
+
r = @connection.exec("COMMIT;")
|
187
|
+
if PG::PGRES_COMMAND_OK != r.result_status
|
188
|
+
raise "Unable to COMMIT a transaction!"
|
189
|
+
end
|
190
|
+
@transaction_open = false
|
191
|
+
end
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Call this to open a transaction
|
197
|
+
#
|
198
|
+
def rollback
|
199
|
+
@mutex.synchronize {
|
200
|
+
if nil != @connection && true == @transaction_open
|
201
|
+
r = @connection.exec("ROLLBACK;")
|
202
|
+
if PG::PGRES_COMMAND_OK != r.result_status
|
203
|
+
raise "Unable to ROLLBACK a transaction!"
|
204
|
+
end
|
205
|
+
@transaction_open = false
|
206
|
+
end
|
207
|
+
}
|
208
|
+
|
209
|
+
end
|
210
|
+
|
137
211
|
private
|
138
212
|
|
139
|
-
def _connect ()
|
213
|
+
def _connect ()
|
140
214
|
_disconnect()
|
141
215
|
@connection = PG.connect(@config[:conn_str])
|
142
216
|
end
|
143
217
|
|
144
218
|
def _disconnect ()
|
219
|
+
@transaction_open = false
|
145
220
|
if @connection.nil?
|
146
221
|
return
|
147
222
|
end
|
148
223
|
|
149
|
-
@
|
150
|
-
|
224
|
+
if @id_cache.size
|
225
|
+
@connection.exec("DEALLOCATE ALL")
|
226
|
+
@id_cache = {}
|
227
|
+
end
|
151
228
|
|
152
229
|
@connection.close
|
153
230
|
@connection = nil
|
@@ -158,6 +235,9 @@ module SP
|
|
158
235
|
# Check connection life span
|
159
236
|
#
|
160
237
|
def _check_life_span ()
|
238
|
+
if true == @transaction_open
|
239
|
+
return
|
240
|
+
end
|
161
241
|
return unless @treshold > 0
|
162
242
|
@counter += 1
|
163
243
|
if @counter > @treshold
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2011-2016 Cloudware S.A. All rights reserved.
|
3
|
+
#
|
4
|
+
# This file is part of sp-job.
|
5
|
+
#
|
6
|
+
# sp-job is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# sp-job is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with sp-job. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
# encoding: utf-8
|
20
|
+
#
|
21
|
+
|
22
|
+
require 'cgi'
|
23
|
+
|
24
|
+
module SP
|
25
|
+
module Job
|
26
|
+
module QueryParams
|
27
|
+
|
28
|
+
def self.encode(value, key = nil)
|
29
|
+
case value
|
30
|
+
when Hash then value.map { |k,v| encode(v, append_key(key,k)) }.join('&')
|
31
|
+
when Array then value.map { |v| encode(v, "#{key}[]") }.join('&')
|
32
|
+
when nil then ''
|
33
|
+
else
|
34
|
+
"#{key}=#{CGI.escape(value.to_s)}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self.append_key(root_key, key)
|
41
|
+
root_key.nil? ? key : "#{root_key}[#{key.to_s}]"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module RFC822
|
2
|
+
module Patterns
|
3
|
+
ATOM = "[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\u00ff]+"
|
4
|
+
QTEXT = "[^\\x0d\\x22\\x5c\\u0080-\\u00ff]"
|
5
|
+
QPAIR = "\\x5c[\\x00-\\x7f]"
|
6
|
+
QSTRING = "\\x22(?:#{QTEXT}|#{QPAIR})*\\x22"
|
7
|
+
WORD = "(?:#{ATOM}|#{QSTRING})"
|
8
|
+
LOCAL_PT = "#{WORD}(?:\\x2e#{WORD})*"
|
9
|
+
ADDRESS = "#{LOCAL_PT}\\x40(?:#{URI::REGEXP::PATTERN::HOSTNAME})?#{ATOM}"
|
10
|
+
end
|
11
|
+
|
12
|
+
EMAIL = /\A#{Patterns::ADDRESS}\z/
|
13
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2017-2018 Cloudware S.A. All rights reserved.
|
3
|
+
#
|
4
|
+
# This file is part of sp-job.
|
5
|
+
#
|
6
|
+
# sp-job is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# sp-job is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with sp-job. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
# encoding: utf-8
|
20
|
+
#
|
21
|
+
|
22
|
+
#
|
23
|
+
# Helper class to create simplifed OAUTH sessions suitable for jobs and casper applications, for the full blown OAUTH scene check casper-nginx-broker
|
24
|
+
#
|
25
|
+
|
26
|
+
module SP
|
27
|
+
module Job
|
28
|
+
|
29
|
+
class Session
|
30
|
+
|
31
|
+
attr_reader :access_ttl
|
32
|
+
attr_reader :refresh_ttl
|
33
|
+
|
34
|
+
def initialize (configuration:, serviceId:, multithread: false, programName:, redis:)
|
35
|
+
@sid = serviceId
|
36
|
+
@access_ttl = configuration[:oauth2][:access_ttl] || (1 * 3600) # Duration of the access tokens
|
37
|
+
@refresh_ttl = configuration[:oauth2][:refresh_ttl] || (2 * 3600) # Duration of the refresh tokens
|
38
|
+
@tolerance_ttl = configuration[:oauth2][:deleted_ttl] || 30 # Time a deleted token will remain "alive"
|
39
|
+
@redis = redis
|
40
|
+
@session_base = {
|
41
|
+
patched_by: programName,
|
42
|
+
client_id: configuration[:oauth2][:client_id],
|
43
|
+
redirect_uri: configuration[:oauth2][:redirect_uri],
|
44
|
+
scope: configuration[:oauth2][:scope],
|
45
|
+
issuer: programName
|
46
|
+
}
|
47
|
+
|
48
|
+
if multithread
|
49
|
+
raise 'Multithreading is not supported in MRI/CRuby' unless RUBY_ENGINE == 'jruby'
|
50
|
+
@redis_mutex = Mutex.new
|
51
|
+
else
|
52
|
+
@redis_mutex = nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Thread safe redis driver, pass a block to execute a generic redis operation
|
58
|
+
#
|
59
|
+
def redis
|
60
|
+
# callback is not optional
|
61
|
+
if @redis_mutex.nil?
|
62
|
+
yield(@redis)
|
63
|
+
else
|
64
|
+
# ... to enforce safe usage!
|
65
|
+
@redis_mutex.synchronize {
|
66
|
+
yield(@redis)
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Create a brand new access token with optional refresh token
|
73
|
+
#
|
74
|
+
# @param patch symbolicated hash with session data
|
75
|
+
# @param with_refresh when true the refresh token is also created, when false just access
|
76
|
+
# @return access_token or access_token and refresh token
|
77
|
+
#
|
78
|
+
def create (patch:, with_refresh: false)
|
79
|
+
session = patch.merge(@session_base)
|
80
|
+
session[:created_at] = Time.new.iso8601
|
81
|
+
if with_refresh
|
82
|
+
refresh_token = create_token(session: session, refresh_token: true)
|
83
|
+
session[:refresh_token] = refresh_token
|
84
|
+
else
|
85
|
+
session.delete(:refresh_token)
|
86
|
+
refresh_token = nil
|
87
|
+
end
|
88
|
+
access_token = create_token(session: session)
|
89
|
+
return access_token, refresh_token
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Create an access token by clonning a refresh token
|
94
|
+
#
|
95
|
+
# @param refresh_token the id of the refresh token
|
96
|
+
# @param session symbolicated session data
|
97
|
+
#
|
98
|
+
def create_from_refresh (refresh_token:, session:)
|
99
|
+
session[:created_at] = Time.new.iso8601
|
100
|
+
session[:refresh_token] = refresh_token
|
101
|
+
return create_token(session: session)
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Retrieve session hash from redis, keys are symbolicated
|
106
|
+
#
|
107
|
+
# @param token The access or refresh token
|
108
|
+
# @param refresh true for refresh, false for access_token
|
109
|
+
#
|
110
|
+
def get (token:, refresh: false)
|
111
|
+
key = "#{@sid}:oauth:#{refresh ? 'refresh_token' : 'access_token'}:#{token}"
|
112
|
+
session = nil
|
113
|
+
redis do |r|
|
114
|
+
session = r.hgetall(key)
|
115
|
+
end
|
116
|
+
rv = Hash.new
|
117
|
+
session.each do |key,value|
|
118
|
+
rv[key.to_sym] = value
|
119
|
+
end
|
120
|
+
return rv
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Create a token pair session by merging an existing access_token with the given patch
|
125
|
+
#
|
126
|
+
# @param token The access token to retrieve the original session hash
|
127
|
+
# @param patch a symbolicated hash that will overide existing keys and/or add new ones
|
128
|
+
#
|
129
|
+
# @note Use null values on the patch to delete keys from the original session
|
130
|
+
#
|
131
|
+
def patch (token:, patch:)
|
132
|
+
session = get(token: token)
|
133
|
+
refresh_token = session[:refresh_token]
|
134
|
+
patch.each do |key, value|
|
135
|
+
if value.nil?
|
136
|
+
session.delete(key)
|
137
|
+
else
|
138
|
+
session[key] = value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
at, rt = create(patch: session, with_refresh: refresh_token != nil)
|
142
|
+
dispose(token: token, refresh_token: refresh_token)
|
143
|
+
return at,rt
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Cross patch, creates a new token on the current cluster by patching a source token from another cluster
|
148
|
+
#
|
149
|
+
# @param source session handler from which the original token is read
|
150
|
+
# @param token id of the original token on the source cluster
|
151
|
+
# @param patch symbolicated hash that is fused into source cluster
|
152
|
+
# @return fresh pait of access_token and refresh_token
|
153
|
+
#
|
154
|
+
def x_patch (source:, token:, patch:)
|
155
|
+
session = source.get(token: token)
|
156
|
+
refresh_token = session[:refresh_token]
|
157
|
+
patch.each do |key, value|
|
158
|
+
if value.nil?
|
159
|
+
session.delete(key)
|
160
|
+
else
|
161
|
+
session[key] = value
|
162
|
+
end
|
163
|
+
end
|
164
|
+
at, rt = create(patch: session, with_refresh: refresh_token != nil)
|
165
|
+
source.dispose(token: token, refresh_token: refresh_token)
|
166
|
+
return at,rt
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Delete tokens, immediately or after a grace period.
|
171
|
+
#
|
172
|
+
# @param token access token to dispose
|
173
|
+
# @param refresh_token (optional) refresh token to dispose
|
174
|
+
# @param timeleft grace period to keep the token alive, 0 to dispose immediately
|
175
|
+
#
|
176
|
+
# @note if the refresh token is not supplied attempts to retrive it from the access token
|
177
|
+
#
|
178
|
+
def dispose (token:, refresh_token: nil, timeleft: nil)
|
179
|
+
timeleft ||= @tolerance_ttl
|
180
|
+
key = "#{@sid}:oauth:access_token:#{token}"
|
181
|
+
redis do |r|
|
182
|
+
if refresh_token.to_s.size == 0
|
183
|
+
refresh_token = r.hget(key, 'refresh_token')
|
184
|
+
end
|
185
|
+
if refresh_token.to_s.size != 0
|
186
|
+
rkey = "#{@sid}:oauth:refresh_token:#{refresh_token}"
|
187
|
+
r.expire(rkey, timeleft)
|
188
|
+
end
|
189
|
+
r.expire(key, timeleft)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# Extend the life of a token by timetolive seconds
|
195
|
+
#
|
196
|
+
# @param token the token to preserve
|
197
|
+
# @param refresh true it's a refresh token, false for access
|
198
|
+
# @param timetolive new duration in seconds
|
199
|
+
#
|
200
|
+
def extend (token:, refresh: false, timetolive:)
|
201
|
+
key = "#{@sid}:oauth:#{refresh ? 'refresh_token' : 'access_token'}:#{token}"
|
202
|
+
rv = 0
|
203
|
+
redis do |r|
|
204
|
+
rv = r.expire(key, timetolive)
|
205
|
+
end
|
206
|
+
return rv
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
|
211
|
+
def create_token (session:, refresh_token: false)
|
212
|
+
token = nil
|
213
|
+
3.times do
|
214
|
+
token = SecureRandom.hex(32)
|
215
|
+
key = "#{@sid}:oauth:#{refresh_token ? 'refresh_token' : 'access_token'}:#{token}"
|
216
|
+
hset = []
|
217
|
+
session.each do |key, value|
|
218
|
+
unless value.nil?
|
219
|
+
hset << key
|
220
|
+
hset << value.to_s
|
221
|
+
end
|
222
|
+
end
|
223
|
+
redis do |r|
|
224
|
+
unless r.exists(key)
|
225
|
+
r.pipelined do
|
226
|
+
r.hmset(key, hset)
|
227
|
+
r.expire(key, refresh_token ? @refresh_ttl : @access_ttl)
|
228
|
+
end
|
229
|
+
return token
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
return nil
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|