kiosk-server 0.5.20 → 0.6.28
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 +4 -4
- data/CHANGELOG.md +196 -0
- data/README.md +192 -138
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +21 -65
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +24 -7
- data/app/controllers/kiosk/server/jwks_controller.rb +2 -2
- data/app/controllers/kiosk/server/kyc_attestation_controller.rb +1 -4
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +17 -34
- data/app/controllers/kiosk/server/oauth_registration_controller.rb +34 -0
- data/app/controllers/kiosk/server/oauth_token_controller.rb +36 -32
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +20 -0
- data/app/models/kiosk/settlement.rb +1 -3
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -69
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -15
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +19 -25
- data/lib/kiosk/server/account_binding.rb +32 -62
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +18 -16
- data/lib/kiosk/server/agent_rotation.rb +62 -0
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/assistant_key.rb +86 -0
- data/lib/kiosk/server/auth_toll.rb +6 -6
- data/lib/kiosk/server/binding_module_gate.rb +1 -1
- data/lib/kiosk/server/checkout.rb +57 -0
- data/lib/kiosk/server/client.rb +61 -0
- data/lib/kiosk/server/client_assertion.rb +154 -0
- data/lib/kiosk/server/client_registration.rb +98 -0
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +64 -75
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +38 -69
- data/lib/kiosk/server/dpop.rb +126 -0
- data/lib/kiosk/server/eddsa.rb +38 -0
- data/lib/kiosk/server/engine.rb +16 -13
- data/lib/kiosk/server/errors.rb +28 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +38 -10
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +13 -12
- data/lib/kiosk/server/executor/spending_cap.rb +17 -76
- data/lib/kiosk/server/executor.rb +86 -105
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +12 -3
- data/lib/kiosk/server/jwk.rb +83 -0
- data/lib/kiosk/server/jwks.rb +6 -0
- data/lib/kiosk/server/jwt_issuer.rb +7 -13
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +28 -29
- data/lib/kiosk/server/link_code.rb +9 -18
- data/lib/kiosk/server/linked_assistants.rb +22 -17
- data/lib/kiosk/server/mandate_verifier.rb +97 -139
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +31 -0
- data/lib/kiosk/server/oauth_error.rb +33 -0
- data/lib/kiosk/server/open_api.rb +31 -29
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +47 -78
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +30 -0
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +3 -3
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +9 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +369 -58
- data/lib/kiosk/server/schemas/binding.schema.json +24 -59
- data/lib/kiosk/server/schemas/kyc.schema.json +40 -78
- data/lib/kiosk/server/schemas/mandates.schema.json +123 -39
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/sd_jwt.rb +104 -0
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/signing_key.rb +18 -52
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +4 -4
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +272 -205
- data/lib/kiosk/server/wire_rendering.rb +2 -2
- data/lib/kiosk/server.rb +8 -2
- data/lib/kiosk/skill_link.rb +6 -2
- data/listen.py +38 -8
- metadata +34 -24
- data/app/models/kiosk/cart_mandate.rb +0 -21
- data/lib/generators/kiosk/install/templates/create_kiosk_auth_challenges.rb.tt +0 -16
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/agent_login.rb +0 -49
- data/lib/kiosk/server/agent_registration.rb +0 -88
- data/lib/kiosk/server/auth_challenge.rb +0 -33
- data/lib/kiosk/server/auth_challenge_stores.rb +0 -74
- data/lib/kiosk/server/pop_verifier.rb +0 -87
- data/lib/kiosk/server/schema_definitions.rb +0 -351
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
data/lib/kiosk/server/jwks.rb
CHANGED
|
@@ -9,6 +9,12 @@ module Kiosk
|
|
|
9
9
|
def build(keys:)
|
|
10
10
|
{ keys: Array(keys).map(&:to_jwk) }
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
# The public key of the JWK in `jwks` whose `kid` is `kid`, or nil.
|
|
14
|
+
def find(jwks, kid)
|
|
15
|
+
jwk = Array(jwks[:keys] || jwks['keys']).find { |k| (k[:kid] || k['kid']) == kid }
|
|
16
|
+
jwk && Jwk.import(jwk)
|
|
17
|
+
end
|
|
12
18
|
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
@@ -5,16 +5,12 @@ require 'securerandom'
|
|
|
5
5
|
|
|
6
6
|
module Kiosk
|
|
7
7
|
module Server
|
|
8
|
-
# Issues and verifies the
|
|
8
|
+
# Issues and verifies the EdDSA access tokens of the bundled agent IdP.
|
|
9
9
|
module JwtIssuer
|
|
10
|
-
ALGORITHM =
|
|
10
|
+
ALGORITHM = Eddsa::NAME
|
|
11
11
|
DEFAULT_EXPIRES_IN = 3600
|
|
12
12
|
DEFAULT_LEEWAY = 60
|
|
13
13
|
|
|
14
|
-
# A `kid` not in the JWKS: the signature cannot be checked.
|
|
15
|
-
UNKNOWN_KID = ->(e) { e.message.include?('public key for kid') || e.message.include?('Could not find public key') }
|
|
16
|
-
private_constant :UNKNOWN_KID
|
|
17
|
-
|
|
18
14
|
class Error < StandardError; end
|
|
19
15
|
class SignatureError < Error; end
|
|
20
16
|
class ExpiredError < Error; end
|
|
@@ -33,7 +29,7 @@ module Kiosk
|
|
|
33
29
|
raise ArgumentError, 'signing_key must carry a private key for issuance' unless signing_key.private?
|
|
34
30
|
|
|
35
31
|
payload = registered_claims(claims, issuer: issuer, audience: audience, issued_at: now.to_i, expires_in: expires_in)
|
|
36
|
-
::JWT.encode(payload, signing_key.
|
|
32
|
+
::JWT.encode(payload, signing_key.key, ALGORITHM, { kid: signing_key.kid, typ: 'JWT' })
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
def verify(token:, jwks:, audience: nil, issuer: nil, leeway: DEFAULT_LEEWAY)
|
|
@@ -58,9 +54,9 @@ module Kiosk
|
|
|
58
54
|
def decode(token, jwks, audience:, issuer:, leeway:)
|
|
59
55
|
decoded, = ::JWT.decode(
|
|
60
56
|
token, nil, true,
|
|
61
|
-
|
|
57
|
+
algorithms: [ALGORITHM], aud: audience, iss: issuer,
|
|
62
58
|
verify_aud: !audience.nil?, verify_iss: !issuer.nil?, leeway: leeway
|
|
63
|
-
)
|
|
59
|
+
) { |header| Jwks.find(jwks, header['kid']) || raise(SignatureError, 'no key of the JWKS has this kid') }
|
|
64
60
|
symbolize(decoded)
|
|
65
61
|
rescue ::JWT::DecodeError => e
|
|
66
62
|
raise error_class_for(e), e.message
|
|
@@ -70,7 +66,7 @@ module Kiosk
|
|
|
70
66
|
case error
|
|
71
67
|
when ::JWT::ExpiredSignature then ExpiredError
|
|
72
68
|
when ::JWT::InvalidAudError then AudienceError
|
|
73
|
-
when ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
|
|
69
|
+
when ::JWT::VerificationError, ::JWT::IncorrectAlgorithm then SignatureError
|
|
74
70
|
else InvalidError
|
|
75
71
|
end
|
|
76
72
|
end
|
|
@@ -88,9 +84,7 @@ module Kiosk
|
|
|
88
84
|
end
|
|
89
85
|
end
|
|
90
86
|
|
|
91
|
-
def symbolize(hash)
|
|
92
|
-
hash.each_with_object({}) { |(k, v), out| out[k.to_sym] = v }
|
|
93
|
-
end
|
|
87
|
+
def symbolize(hash) = hash.deep_symbolize_keys
|
|
94
88
|
|
|
95
89
|
private_class_method :registered_claims, :decode, :error_class_for
|
|
96
90
|
end
|
data/lib/kiosk/server/kyc.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Kiosk
|
|
|
49
49
|
def claims = Array(Kiosk.configuration.kyc_claims).map(&:to_s)
|
|
50
50
|
|
|
51
51
|
def served!
|
|
52
|
-
return if provider
|
|
52
|
+
return if provider&.opens_verifications?
|
|
53
53
|
|
|
54
54
|
raise Errors::ModuleNotServed.new(
|
|
55
55
|
'this operator does not serve the KYC module',
|
|
@@ -115,8 +115,8 @@ module Kiosk
|
|
|
115
115
|
|
|
116
116
|
# Names only a path this origin serves.
|
|
117
117
|
def gate_hint
|
|
118
|
-
return REQUEST_HINT if provider
|
|
119
|
-
return ATTEST_HINT if
|
|
118
|
+
return REQUEST_HINT if provider&.opens_verifications?
|
|
119
|
+
return ATTEST_HINT if provider
|
|
120
120
|
|
|
121
121
|
CLOSED_HINT
|
|
122
122
|
end
|
|
@@ -148,9 +148,7 @@ module Kiosk
|
|
|
148
148
|
private
|
|
149
149
|
|
|
150
150
|
def open_verification(user_id)
|
|
151
|
-
Kyc.provider.open_verification(subject: user_id, claims: Kyc.claims,
|
|
152
|
-
audience: Kiosk.configuration.kyc_audience.to_s,
|
|
153
|
-
callback_url: Kyc.callback_url)
|
|
151
|
+
Kyc.provider.open_verification(subject: user_id, claims: Kyc.claims, callback_url: Kyc.callback_url)
|
|
154
152
|
rescue Kiosk::KycProviders::Unavailable => e
|
|
155
153
|
FailureLog.report("#{NAME} could not open a verification", e)
|
|
156
154
|
raise Errors::WireError.new(
|
|
@@ -249,7 +247,7 @@ module Kiosk
|
|
|
249
247
|
JSON.generate(attributes), user_id.to_s)
|
|
250
248
|
end
|
|
251
249
|
|
|
252
|
-
def table(name) = connection.quote_table_name("#{Kiosk.configuration.schema}
|
|
250
|
+
def table(name) = connection.quote_table_name("#{Kiosk.configuration.schema}.kiosk_#{name}")
|
|
253
251
|
|
|
254
252
|
def execute(sql, *binds) = connection.exec_query(sql, 'Kiosk KYC', binds).to_a
|
|
255
253
|
|
|
@@ -4,14 +4,14 @@ require 'jwt'
|
|
|
4
4
|
|
|
5
5
|
module Kiosk
|
|
6
6
|
module Server
|
|
7
|
-
# Verifies a KYC attestation JWS against `
|
|
8
|
-
# `sub` and `level: "verified"`
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# Verifies a KYC attestation JWS against the configured `kyc_provider`: its
|
|
8
|
+
# key signs it, its `iss` and `aud` match, `sub` and `level: "verified"`
|
|
9
|
+
# match, and the provider accepts it; only attributes that are literally
|
|
10
|
+
# `true` are granted.
|
|
11
11
|
module KycVerifier
|
|
12
12
|
# Named once for the decode and the published hint.
|
|
13
13
|
REQUIRED_CLAIMS = %w[exp iss aud sub].freeze
|
|
14
|
-
SIGNATURE_HINT = 'an attestation is a compact
|
|
14
|
+
SIGNATURE_HINT = 'an attestation is a compact EdDSA JWS signed by the issuer this origin ' \
|
|
15
15
|
'is configured to trust'
|
|
16
16
|
SUBJECT_HINT = "sub must name the principal: the caller's at POST <endpoint>/agents/kyc, " \
|
|
17
17
|
"the open verification's at the callback"
|
|
@@ -19,17 +19,17 @@ module Kiosk
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
21
|
def verify(raw_jws:, subject:)
|
|
22
|
-
|
|
23
|
-
payload
|
|
24
|
-
check_issuer!(payload,
|
|
22
|
+
provider = provider!
|
|
23
|
+
payload = decode(raw_jws, provider)
|
|
24
|
+
check_issuer!(payload, provider)
|
|
25
25
|
# Binds the attestation to this operator.
|
|
26
|
-
if payload[:aud].to_s !=
|
|
26
|
+
if payload[:aud].to_s != provider.audience
|
|
27
27
|
raise Errors::Forbidden.new('KYC attestation audience mismatch',
|
|
28
|
-
hint: audience_hint(payload,
|
|
28
|
+
hint: audience_hint(payload, provider))
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
check_subject!(payload, subject)
|
|
32
|
-
check_provider!(payload,
|
|
32
|
+
check_provider!(payload, provider)
|
|
33
33
|
|
|
34
34
|
payload.merge(attributes: verified_attributes(payload[:attributes]))
|
|
35
35
|
end
|
|
@@ -47,9 +47,9 @@ module Kiosk
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# The verified payload with symbol keys.
|
|
50
|
-
def decode(raw_jws,
|
|
51
|
-
payload, = ::JWT.decode(raw_jws, public_key
|
|
52
|
-
algorithms: [
|
|
50
|
+
def decode(raw_jws, provider)
|
|
51
|
+
payload, = ::JWT.decode(raw_jws, provider.public_key, true,
|
|
52
|
+
algorithms: [Eddsa::NAME], verify_expiration: true, required_claims: REQUIRED_CLAIMS)
|
|
53
53
|
payload.transform_keys(&:to_sym)
|
|
54
54
|
rescue ::JWT::ExpiredSignature
|
|
55
55
|
raise Errors::Forbidden, 'KYC attestation expired'
|
|
@@ -59,30 +59,30 @@ module Kiosk
|
|
|
59
59
|
raise Errors::Forbidden.new('KYC attestation signature invalid', hint: SIGNATURE_HINT)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# Refused when this origin trusts no KYC
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
return
|
|
62
|
+
# Refused when this origin trusts no KYC provider.
|
|
63
|
+
def provider!
|
|
64
|
+
provider = Kiosk.configuration.kyc_provider
|
|
65
|
+
return provider unless provider.nil?
|
|
66
66
|
|
|
67
67
|
raise Errors::ModuleNotServed.new(
|
|
68
68
|
'this operator does not serve the KYC module',
|
|
69
69
|
hint: 'no KYC attestation is accepted at this origin; retrying will not help. ' \
|
|
70
|
-
'An operator that means to serve KYC sets Kiosk.configuration.
|
|
70
|
+
'An operator that means to serve KYC sets Kiosk.configuration.kyc_provider.'
|
|
71
71
|
)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def check_issuer!(payload,
|
|
75
|
-
return if payload[:iss] ==
|
|
74
|
+
def check_issuer!(payload, provider)
|
|
75
|
+
return if payload[:iss] == provider.issuer
|
|
76
76
|
|
|
77
77
|
raise Errors::Forbidden.new(
|
|
78
78
|
'KYC attestation issuer mismatch',
|
|
79
|
-
hint: "expected #{
|
|
79
|
+
hint: "expected #{provider.issuer.inspect}, got #{payload[:iss].inspect}"
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def audience_hint(payload,
|
|
84
|
-
"aud must equal this operator's
|
|
85
|
-
"(expected #{
|
|
83
|
+
def audience_hint(payload, provider)
|
|
84
|
+
"aud must equal the audience this operator's KYC provider mints " \
|
|
85
|
+
"(expected #{provider.audience.inspect}, got #{payload[:aud].inspect})"
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
# Subject and level. A bigint-PK host's subject is an Integer.
|
|
@@ -93,14 +93,13 @@ module Kiosk
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# The provider's own checks ({Kiosk::KycProviders::Base#accepts?}).
|
|
96
|
-
def check_provider!(payload,
|
|
97
|
-
|
|
98
|
-
return if provider.nil? || provider.accepts?(payload.transform_keys(&:to_s))
|
|
96
|
+
def check_provider!(payload, provider)
|
|
97
|
+
return if provider.accepts?(payload.transform_keys(&:to_s))
|
|
99
98
|
|
|
100
99
|
raise Errors::Forbidden, 'the KYC provider does not accept this attestation at this operator'
|
|
101
100
|
end
|
|
102
101
|
|
|
103
|
-
private_class_method :decode, :
|
|
102
|
+
private_class_method :decode, :provider!, :check_issuer!, :audience_hint, :check_subject!, :check_provider!
|
|
104
103
|
end
|
|
105
104
|
end
|
|
106
105
|
end
|
|
@@ -4,7 +4,7 @@ module Kiosk
|
|
|
4
4
|
module Server
|
|
5
5
|
# Account binding started by the human: the signed-in holder mints a
|
|
6
6
|
# single-use link code ({.mint}, POST /auth/link) and their assistant
|
|
7
|
-
# redeems it with
|
|
7
|
+
# redeems it with a client assertion and the auth toll ({.redeem}, POST /auth/claim).
|
|
8
8
|
module LinkCode
|
|
9
9
|
CLIENT_ID = 'kiosk-link'
|
|
10
10
|
|
|
@@ -31,37 +31,28 @@ module Kiosk
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# The toll comes before the lookup, so an unknown code costs the same work as a live one.
|
|
34
|
-
|
|
34
|
+
#
|
|
35
|
+
# @return [Client] the client under the account of the human who minted the code
|
|
36
|
+
# @raise [ClientAssertion::Refused] the client assertion is refused
|
|
37
|
+
def redeem(code:, assertion_type:, assertion:, pow: nil, # rubocop:disable Metrics/ParameterLists -- the claim controller and the specs pass these by name
|
|
35
38
|
store: Kiosk.configuration.device_authorization_store,
|
|
36
39
|
now: Time.current)
|
|
37
40
|
raise Errors::BadRequest, 'code required' if code.to_s.empty?
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
PopVerifier.load_public_key(pem)
|
|
41
|
-
AuthToll.gate(verb: 'auth/claim', body: { code: code.to_s, public_key: pem }, pow: pow)
|
|
42
|
+
AuthToll.gate(verb: 'auth/claim', body: { code: code.to_s }, pow: pow)
|
|
42
43
|
da = live_link(store, code.to_s, now)
|
|
43
44
|
|
|
44
|
-
# The
|
|
45
|
-
|
|
45
|
+
# The client authenticates before the consume, so a refused assertion leaves the code live.
|
|
46
|
+
client = ClientAssertion.authenticate!(assertion_type, assertion)
|
|
46
47
|
# The atomic consume, not `consumed?` below, decides single use under a race.
|
|
47
48
|
raise Errors::Conflict, 'link code already used' if store.claim_consume(da, now: now).nil?
|
|
48
49
|
|
|
49
|
-
bind(
|
|
50
|
+
AccountBinding.bind!(agent_id: client.id, user_id: da.user_id, requested_role: da.requested_role)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
class << self
|
|
53
54
|
private
|
|
54
55
|
|
|
55
|
-
def prove_possession!(pem, signed)
|
|
56
|
-
payload = PopVerifier.verify!(public_key_pem: pem, signed: signed)
|
|
57
|
-
AuthChallenge.consume!(public_key_pem: pem, nonce: payload.fetch(:nonce))
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def bind(pem, link)
|
|
61
|
-
result = AccountBinding.bind!(public_key_pem: pem, user_id: link.user_id, requested_role: link.requested_role)
|
|
62
|
-
{ agent_id: result[:agent_id], user_id: result[:user_id], access_token: result[:access_token] }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
56
|
def live_link(store, code, now)
|
|
66
57
|
da = store.lookup_by_device_code_hash(DeviceAuthorization.hash_device_code(code))
|
|
67
58
|
unless da&.link?
|
|
@@ -34,7 +34,7 @@ module Kiosk
|
|
|
34
34
|
|
|
35
35
|
def settings_sql(assignments, bind_count)
|
|
36
36
|
<<~SQL.squish
|
|
37
|
-
UPDATE #{Kiosk.configuration.schema}.
|
|
37
|
+
UPDATE #{Kiosk.configuration.schema}.kiosk_agents
|
|
38
38
|
SET #{assignments.join(', ')}
|
|
39
39
|
WHERE id = $#{bind_count - 2}
|
|
40
40
|
AND user_id = $#{bind_count - 1}
|
|
@@ -47,18 +47,16 @@ module Kiosk
|
|
|
47
47
|
|
|
48
48
|
def agents_sql
|
|
49
49
|
<<~SQL.squish
|
|
50
|
-
SELECT id, public_key,
|
|
51
|
-
FROM #{Kiosk.configuration.schema}.
|
|
50
|
+
SELECT id, public_key, bound_at, human_label, spending_cap_cents, spending_cap_currency
|
|
51
|
+
FROM #{Kiosk.configuration.schema}.kiosk_agents
|
|
52
52
|
WHERE user_id = $1 AND issuer = $2 AND revoked_at IS NULL
|
|
53
53
|
ORDER BY created_at
|
|
54
54
|
SQL
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# `{ agent_id => { currency => cents } }`:
|
|
58
|
-
# counts, within `spending_cap_window_days` when set.
|
|
57
|
+
# `{ agent_id => { currency => cents } }`: what each agent spent.
|
|
59
58
|
def spend_by_agent(user_id)
|
|
60
|
-
|
|
61
|
-
tally(run(spend_sql(window_days), 'Kiosk assistant spend', [user_id, Kiosk.current_issuer, *window_days]))
|
|
59
|
+
tally(run(spend_sql, 'Kiosk assistant spend', [user_id, Kiosk.current_issuer]))
|
|
62
60
|
rescue ::ActiveRecord::StatementInvalid => e
|
|
63
61
|
raise unless missing_table?(e)
|
|
64
62
|
|
|
@@ -71,20 +69,27 @@ module Kiosk
|
|
|
71
69
|
end
|
|
72
70
|
end
|
|
73
71
|
|
|
74
|
-
#
|
|
75
|
-
def spend_sql
|
|
72
|
+
# One tally per currency.
|
|
73
|
+
def spend_sql
|
|
76
74
|
schema = Kiosk.configuration.schema
|
|
77
|
-
window = window_days ? 'AND settlements.settled_at >= now() - make_interval(days => $3)' : ''
|
|
78
75
|
<<~SQL.squish
|
|
79
|
-
SELECT settlements.agent_id, lower(btrim(settlements.currency)) AS currency,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
WHERE agents.user_id = $1 AND agents.issuer = $2 AND agents.revoked_at IS NULL #{window}
|
|
76
|
+
SELECT settlements.agent_id, lower(btrim(settlements.currency)) AS currency, SUM(#{net_cents_sql(schema)}) AS cents
|
|
77
|
+
FROM #{schema}.kiosk_settlements settlements
|
|
78
|
+
JOIN #{schema}.kiosk_agents agents ON agents.id = settlements.agent_id
|
|
79
|
+
WHERE agents.user_id = $1 AND agents.issuer = $2 AND agents.revoked_at IS NULL
|
|
84
80
|
GROUP BY 1, 2 ORDER BY 2
|
|
85
81
|
SQL
|
|
86
82
|
end
|
|
87
83
|
|
|
84
|
+
# Each settlement less what was refunded of it, never below zero.
|
|
85
|
+
def net_cents_sql(schema)
|
|
86
|
+
<<~SQL.squish
|
|
87
|
+
GREATEST(settlements.settled_amount_cents - (
|
|
88
|
+
SELECT COALESCE(SUM(r.refunded_amount_cents), 0) FROM #{schema}.kiosk_refunds r WHERE r.settlement_id = settlements.id
|
|
89
|
+
), 0)
|
|
90
|
+
SQL
|
|
91
|
+
end
|
|
92
|
+
|
|
88
93
|
# By class name, so this file does not load `PG`.
|
|
89
94
|
def missing_table?(error)
|
|
90
95
|
error.cause&.class&.name == 'PG::UndefinedTable'
|
|
@@ -95,7 +100,7 @@ module Kiosk
|
|
|
95
100
|
{
|
|
96
101
|
agent_id: row.fetch('id'),
|
|
97
102
|
fingerprint: fingerprint(row.fetch('public_key')),
|
|
98
|
-
|
|
103
|
+
bound_at: row.fetch('bound_at', nil),
|
|
99
104
|
human_label: row.fetch('human_label', nil),
|
|
100
105
|
spending_cap_cents: cap&.to_i,
|
|
101
106
|
spending_cap_currency: row.fetch('spending_cap_currency', nil),
|
|
@@ -106,7 +111,7 @@ module Kiosk
|
|
|
106
111
|
def fingerprint(pem)
|
|
107
112
|
return '(no key)' if pem.blank?
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
AssistantKey.thumbprint(AssistantKey.from_pem(pem))
|
|
110
115
|
rescue StandardError
|
|
111
116
|
'(unreadable key)'
|
|
112
117
|
end
|