Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions lib/privy/public_api/services/policies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ def create(policy_create_params:, idempotency_key: nil, request_options: nil)
# @option policy_update_params [String, nil] :owner_id Key quorum ID to set as owner.
# @option policy_update_params [Array<Hash>, nil] :rules New rules for the policy.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::Policy]
def update(policy_id, policy_update_params:, authorization_context: nil, request_options: nil)
def update(policy_id, policy_update_params:, authorization_context: nil, request_expiry: nil, request_options: nil)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :patch,
url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}"),
body: policy_update_params,
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = policy_update_params.merge(request_options: request_options)
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand All @@ -92,16 +96,20 @@ def update(policy_id, policy_update_params:, authorization_context: nil, request
#
# @param policy_id [String] ID of the policy to delete.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::SuccessResponse]
def delete(policy_id, authorization_context: nil, request_options: nil)
def delete(policy_id, authorization_context: nil, request_expiry: nil, request_options: nil)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :delete,
url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}"),
body: "",
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
# Workaround: the Stainless-generated Ruby client sends Content-Type: application/json on
# every request (lib/privy/internal/transport/base_client.rb:204), even bodyless DELETEs.
Expand Down Expand Up @@ -136,16 +144,20 @@ def delete(policy_id, authorization_context: nil, request_options: nil)
# @option policy_create_rule_params [String] :action Action when the rule matches ("ALLOW" or "DENY", required).
# @option policy_create_rule_params [Array<Hash>] :conditions Array of condition objects (required).
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::PolicyRuleResponse]
def create_rule(policy_id, policy_create_rule_params:, authorization_context: nil, request_options: nil)
def create_rule(policy_id, policy_create_rule_params:, authorization_context: nil, request_expiry: nil, request_options: nil)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :post,
url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules"),
body: policy_create_rule_params,
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = policy_create_rule_params.merge(request_options: request_options)
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand Down Expand Up @@ -176,6 +188,9 @@ def create_rule(policy_id, policy_create_rule_params:, authorization_context: ni
# @option policy_update_rule_params [String] :action Action when the rule matches ("ALLOW" or "DENY", required).
# @option policy_update_rule_params [Array<Hash>] :conditions Array of condition objects (required).
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::PolicyRuleResponse]
Expand All @@ -184,14 +199,16 @@ def update_rule(
policy_id:,
policy_update_rule_params:,
authorization_context: nil,
request_expiry: nil,
request_options: nil
)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :patch,
url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules/#{rule_id}"),
body: policy_update_rule_params,
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = policy_update_rule_params.merge(
policy_id: policy_id,
Expand All @@ -210,16 +227,20 @@ def update_rule(
# @param rule_id [String] ID of the rule to delete.
# @param policy_id [String] ID of the policy the rule belongs to.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::SuccessResponse]
def delete_rule(rule_id, policy_id:, authorization_context: nil, request_options: nil)
def delete_rule(rule_id, policy_id:, authorization_context: nil, request_expiry: nil, request_options: nil)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :delete,
url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules/#{rule_id}"),
body: "",
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
# Workaround: same Content-Type issue as delete — see comment there.
opts = (request_options || {}).merge(extra_headers: {"Content-Type" => nil})
Expand Down
150 changes: 150 additions & 0 deletions test/privy/public_api/services/policies_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# frozen_string_literal: true

require_relative "../../test_helper"

class Privy::Services::PoliciesTest < Minitest::Test
extend Minitest::Serial
include WebMock::API

BASE_URL = "https://api.staging.privy.io"

def before_all
super
WebMock.enable!
end

def after_all
WebMock.disable!
super
end

def teardown
WebMock.reset!
super
end

def build_client(**opts)
Privy::PrivyClient.new(app_id: "app-abc", app_secret: "secret", environment: :staging, **opts)
end

def policy_response_body(**overrides)
{
id: "p-1",
chain_type: "ethereum",
created_at: 0,
name: "test policy",
owner_id: nil,
rules: [],
version: "1.0"
}.merge(overrides).to_json
end

def stub_json(method, url, body:)
stub_request(method, url).to_return(
status: 200,
body: body,
headers: {"content-type" => "application/json"}
)
end

# --- request_expiry on update -----------------------------------------------

def test_update_default_request_expiry_is_auto_set
stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n"))
before = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
build_client.policies.update("p-1", policy_update_params: {name: "n"})
assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req|
sent = req.headers["Privy-Request-Expiry"].to_i
assert_in_delta(before + (15 * 60 * 1_000), sent, 5_000)
end
end

def test_update_per_call_request_expiry_overrides_default
stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n"))
build_client.policies.update(
"p-1",
policy_update_params: {name: "n"},
request_expiry: 1_750_000_000_000
)
assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_update_disabled_options_omit_header
stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n"))
build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true))
.policies.update("p-1", policy_update_params: {name: "n"})
assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req|
refute(req.headers.key?("Privy-Request-Expiry"))
end
end

def test_update_disabled_options_still_honor_explicit_override
stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n"))
build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true))
.policies.update("p-1", policy_update_params: {name: "n"}, request_expiry: 1_750_000_000_000)
assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

# --- request_expiry on delete / create_rule / update_rule / delete_rule ----

def test_delete_per_call_request_expiry_overrides_default
stub_request(:delete, "#{BASE_URL}/v1/policies/p-1").to_return(
status: 200, body: '{"success":true}', headers: {"content-type" => "application/json"}
)
build_client.policies.delete("p-1", request_expiry: 1_750_000_000_000)
assert_requested(:delete, "#{BASE_URL}/v1/policies/p-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_create_rule_per_call_request_expiry_overrides_default
stub_json(
:post,
"#{BASE_URL}/v1/policies/p-1/rules",
body: '{"id":"r-1","name":"n","method":"eth_sendTransaction","action":"ALLOW","conditions":[]}'
)
build_client.policies.create_rule(
"p-1",
policy_create_rule_params: {
name: "n", method: "eth_sendTransaction", action: "ALLOW", conditions: []
},
request_expiry: 1_750_000_000_000
)
assert_requested(:post, "#{BASE_URL}/v1/policies/p-1/rules") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_update_rule_per_call_request_expiry_overrides_default
stub_json(
:patch,
"#{BASE_URL}/v1/policies/p-1/rules/r-1",
body: '{"id":"r-1","name":"n","method":"eth_sendTransaction","action":"ALLOW","conditions":[]}'
)
build_client.policies.update_rule(
"r-1",
policy_id: "p-1",
policy_update_rule_params: {
name: "n", method: "eth_sendTransaction", action: "ALLOW", conditions: []
},
request_expiry: 1_750_000_000_000
)
assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1/rules/r-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_delete_rule_per_call_request_expiry_overrides_default
stub_request(:delete, "#{BASE_URL}/v1/policies/p-1/rules/r-1").to_return(
status: 200, body: '{"success":true}', headers: {"content-type" => "application/json"}
)
build_client.policies.delete_rule("r-1", policy_id: "p-1", request_expiry: 1_750_000_000_000)
assert_requested(:delete, "#{BASE_URL}/v1/policies/p-1/rules/r-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end
end
Loading