Skip to content

Commit

Permalink
Support :until and :seconds option in hmac_paths plugin, for paths va…
Browse files Browse the repository at this point in the history
…lid only until a specific time
  • Loading branch information
jeremyevans committed May 21, 2024
1 parent b9a91aa commit 8296c32
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
= master

* Support :until and :seconds option in hmac_paths plugin, for paths valid only until a specific time (jeremyevans)

= 3.80.0 (2024-05-10)

* Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans)
Expand Down
42 changes: 41 additions & 1 deletion lib/roda/plugins/hmac_paths.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ module RodaPlugins
# this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
# instead of +r.params+ to specifically check query string parameters.
#
# The generated paths can be timestamped, so that they are only valid until a given time
# or for a given number of seconds after they are generated, using the :until or :seconds
# options:
#
# hmac_path('/widget/1', until: Time.utc(2100))
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
#
# hmac_path('/widget/1', seconds: Time.utc(2100).to_i - Time.now.to_i)
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
#
# The :namespace option, if provided, should be a string, and it modifies the generated HMACs
# to only match those in the same namespace. This can be used to provide different paths to
# different users or groups of users.
Expand Down Expand Up @@ -190,6 +200,11 @@ module RodaPlugins
# r.hmac_path('/1', params: {k: 2})
# HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
#
# The +:until+ and +:seconds+ option include the timestamp in the HMAC:
#
# r.hmac_path('/1', until: Time.utc(2100))
# HMAC_hex(HMAC_hex(secret, ''), '/t/4102444800/1')
#
# If a +:namespace+ option is provided, the original secret used before the +:root+ option is
# an HMAC of the +:secret+ plugin option and the given namespace.
#
Expand Down Expand Up @@ -232,6 +247,8 @@ module InstanceMethods
# the already matched path of the routing tree using r.hmac_path. Defaults
# to the empty string, which will returns paths valid for r.hmac_path at
# the top level of the routing tree.
# :seconds :: Make the given path valid for the given integer number of seconds.
# :until :: Make the given path valid until the given Time.
def hmac_path(path, opts=OPTS)
unless path.is_a?(String) && path.getbyte(0) == 47
raise RodaError, "path must be a string starting with /"
Expand All @@ -242,6 +259,12 @@ def hmac_path(path, opts=OPTS)
raise RodaError, "root must be empty string or string starting with /"
end

if valid_until = opts[:until]
valid_until = valid_until.to_i
elsif seconds = opts[:seconds]
valid_until = Time.now.to_i + seconds
end

flags = String.new
path = path.dup

Expand All @@ -258,6 +281,11 @@ def hmac_path(path, opts=OPTS)
flags << 'n'
end

if valid_until
flags << 't'
path = "/#{valid_until}#{path}"
end

flags << '0' if flags.empty?

hmac_path = if method
Expand Down Expand Up @@ -335,7 +363,19 @@ def hmac_path(opts=OPTS, &block)
end

if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
always(&block)
if flags.include?('t')
on Integer do |int|
if int >= Time.now.to_i
always(&block)
else
# Return from method without matching
@remaining_path = orig_path
return
end
end
else
always(&block)
end
end
end

Expand Down
60 changes: 59 additions & 1 deletion spec/plugin/hmac_paths_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def hmac_paths_app(&block)
body.must_equal "/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c/p/1?foo=bar"
end

it "hmac_path HMAC depends on :query option" do
it "hmac_path HMAC depends on :params option" do
hmac_paths_app do |r|
r.get('a'){hmac_path('/1', params: {bar: :foo})}
hmac_path('/1', params: {foo: :bar})
Expand All @@ -103,6 +103,39 @@ def hmac_paths_app(&block)
body('/a').must_equal "/1785c857e23dfd04c127162d292f557cd2db4b0a6ac9c39515c85ce9ff404165/p/1?bar=foo"
end

it "hmac_path sets t flag for :seconds and :until options" do
t = Time.utc(2100).to_i
seconds = t - Time.now.to_i
hmac_paths_app do |r|
r.get('s'){hmac_path('/1', seconds: seconds)}
hmac_path('/1', until: 3)
end
body.must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1"
body('/s').must_equal "/64e31560c6df065b6116599c370aca918cfcbde092724b94f2770192ae513a28/t/#{t}/1"
end

it "hmac_path HMAC depends on :seconds and :until options" do
t = Time.utc(2100).to_i
seconds = t - Time.now.to_i
hmac_paths_app do |r|
r.on(Integer) do |v|
r.get('s'){hmac_path('/1', seconds: seconds - v)}
hmac_path('/1', until: v)
end
end
body('/3').must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1"
body('/3/s').must_equal "/9ffb7ce6b6ae76664aeebc53955c7c1d8d09e6908a512a6b5f22e7a24fffdc15/t/#{t-3}/1"
body('/4').must_equal "/f04e6d0571e2f3322bfa03b4b251070b7cdbde1004726fd69ded8cb40d1fb4ae/t/4/1"
body('/4/s').must_equal "/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c86/t/#{t-4}/1"
end

it "hmac_path gives priority to :until option over seconds option" do
hmac_paths_app do |r|
hmac_path('/1', until: 3, seconds: 2)
end
body.must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1"
end

it "hmac_path HMAC depends on :namespace option" do
hmac_paths_app do |r|
r.is String, String do |ns, path|
Expand Down Expand Up @@ -349,6 +382,21 @@ def hmac_paths_app(&block)
status(p2, 'QUERY_STRING'=>qs1).must_equal 404
end

it "r.hmac_path yields if the path is timestamped, hmac matches, and before the timestamp" do
hmac_paths_app{|r| r.hmac_path{r.remaining_path}}
body('/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c86/t/4102444796/1').must_equal '/1'
end

it "r.hmac_path does not yield if the path is timestamped, hmac matches, and not before the timestamp" do
hmac_paths_app{|r| r.hmac_path{r.remaining_path}}
status("/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1").must_equal 404
end

it "r.hmac_path does not yield if the path is timestamped and hmac does not match" do
hmac_paths_app{|r| r.hmac_path{r.remaining_path}}
status('/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c85/t/4102444796/1').must_equal 404
end

it "r.hmac_path yields if there is a namespace provided and required and it matches" do
hmac_paths_app{|r| r.hmac_path(namespace: r.GET['ns']){r.remaining_path}}
body('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1', 'QUERY_STRING'=>'ns=1').must_equal '/1'
Expand Down Expand Up @@ -591,6 +639,14 @@ def hmac_paths_app(&block)
hmac_path(r.remaining_path, params: {k=>v})
end

r.on 'until' do
hmac_path(r.remaining_path, until: Time.utc(2100))
end

r.on 'seconds' do
hmac_path(r.remaining_path, seconds: Time.utc(2100).to_i - Time.now.to_i)
end

r.on 'namespace', String do |ns|
hmac_path(r.remaining_path, namespace: ns)
end
Expand All @@ -608,6 +664,8 @@ def hmac_paths_app(&block)
body('/root/foobar/1').must_equal "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1"
body('/method/get/widget/1').must_equal "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1"
body('/params/foo/bar/widget/1').must_equal "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar"
body('/until/widget/1').must_equal "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
body('/seconds/widget/1').must_equal "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
body('/namespace/1/widget/1').must_equal "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
body('/namespace/2/widget/1').must_equal "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
body('/all/widget/get/foo/bar/1/1').must_equal "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar"
Expand Down

0 comments on commit 8296c32

Please sign in to comment.