etna 0.1.11
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.
- checksums.yaml +7 -0
- data/lib/etna/application.rb +90 -0
- data/lib/etna/auth.rb +128 -0
- data/lib/etna/client.rb +144 -0
- data/lib/etna/command.rb +26 -0
- data/lib/etna/controller.rb +111 -0
- data/lib/etna/cross_origin.rb +71 -0
- data/lib/etna/describe_routes.rb +31 -0
- data/lib/etna/errors.rb +37 -0
- data/lib/etna/ext.rb +13 -0
- data/lib/etna/hmac.rb +86 -0
- data/lib/etna/logger.rb +29 -0
- data/lib/etna/parse_body.rb +41 -0
- data/lib/etna/route.rb +165 -0
- data/lib/etna/server.rb +84 -0
- data/lib/etna/sign_service.rb +67 -0
- data/lib/etna/spec/auth.rb +28 -0
- data/lib/etna/spec.rb +1 -0
- data/lib/etna/symbolize_params.rb +30 -0
- data/lib/etna/test_auth.rb +80 -0
- data/lib/etna/user.rb +79 -0
- data/lib/etna.rb +17 -0
- metadata +119 -0
data/lib/etna/hmac.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Etna
|
2
|
+
class Hmac
|
3
|
+
# These are the items that need to be signed
|
4
|
+
SIGN_ITEMS=[ :method, :host, :path, :expiration, :nonce, :id, :headers ]
|
5
|
+
SIGN_ITEMS.each { |item| attr_reader item }
|
6
|
+
|
7
|
+
def initialize application, params
|
8
|
+
@application = application
|
9
|
+
|
10
|
+
@test_signature = params.delete(:test_signature)
|
11
|
+
|
12
|
+
SIGN_ITEMS.each do |item|
|
13
|
+
raise ArgumentError, "Hmac requires param #{item}" unless params[item]
|
14
|
+
instance_variable_set("@#{item}", params[item])
|
15
|
+
end
|
16
|
+
|
17
|
+
@id = @id.to_sym
|
18
|
+
|
19
|
+
raise ArgumentError, "Headers must be a Hash" unless @headers.is_a?(Hash)
|
20
|
+
end
|
21
|
+
|
22
|
+
# this returns arguments for URI::HTTP.build
|
23
|
+
def url_params
|
24
|
+
params = {
|
25
|
+
signature: signature,
|
26
|
+
expiration: @expiration,
|
27
|
+
nonce: @nonce,
|
28
|
+
id: @id.to_s,
|
29
|
+
headers: @headers.keys.join(','),
|
30
|
+
}.merge(@headers).map do |name, value|
|
31
|
+
[
|
32
|
+
"X-Etna-#{ name.to_s.split(/_/).map(&:capitalize).join('-') }",
|
33
|
+
value
|
34
|
+
]
|
35
|
+
end.to_h
|
36
|
+
|
37
|
+
return {
|
38
|
+
host: @host,
|
39
|
+
path: @path,
|
40
|
+
query: URI.encode_www_form(params)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
valid_id? && valid_signature? && valid_timestamp?
|
46
|
+
end
|
47
|
+
|
48
|
+
def signature
|
49
|
+
@application.sign.hmac(text_to_sign, @application.config(:hmac_keys)[@id])
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def valid_signature?
|
55
|
+
signature == @test_signature
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_timestamp?
|
59
|
+
DateTime.parse(@expiration) >= DateTime.now
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid_id?
|
63
|
+
@application.config(:hmac_keys).key?(@id)
|
64
|
+
end
|
65
|
+
|
66
|
+
# This scheme is adapted from the Hawk spec
|
67
|
+
# (github:hueniverse/hawk) and the Acquia HTTP
|
68
|
+
# Hmac spec (github:acquia/http-hmac-spec)
|
69
|
+
|
70
|
+
|
71
|
+
def text_to_sign
|
72
|
+
[
|
73
|
+
# these come from the route
|
74
|
+
@method,
|
75
|
+
@host,
|
76
|
+
@path,
|
77
|
+
|
78
|
+
# these are set as headers or params
|
79
|
+
@nonce,
|
80
|
+
@id,
|
81
|
+
@headers.map{|l| l.join('=')}.join(';'),
|
82
|
+
@expiration,
|
83
|
+
].join("\n")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/etna/logger.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Etna
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(log_dev, age, size)
|
6
|
+
super
|
7
|
+
|
8
|
+
self.formatter = proc do |severity, datetime, progname, msg|
|
9
|
+
format(severity, datetime, progname, msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(severity, datetime, progname, msg)
|
14
|
+
"#{severity}:#{datetime.iso8601} #{msg}\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_error(e)
|
18
|
+
error(e.message)
|
19
|
+
e.backtrace.each do |trace|
|
20
|
+
error(trace)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_request(request)
|
25
|
+
request.env['etna.logger'] = self
|
26
|
+
request.env['etna.request_id'] = (rand*36**6).to_i.to_s(36)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Etna
|
2
|
+
class ParseBody
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
params = env['rack.request.params'] || {}
|
9
|
+
|
10
|
+
case env['CONTENT_TYPE']
|
11
|
+
when %r{application/json}i
|
12
|
+
body = env['rack.input'].read
|
13
|
+
if body =~ %r/^\s*\{/
|
14
|
+
params.update(
|
15
|
+
JSON.parse(body)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
when %r{application/x-www-form-urlencoded}i
|
19
|
+
params.update(
|
20
|
+
Rack::Utils.parse_nested_query(
|
21
|
+
env['rack.input'].read
|
22
|
+
)
|
23
|
+
)
|
24
|
+
when %r{multipart/form-data}i
|
25
|
+
params.update(
|
26
|
+
Rack::Multipart.parse_multipart(env)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
# Always parse the params that are url-encoded.
|
30
|
+
params.update(
|
31
|
+
Rack::Utils.parse_nested_query(
|
32
|
+
env['QUERY_STRING'], '&'
|
33
|
+
)
|
34
|
+
)
|
35
|
+
env.update(
|
36
|
+
'rack.request.params' => params
|
37
|
+
)
|
38
|
+
@app.call(env)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/etna/route.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
module Etna
|
2
|
+
class Route
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(method, route, options, &block)
|
6
|
+
@method = method
|
7
|
+
@action = options[:action]
|
8
|
+
@auth = options[:auth]
|
9
|
+
@name = route_name(options)
|
10
|
+
@route = route.gsub(/\A(?=[^\/])/, '/')
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
{
|
16
|
+
method: @method,
|
17
|
+
route: @route,
|
18
|
+
name: @name.to_s,
|
19
|
+
params: parts
|
20
|
+
}.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def matches?(request)
|
24
|
+
@method == request.request_method && request.path.match(route_regexp)
|
25
|
+
end
|
26
|
+
|
27
|
+
NAMED_PARAM=/:([\w]+)/
|
28
|
+
GLOB_PARAM=/\*([\w]+)$/
|
29
|
+
|
30
|
+
PARAM_TYPES=[ NAMED_PARAM, GLOB_PARAM ]
|
31
|
+
|
32
|
+
UNSAFE=/[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,]/
|
33
|
+
|
34
|
+
def self.path(route, params=nil)
|
35
|
+
if params
|
36
|
+
PARAM_TYPES.reduce(route) do |path,pat|
|
37
|
+
path.gsub(pat) do
|
38
|
+
URI.encode( params[$1.to_sym], UNSAFE)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
route
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def path(params=nil)
|
47
|
+
self.class.path(@route, params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def parts
|
51
|
+
part_list = PARAM_TYPES.map do |pat|
|
52
|
+
"(?:#{pat.source})"
|
53
|
+
end
|
54
|
+
@route.scan(
|
55
|
+
/(?:#{part_list.join('|')})/
|
56
|
+
).flatten.compact
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(app, request)
|
60
|
+
update_params(request)
|
61
|
+
|
62
|
+
unless authorized?(request)
|
63
|
+
return [ 403, { 'Content-Type' => 'application/json' }, [ { error: 'You are forbidden from performing this action.' }.to_json ] ]
|
64
|
+
end
|
65
|
+
|
66
|
+
if @action
|
67
|
+
controller, action = @action.split('#')
|
68
|
+
controller_class = Kernel.const_get(
|
69
|
+
:"#{controller.camel_case}Controller"
|
70
|
+
)
|
71
|
+
logger = request.env['etna.logger']
|
72
|
+
user = request.env['etna.user']
|
73
|
+
|
74
|
+
params = request.env['rack.request.params'].map do |key,value|
|
75
|
+
value = value.to_s
|
76
|
+
value = value[0..500] + "..." + value[-100..-1] if value.length > 600
|
77
|
+
[ key, value ]
|
78
|
+
end.to_h
|
79
|
+
|
80
|
+
logger.warn("User #{user ? user.email : :unknown} calling #{controller}##{action} with params #{params}")
|
81
|
+
return controller_class.new(request, action).response
|
82
|
+
elsif @block
|
83
|
+
application = Etna::Application.find(app.class).class
|
84
|
+
controller_class = application.const_defined?(:Controller) ? application.const_get(:Controller) : Etna::Controller
|
85
|
+
|
86
|
+
controller_class.new(request).response(&@block)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# the route does not require authorization
|
91
|
+
def noauth?
|
92
|
+
@auth && @auth[:noauth]
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def authorized?(request)
|
98
|
+
# If there is no @auth requirement, they are ok - this doesn't preclude
|
99
|
+
# them being rejected in the controller response
|
100
|
+
!@auth || @auth[:noauth] || (user_authorized?(request) && hmac_authorized?(request))
|
101
|
+
end
|
102
|
+
|
103
|
+
def user_authorized?(request)
|
104
|
+
# this is true if there are no user requirements
|
105
|
+
return true unless @auth[:user]
|
106
|
+
|
107
|
+
user = request.env['etna.user']
|
108
|
+
|
109
|
+
# if there is a user requirement, we must have a user
|
110
|
+
return false unless user
|
111
|
+
|
112
|
+
params = request.env['rack.request.params']
|
113
|
+
|
114
|
+
@auth[:user].all? do |constraint, param_name|
|
115
|
+
user.respond_to?(constraint) && user.send(constraint, params[param_name])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def hmac_authorized?(request)
|
120
|
+
# either there is no hmac requirement, or we have a valid hmac
|
121
|
+
!@auth[:hmac] || request.env['etna.hmac'].valid?
|
122
|
+
end
|
123
|
+
|
124
|
+
def route_name(options)
|
125
|
+
# use the given one if you can
|
126
|
+
return options[:as] if options[:as]
|
127
|
+
|
128
|
+
# otherwise formulate it from the action if possible
|
129
|
+
return options[:action].sub(/#/,'_').to_sym if options[:action]
|
130
|
+
|
131
|
+
# unnamed route
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def update_params(request)
|
136
|
+
match = route_regexp.match(request.path)
|
137
|
+
request.env['rack.request.params'].update(
|
138
|
+
Hash[
|
139
|
+
match.names.map(&:to_sym).zip(
|
140
|
+
match.captures.map do |capture|
|
141
|
+
URI.decode(capture)
|
142
|
+
end
|
143
|
+
)
|
144
|
+
]
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def route_regexp
|
149
|
+
@route_regexp ||=
|
150
|
+
Regexp.new(
|
151
|
+
'\A' +
|
152
|
+
@route.
|
153
|
+
# any :params match separator-free strings
|
154
|
+
gsub(NAMED_PARAM, '(?<\1>[^\.\/\?]+)').
|
155
|
+
# any *params match arbitrary strings
|
156
|
+
gsub(GLOB_PARAM, '(?<\1>.+)').
|
157
|
+
# ignore any trailing slashes in the route
|
158
|
+
gsub(/\/\z/, '') +
|
159
|
+
# trailing slashes in the path can be ignored
|
160
|
+
'/?' +
|
161
|
+
'\z'
|
162
|
+
)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/etna/server.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# This class handles the http request and routing.
|
2
|
+
module Etna
|
3
|
+
class Server
|
4
|
+
class << self
|
5
|
+
def route(method, path, options={}, &block)
|
6
|
+
@routes ||= []
|
7
|
+
|
8
|
+
@routes << Etna::Route.new(
|
9
|
+
method,
|
10
|
+
path,
|
11
|
+
(@default_options || {}).merge(options),
|
12
|
+
&block
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_route(request)
|
17
|
+
@routes.find do |route|
|
18
|
+
route.matches? request
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def with(options={}, &block)
|
23
|
+
@default_options = options
|
24
|
+
instance_eval(&block)
|
25
|
+
@default_options = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def get(path, options={}, &block)
|
29
|
+
route('GET', path, options, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def post(path, options={}, &block)
|
33
|
+
route('POST', path, options, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def put(path, options={}, &block)
|
37
|
+
route('PUT', path, options, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(path, options={}, &block)
|
41
|
+
route('DELETE', path, options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def route_path(request,name,params={})
|
45
|
+
route = routes.find do |route|
|
46
|
+
route.name.to_s == name.to_s
|
47
|
+
end
|
48
|
+
return route ? route.path(params) : nil
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :routes
|
52
|
+
end
|
53
|
+
|
54
|
+
def call(env)
|
55
|
+
request = Rack::Request.new(env)
|
56
|
+
|
57
|
+
request.env['etna.server'] = self
|
58
|
+
|
59
|
+
application.logger.log_request(request)
|
60
|
+
|
61
|
+
route = self.class.find_route(request)
|
62
|
+
|
63
|
+
if route
|
64
|
+
@params = request.env['rack.request.params']
|
65
|
+
return route.call(self, request)
|
66
|
+
end
|
67
|
+
|
68
|
+
[404, {}, ["There is no such path '#{request.path}'"]]
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
# Setup logging.
|
73
|
+
application.setup_logger
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# The base application class is a singleton independent of this rack server,
|
79
|
+
# holding e.g. configuration.
|
80
|
+
def application
|
81
|
+
@application ||= Etna::Application.instance
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# General signing/hashing utilities.
|
2
|
+
require 'jwt'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Etna
|
6
|
+
class SignService
|
7
|
+
def initialize(application)
|
8
|
+
@application = application
|
9
|
+
end
|
10
|
+
|
11
|
+
def hash_password(password)
|
12
|
+
signature(
|
13
|
+
[ password, @application.config(:pass_salt) ],
|
14
|
+
@application.config(:pass_algo)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def hmac(message, key)
|
19
|
+
OpenSSL::HMAC.hexdigest(
|
20
|
+
'SHA256',
|
21
|
+
key,
|
22
|
+
message
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def jwt_token(payload)
|
27
|
+
return JWT.encode(
|
28
|
+
payload,
|
29
|
+
private_key,
|
30
|
+
@application.config(:token_algo)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def uid(size=nil)
|
35
|
+
SecureRandom.hex(size)
|
36
|
+
end
|
37
|
+
|
38
|
+
def jwt_decode(token)
|
39
|
+
return JWT.decode(
|
40
|
+
token,
|
41
|
+
public_key,
|
42
|
+
true,
|
43
|
+
algorithm: @application.config(:token_algo)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def private_key
|
48
|
+
@private_key ||= OpenSSL::PKey::RSA.new(@application.config(:rsa_private))
|
49
|
+
end
|
50
|
+
|
51
|
+
def public_key
|
52
|
+
@public_key ||= OpenSSL::PKey::RSA.new(@application.config(:rsa_public))
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_private_key(key_size)
|
56
|
+
OpenSSL::PKey::RSA.generate(key_size)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def signature(params, algo)
|
62
|
+
algo = algo.upcase.to_sym
|
63
|
+
raise "Unknown signature algorithm!" unless [ :MD5, :SHA256 ].include?(algo)
|
64
|
+
Digest.const_get(algo).hexdigest(params.join)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Etna::Spec
|
2
|
+
module Auth
|
3
|
+
AUTH_USERS = {
|
4
|
+
superuser: {
|
5
|
+
email: 'zeus@olympus.org', first: 'Zeus', perm: 'A:administration'
|
6
|
+
},
|
7
|
+
admin: {
|
8
|
+
email: 'hera@olympus.org', first: 'Hera', perm: 'a:labors'
|
9
|
+
},
|
10
|
+
editor: {
|
11
|
+
email: 'eurystheus@twelve-labors.org', first: 'Eurystheus', perm: 'E:labors'
|
12
|
+
},
|
13
|
+
restricted_editor: {
|
14
|
+
email: 'copreus@twelve-labors.org', first: 'Copreus', perm: 'e:labors'
|
15
|
+
},
|
16
|
+
viewer: {
|
17
|
+
email: 'hercules@twelve-labors.org', first: 'Hercules', perm: 'v:labors'
|
18
|
+
},
|
19
|
+
non_user: {
|
20
|
+
email: 'nessus@centaurs.org', first: 'Nessus', perm: ''
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
def auth_header(user_type)
|
25
|
+
header(*Etna::TestAuth.token_header(AUTH_USERS[user_type]))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/etna/spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'spec/auth'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Etna
|
2
|
+
class SymbolizeParams
|
3
|
+
def initialize(server)
|
4
|
+
@server = server
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
env['rack.request.params'] = symbolize(env['rack.request.params']) || {}
|
9
|
+
|
10
|
+
@server.call(env)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def symbolize(obj)
|
16
|
+
if obj.is_a?(Hash)
|
17
|
+
return obj.reduce({}) do |memo,(k,v)|
|
18
|
+
memo[k.to_sym] = symbolize(v)
|
19
|
+
memo
|
20
|
+
end
|
21
|
+
elsif obj.is_a?(Array)
|
22
|
+
return obj.reduce([]) do |memo,v|
|
23
|
+
memo << symbolize(v)
|
24
|
+
memo
|
25
|
+
end
|
26
|
+
end
|
27
|
+
obj
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative 'user'
|
2
|
+
|
3
|
+
# This is an authentication layer you can use in testing. It will make an
|
4
|
+
# Etna::User as usual that your controller can respond to; you can pass
|
5
|
+
# permissions for this user directly in the Authorization header
|
6
|
+
module Etna
|
7
|
+
class TestAuth < Auth
|
8
|
+
def self.token_header(params)
|
9
|
+
token = Base64.strict_encode64(params.to_json)
|
10
|
+
return [ 'Authorization', "Etna #{token}" ]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.token_param(params)
|
14
|
+
token = Base64.strict_encode64(params.to_json)
|
15
|
+
return [ Etna::Auth.etna_url_param(:authorization).to_s, "Etna #{token}" ]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.hmac_header(signature)
|
19
|
+
return [ Etna::Auth.etna_url_param(:signature).to_s, signature ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.hmac_params(params)
|
23
|
+
return {
|
24
|
+
expiration: params.delete(:expiration) || DateTime.now.iso8601,
|
25
|
+
id: params.delete(:id) || 'etna',
|
26
|
+
nonce: 'nonce',
|
27
|
+
signature: params.delete(:signature) || 'invalid',
|
28
|
+
headers: params.keys.join(',')
|
29
|
+
}.merge(params).map do |item, value|
|
30
|
+
[ Etna::Auth.etna_url_param(item).to_s, value ]
|
31
|
+
end.to_h
|
32
|
+
end
|
33
|
+
|
34
|
+
def approve_user(request)
|
35
|
+
token = auth(request,:etna)
|
36
|
+
|
37
|
+
return false unless token
|
38
|
+
|
39
|
+
# here we simply base64-encode our user hash and pass it through
|
40
|
+
payload = JSON.parse(Base64.decode64(token))
|
41
|
+
|
42
|
+
request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
|
43
|
+
end
|
44
|
+
|
45
|
+
def approve_hmac(request)
|
46
|
+
hmac_signature = etna_param(request, :signature) || 'invalid'
|
47
|
+
|
48
|
+
headers = (etna_param(request, :headers)&.split(/,/) || []).map do |header|
|
49
|
+
[ header.to_sym, etna_param(request, header) ]
|
50
|
+
end.to_h
|
51
|
+
|
52
|
+
hmac_params = {
|
53
|
+
method: request.request_method,
|
54
|
+
host: request.host,
|
55
|
+
path: request.path,
|
56
|
+
|
57
|
+
expiration: etna_param(request, :expiration) || DateTime.now.iso8601,
|
58
|
+
id: etna_param(request, :id) || 'etna',
|
59
|
+
nonce: etna_param(request, :nonce) || 'nonce',
|
60
|
+
headers: headers,
|
61
|
+
test_signature: hmac_signature
|
62
|
+
}
|
63
|
+
|
64
|
+
hmac = Etna::TestHmac.new(application, hmac_params)
|
65
|
+
|
66
|
+
request.env['etna.hmac'] = hmac
|
67
|
+
|
68
|
+
return nil unless hmac.valid?
|
69
|
+
|
70
|
+
params(request).update(headers)
|
71
|
+
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
class TestHmac < Hmac
|
76
|
+
def valid?
|
77
|
+
@test_signature == 'valid'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/etna/user.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
module Etna
|
2
|
+
class User
|
3
|
+
ROLE_NAMES = {
|
4
|
+
'A' => :admin,
|
5
|
+
'E' => :editor,
|
6
|
+
'V' => :viewer
|
7
|
+
}
|
8
|
+
|
9
|
+
def initialize params, token=nil
|
10
|
+
@first, @last, @email, @encoded_permissions = params.values_at(:first, :last, :email, :perm)
|
11
|
+
@token = token unless !token
|
12
|
+
raise ArgumentError, "No email given!" unless @email
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :first, :last, :email, :token
|
16
|
+
|
17
|
+
def permissions
|
18
|
+
@permissions ||= @encoded_permissions.split(/\;/).map do |roles|
|
19
|
+
role, projects = roles.split(/:/)
|
20
|
+
|
21
|
+
projects.split(/\,/).reduce([]) do |perms,project_name|
|
22
|
+
perms.push([
|
23
|
+
project_name,
|
24
|
+
{
|
25
|
+
role: ROLE_NAMES[role.upcase],
|
26
|
+
restricted: role == role.upcase
|
27
|
+
}
|
28
|
+
])
|
29
|
+
end
|
30
|
+
end.inject([],:+).to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
"#{first} #{last}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def projects
|
38
|
+
permissions.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
ROLE_MATCH = {
|
42
|
+
admin: /[Aa]/,
|
43
|
+
editor: /[Ee]/,
|
44
|
+
viewer: /[Vv]/,
|
45
|
+
restricted: /[AEV]/,
|
46
|
+
}
|
47
|
+
def has_roles(project, *roles)
|
48
|
+
perm = permissions[project.to_s]
|
49
|
+
|
50
|
+
return false unless perm
|
51
|
+
|
52
|
+
return roles.map(&:to_sym).include?(perm[:role])
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_superuser? project=nil
|
56
|
+
has_roles(:administration, :admin)
|
57
|
+
end
|
58
|
+
|
59
|
+
def can_edit? project
|
60
|
+
is_superuser? || has_roles(project, :admin, :editor)
|
61
|
+
end
|
62
|
+
|
63
|
+
def can_view? project
|
64
|
+
is_superuser? || has_roles(project, :admin, :editor, :viewer)
|
65
|
+
end
|
66
|
+
|
67
|
+
# superusers - administrators of the Administration group - cannot
|
68
|
+
# automatically see restricted data, they should be granted
|
69
|
+
# project-specific access.
|
70
|
+
def can_see_restricted? project
|
71
|
+
perm = permissions[project.to_s]
|
72
|
+
perm && perm[:restricted]
|
73
|
+
end
|
74
|
+
|
75
|
+
def is_admin? project
|
76
|
+
is_superuser? || has_roles(project, :admin)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|