stitches 5.1.0.RC3 → 5.1.0.RC5
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/README.md +47 -43
- data/lib/stitches/calling_service_client.rb +15 -0
- data/lib/stitches/calling_service_middleware.rb +28 -0
- data/lib/stitches/calling_service_name.rb +1 -3
- data/lib/stitches/railtie.rb +2 -0
- data/lib/stitches/version.rb +1 -1
- data/spec/calling_service_middleware_spec.rb +72 -0
- data/spec/calling_service_name_spec.rb +9 -41
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f589f4d3f2537a4f9b17328ad88ca1b89031bd80309eddc63657b2799fb7e0e9
|
|
4
|
+
data.tar.gz: 1c51e01a86ee6ad3fa70f7e7e69b9a700bfc2ea87040f8b9c059e52648a70632
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5e2b42fa7a6609e8dc8e6bec1cf863e4c2e0e7d5bc47ae581187ce7ae2b15c672ec407fb20d07fe3829807e0ec976cbb7b56eeb47d47303352d3807ffd99b40
|
|
7
|
+
data.tar.gz: 766a73badd49a7aee8c3b16635230082dc0fb7e39cba5e20ef0e7acaaab42641874dd2348b233adcfa7f15aee883bef42965f82d3a7d46187f04799a1e632939
|
data/README.md
CHANGED
|
@@ -42,73 +42,77 @@ Stitches.configure do |config|
|
|
|
42
42
|
end
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
### Caller Identification
|
|
45
|
+
### Caller Identification
|
|
46
46
|
|
|
47
47
|
When API key auth is disabled, services lose the ability to identify which
|
|
48
|
-
internal service is calling them.
|
|
49
|
-
|
|
48
|
+
internal service is calling them. Stitches 5.1+ provides two mechanisms that
|
|
49
|
+
work together to restore this — one automatic, one opt-in.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
The calling service header name defaults to `X-StitchFix-Calling-Service` but
|
|
52
|
+
is configurable via `Stitches.configuration.calling_service_header`.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
class Api::ApiController < ActionController::API
|
|
55
|
-
include Stitches::CallingServiceName
|
|
54
|
+
#### CallingServiceMiddleware (automatic)
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
`CallingServiceMiddleware` is registered via the railtie and runs after the
|
|
57
|
+
`ApiKey` middleware. When no auth middleware has populated the caller identity
|
|
58
|
+
env var (`Stitches.configuration.env_var_to_hold_api_client`), it reads the
|
|
59
|
+
calling service header and populates it with a `CallingServiceClient` struct.
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
**This means existing code that reads the caller identity object's `.name`
|
|
62
|
+
continues to work with no changes** — the value now comes from the header
|
|
63
|
+
instead of a DB lookup.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```
|
|
65
|
+
`CallingServiceClient` implements:
|
|
66
|
+
- `.name` — the header value (e.g. "my-app")
|
|
67
|
+
- `.id` — nil
|
|
68
|
+
- `.key` — nil
|
|
70
69
|
|
|
71
|
-
**Resolution order:**
|
|
70
|
+
**Resolution order (for `request.env[env_var_to_hold_api_client]`):**
|
|
72
71
|
|
|
73
|
-
1. `
|
|
74
|
-
2.
|
|
75
|
-
3. `
|
|
72
|
+
1. If the `ApiKey` middleware authenticated a key → `ApiClient` record (has `.name`, `.id`, `.key`)
|
|
73
|
+
2. If JWT or other auth middleware set the env var → that object (e.g. a user)
|
|
74
|
+
3. If neither ran, but `X-StitchFix-Calling-Service` header is present → `CallingServiceClient` struct
|
|
75
|
+
4. If nothing → nil
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
stitches middleware still populates `api_client`, so the fallback returns the
|
|
79
|
-
authenticated name. When true, callers identify themselves via the header.
|
|
77
|
+
**Middleware ordering:** `ApiKey` → `CallingServiceMiddleware` → `ValidMimeType`
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
#### CallingServiceName concern (opt-in)
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
For cases where you want strictly the header value (e.g. metrics tags where
|
|
82
|
+
you never want a human user's name), include the concern:
|
|
85
83
|
|
|
86
84
|
```ruby
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
class Api::ApiController < ActionController::API
|
|
86
|
+
include Stitches::CallingServiceName
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns the header value only, empty string if absent:
|
|
90
|
+
calling_service_name # => "my-app" or ""
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
#### Configuration
|
|
94
|
+
|
|
95
|
+
The header name is configurable (defaults to `X-StitchFix-Calling-Service`):
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Stitches.configure do |config|
|
|
99
|
+
config.calling_service_header = "X-My-Custom-Header"
|
|
100
|
+
end
|
|
101
|
+
```
|
|
95
102
|
|
|
96
103
|
#### Security considerations
|
|
97
104
|
|
|
98
105
|
`X-StitchFix-Calling-Service` is a **self-declared, unsigned header**. Any
|
|
99
106
|
caller that can reach your service can set it to any value. This means:
|
|
100
107
|
|
|
101
|
-
- **Do not use
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
can reach the endpoint.
|
|
108
|
+
- **Do not use it as a sole authorization mechanism** for sensitive operations
|
|
109
|
+
unless the network layer guarantees that only the legitimate service can
|
|
110
|
+
reach the endpoint.
|
|
105
111
|
- **Strip this header at public ingress points** (Traefik, ALB, API gateway)
|
|
106
112
|
to prevent external callers from spoofing internal service identities.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
This header provides identification only. If you need to verify the caller's
|
|
111
|
-
identity cryptographically, use mTLS or a service mesh AuthorizationPolicy.
|
|
113
|
+
- **This header replaces API key-based identity, not authorization.** It
|
|
114
|
+
provides identification only. If you need to verify the caller's identity
|
|
115
|
+
cryptographically, use mTLS or a service mesh AuthorizationPolicy.
|
|
112
116
|
- **Safe uses:** stats tagging, logging, `updated_by` audit fields, routing
|
|
113
117
|
hints, non-security-critical behavioral branching.
|
|
114
118
|
- **Unsafe without network enforcement:** access control decisions, privilege
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Stitches
|
|
2
|
+
# Lightweight stand-in for ApiClient when the caller is identified by
|
|
3
|
+
# the calling service header rather than an API key lookup. Implements
|
|
4
|
+
# the same interface (.name, .id, .key) so existing code that reads
|
|
5
|
+
# the caller identity object continues to work.
|
|
6
|
+
CallingServiceClient = Struct.new(:name) do
|
|
7
|
+
def id
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def key
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative 'calling_service_client'
|
|
2
|
+
|
|
3
|
+
module Stitches
|
|
4
|
+
# Rack middleware that populates the caller identity env var from the
|
|
5
|
+
# calling service header when no other auth middleware has already set
|
|
6
|
+
# it. This allows existing code that reads the caller identity object
|
|
7
|
+
# to work transparently after API keys are disabled.
|
|
8
|
+
class CallingServiceMiddleware
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
client_key = Stitches.configuration.env_var_to_hold_api_client
|
|
15
|
+
|
|
16
|
+
unless env[client_key]
|
|
17
|
+
header_name = Stitches.configuration.calling_service_header
|
|
18
|
+
rack_key = "HTTP_#{header_name.upcase.tr('-', '_')}"
|
|
19
|
+
|
|
20
|
+
if (name = env[rack_key]).present?
|
|
21
|
+
env[client_key] = CallingServiceClient.new(name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@app.call(env)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,9 +2,7 @@ module Stitches
|
|
|
2
2
|
module CallingServiceName
|
|
3
3
|
def calling_service_name
|
|
4
4
|
@calling_service_name ||=
|
|
5
|
-
request.headers[Stitches.configuration.calling_service_header].presence ||
|
|
6
|
-
request.env[Stitches.configuration.env_var_to_hold_api_client]&.name ||
|
|
7
|
-
""
|
|
5
|
+
request.headers[Stitches.configuration.calling_service_header].presence || ""
|
|
8
6
|
end
|
|
9
7
|
end
|
|
10
8
|
end
|
data/lib/stitches/railtie.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require 'stitches/api_key'
|
|
2
2
|
require 'stitches/valid_mime_type'
|
|
3
3
|
require 'stitches/api_client_access_wrapper'
|
|
4
|
+
require 'stitches/calling_service_middleware'
|
|
4
5
|
|
|
5
6
|
module Stitches
|
|
6
7
|
class Railtie < Rails::Railtie
|
|
7
8
|
config.app_middleware.use Stitches::ApiKey
|
|
9
|
+
config.app_middleware.use Stitches::CallingServiceMiddleware
|
|
8
10
|
config.app_middleware.use Stitches::ValidMimeType
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/stitches/version.rb
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
describe Stitches::CallingServiceMiddleware do
|
|
4
|
+
let(:app) { ->(env) { [200, env, "OK"] } }
|
|
5
|
+
let(:middleware) { described_class.new(app) }
|
|
6
|
+
let(:env) { Rack::MockRequest.env_for("/api/test", method: "POST") }
|
|
7
|
+
|
|
8
|
+
let(:client_key) { Stitches.configuration.env_var_to_hold_api_client }
|
|
9
|
+
|
|
10
|
+
describe "#call" do
|
|
11
|
+
context "when the header is present and env var is not set" do
|
|
12
|
+
before { env["HTTP_X_STITCHFIX_CALLING_SERVICE"] = "my-app" }
|
|
13
|
+
|
|
14
|
+
it "populates the env var with a CallingServiceClient" do
|
|
15
|
+
_status, result_env, _body = middleware.call(env)
|
|
16
|
+
client = result_env[client_key]
|
|
17
|
+
|
|
18
|
+
expect(client).to be_a(Stitches::CallingServiceClient)
|
|
19
|
+
expect(client.name).to eq("my-app")
|
|
20
|
+
expect(client.id).to be_nil
|
|
21
|
+
expect(client.key).to be_nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context "when the env var is already set (API key or JWT auth ran)" do
|
|
26
|
+
let(:existing_client) { double("ApiClient", name: "existing-client", id: 42) }
|
|
27
|
+
|
|
28
|
+
before do
|
|
29
|
+
env[client_key] = existing_client
|
|
30
|
+
env["HTTP_X_STITCHFIX_CALLING_SERVICE"] = "some-service"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "does not overwrite the existing value" do
|
|
34
|
+
_status, result_env, _body = middleware.call(env)
|
|
35
|
+
expect(result_env[client_key]).to eq(existing_client)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "when the header is absent and env var is not set" do
|
|
40
|
+
it "leaves the env var nil" do
|
|
41
|
+
_status, result_env, _body = middleware.call(env)
|
|
42
|
+
expect(result_env[client_key]).to be_nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context "when the header is blank" do
|
|
47
|
+
before { env["HTTP_X_STITCHFIX_CALLING_SERVICE"] = "" }
|
|
48
|
+
|
|
49
|
+
it "leaves the env var nil" do
|
|
50
|
+
_status, result_env, _body = middleware.call(env)
|
|
51
|
+
expect(result_env[client_key]).to be_nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context "with a custom configured header" do
|
|
56
|
+
before do
|
|
57
|
+
Stitches.configuration.calling_service_header = "X-Custom-Caller"
|
|
58
|
+
env["HTTP_X_CUSTOM_CALLER"] = "custom-service"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
after { Stitches.configuration.reset_to_defaults! }
|
|
62
|
+
|
|
63
|
+
it "reads from the configured header" do
|
|
64
|
+
_status, result_env, _body = middleware.call(env)
|
|
65
|
+
client = result_env[client_key]
|
|
66
|
+
|
|
67
|
+
expect(client).to be_a(Stitches::CallingServiceClient)
|
|
68
|
+
expect(client.name).to eq("custom-service")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -2,8 +2,7 @@ require 'rails_helper'
|
|
|
2
2
|
|
|
3
3
|
describe Stitches::CallingServiceName do
|
|
4
4
|
let(:headers) { {} }
|
|
5
|
-
let(:
|
|
6
|
-
let(:fake_request) { double("request", headers: headers, env: env) }
|
|
5
|
+
let(:fake_request) { double("request", headers: headers) }
|
|
7
6
|
let(:fake_controller) {
|
|
8
7
|
req = fake_request
|
|
9
8
|
Object.new.tap { |c|
|
|
@@ -13,53 +12,27 @@ describe Stitches::CallingServiceName do
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
describe "#calling_service_name" do
|
|
16
|
-
context "when
|
|
17
|
-
let(:headers) { {"X-StitchFix-Calling-Service" => "
|
|
15
|
+
context "when the header is present" do
|
|
16
|
+
let(:headers) { {"X-StitchFix-Calling-Service" => "my-app"} }
|
|
18
17
|
|
|
19
18
|
it "returns the header value" do
|
|
20
|
-
expect(fake_controller.calling_service_name).to eq("
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
context "and env var client is also present" do
|
|
24
|
-
let(:env) { {Stitches.configuration.env_var_to_hold_api_client => double(name: "other-service")} }
|
|
25
|
-
|
|
26
|
-
it "prefers the header" do
|
|
27
|
-
expect(fake_controller.calling_service_name).to eq("kingmob")
|
|
28
|
-
end
|
|
19
|
+
expect(fake_controller.calling_service_name).to eq("my-app")
|
|
29
20
|
end
|
|
30
21
|
end
|
|
31
22
|
|
|
32
|
-
context "when header is absent
|
|
33
|
-
let(:env) { {Stitches.configuration.env_var_to_hold_api_client => double(name: "mobile-service")} }
|
|
34
|
-
|
|
35
|
-
it "returns the client name from env" do
|
|
36
|
-
expect(fake_controller.calling_service_name).to eq("mobile-service")
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
context "when header is blank" do
|
|
41
|
-
let(:headers) { {"X-StitchFix-Calling-Service" => ""} }
|
|
42
|
-
let(:env) { {Stitches.configuration.env_var_to_hold_api_client => double(name: "fallback-service")} }
|
|
43
|
-
|
|
44
|
-
it "treats blank as absent and falls through to env var client" do
|
|
45
|
-
expect(fake_controller.calling_service_name).to eq("fallback-service")
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
context "when neither header nor env var client is present" do
|
|
23
|
+
context "when the header is absent" do
|
|
50
24
|
it "returns empty string" do
|
|
51
25
|
expect(fake_controller.calling_service_name).to eq("")
|
|
52
26
|
end
|
|
53
27
|
end
|
|
54
28
|
|
|
55
|
-
context "when
|
|
56
|
-
let(:
|
|
29
|
+
context "when the header is blank" do
|
|
30
|
+
let(:headers) { {"X-StitchFix-Calling-Service" => ""} }
|
|
57
31
|
|
|
58
32
|
it "returns empty string" do
|
|
59
33
|
expect(fake_controller.calling_service_name).to eq("")
|
|
60
34
|
end
|
|
61
35
|
end
|
|
62
|
-
|
|
63
36
|
end
|
|
64
37
|
|
|
65
38
|
describe "configurable header" do
|
|
@@ -70,13 +43,8 @@ describe Stitches::CallingServiceName do
|
|
|
70
43
|
context "when configured to a custom header" do
|
|
71
44
|
let(:headers) { {"X-Custom-Caller" => "my-service"} }
|
|
72
45
|
|
|
73
|
-
before
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
after do
|
|
78
|
-
Stitches.configuration.reset_to_defaults!
|
|
79
|
-
end
|
|
46
|
+
before { Stitches.configuration.calling_service_header = "X-Custom-Caller" }
|
|
47
|
+
after { Stitches.configuration.reset_to_defaults! }
|
|
80
48
|
|
|
81
49
|
it "reads from the configured header" do
|
|
82
50
|
expect(fake_controller.calling_service_name).to eq("my-service")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stitches
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.1.0.
|
|
4
|
+
version: 5.1.0.RC5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stitch Fix Engineering
|
|
@@ -174,6 +174,8 @@ files:
|
|
|
174
174
|
- lib/stitches/api_key.rb
|
|
175
175
|
- lib/stitches/api_migration_generator.rb
|
|
176
176
|
- lib/stitches/api_version_constraint.rb
|
|
177
|
+
- lib/stitches/calling_service_client.rb
|
|
178
|
+
- lib/stitches/calling_service_middleware.rb
|
|
177
179
|
- lib/stitches/calling_service_name.rb
|
|
178
180
|
- lib/stitches/configuration.rb
|
|
179
181
|
- lib/stitches/deprecation.rb
|
|
@@ -209,6 +211,7 @@ files:
|
|
|
209
211
|
- owners.json
|
|
210
212
|
- spec/api_key_middleware_spec.rb
|
|
211
213
|
- spec/api_version_constraint_middleware_spec.rb
|
|
214
|
+
- spec/calling_service_middleware_spec.rb
|
|
212
215
|
- spec/calling_service_name_spec.rb
|
|
213
216
|
- spec/configuration_spec.rb
|
|
214
217
|
- spec/deprecation_spec.rb
|
|
@@ -306,6 +309,7 @@ summary: You'll be in stitches at how easy it is to create a service at Stitch F
|
|
|
306
309
|
test_files:
|
|
307
310
|
- spec/api_key_middleware_spec.rb
|
|
308
311
|
- spec/api_version_constraint_middleware_spec.rb
|
|
312
|
+
- spec/calling_service_middleware_spec.rb
|
|
309
313
|
- spec/calling_service_name_spec.rb
|
|
310
314
|
- spec/configuration_spec.rb
|
|
311
315
|
- spec/deprecation_spec.rb
|