diff --git a/themis.yaml b/themis.yaml index de2b67d..7411efa 100644 --- a/themis.yaml +++ b/themis.yaml @@ -114,6 +114,13 @@ token: - key: uuid header: X-Midt-Uuid parameter: uuid + pathValues: + - key: mac + header: X-Midt-Mac-Address + parameter: mac + remote: + method: GET + url: https://localhost:443/device/{mac}/claims?format=json partnerID: claim: partner-id metadata: pid diff --git a/token/claimBuilder.go b/token/claimBuilder.go index cf47b2b..d84c976 100644 --- a/token/claimBuilder.go +++ b/token/claimBuilder.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "errors" "fmt" + "maps" "net/http" "net/url" "time" @@ -125,19 +126,25 @@ type remoteClaimBuilder struct { } func (rc *remoteClaimBuilder) AddClaims(ctx context.Context, r *Request, target map[string]interface{}) error { - metadata := r.Metadata + rCopy := Request{ + Metadata: make(map[string]interface{}), + PathValues: make(map[string]interface{}), + } + + maps.Copy(rCopy.Metadata, r.Metadata) + maps.Copy(rCopy.PathValues, r.PathValues) if len(rc.extra) > 0 { - metadata = make(map[string]interface{}, len(r.Metadata)+len(rc.extra)) + rCopy.Metadata = make(map[string]interface{}, len(r.Metadata)+len(rc.extra)) for k, v := range r.Metadata { - metadata[k] = v + rCopy.Metadata[k] = v } for k, v := range rc.extra { - metadata[k] = v + rCopy.Metadata[k] = v } } - result, err := rc.endpoint(ctx, metadata) + result, err := rc.endpoint(ctx, &rCopy) if err != nil { return err } @@ -171,7 +178,7 @@ func newRemoteClaimBuilder(client xhttpclient.Interface, metadata map[string]int c := kithttp.NewClient( method, url, - kithttp.EncodeJSONRequest, + EncodeRemoteClaimsRequest, DecodeRemoteClaimsResponse, kithttp.SetClient(client), kithttp.ClientBefore( diff --git a/token/claimBuilder_test.go b/token/claimBuilder_test.go index 93a4fa1..8dfd555 100644 --- a/token/claimBuilder_test.go +++ b/token/claimBuilder_test.go @@ -810,7 +810,7 @@ func (suite *NewClaimBuildersTestSuite) TestFull() { actual := make(map[string]interface{}) suite.NoError( - builder.AddClaims(context.Background(), &Request{Claims: map[string]interface{}{"request": 123}}, actual), + builder.AddClaims(context.Background(), &Request{Claims: map[string]interface{}{"request": 123}, PathValues: make(map[string]interface{})}, actual), ) suite.Equal( diff --git a/token/factory.go b/token/factory.go index cea7e15..c42944a 100644 --- a/token/factory.go +++ b/token/factory.go @@ -34,6 +34,10 @@ type Request struct { // metadata is available to lower levels of infrastructure used by the Factory. Metadata map[string]interface{} + // PathValues holds non-claim information about the request, usually garnered from the original HTTP request. This + // PathValues is available to remote claim builders. + PathValues map[string]interface{} + // TLS represents the state of any underlying TLS connection. // For non-tls connections, this field is unset. TLS *tls.ConnectionState @@ -42,9 +46,10 @@ type Request struct { // NewRequest returns an empty, fully initialized token Request func NewRequest() *Request { return &Request{ - Logger: sallust.Default(), - Claims: make(map[string]interface{}), - Metadata: make(map[string]interface{}), + Logger: sallust.Default(), + Claims: make(map[string]interface{}), + Metadata: make(map[string]interface{}), + PathValues: make(map[string]interface{}), } } diff --git a/token/options.go b/token/options.go index 487e5cd..135df8e 100644 --- a/token/options.go +++ b/token/options.go @@ -91,6 +91,10 @@ type PartnerID struct { // is set and thus the partner id won't be transmitted to remote systems. Metadata string + // PathValue is the name of the path value key for the partner id. If unset, no path value + // is set and thus the partner id won't be transmitted to remote systems via url path value. + PathValue string + // Header is the HTTP header containing the partner id Header string @@ -200,6 +204,10 @@ type Options struct { // Metadata describes non-claim data, which can be statically configured or supplied via a request Metadata []Value + // PathValues holds non-claim information about the request, usually garnered from the original HTTP request. This + // PathValues is available to remote claim builders. + PathValues []Value + // PartnerID is the optional partner id configuration. If unset, no partner id processing is // performed, though a partner id may still be configured as part of the claims. PartnerID *PartnerID diff --git a/token/transport.go b/token/transport.go index e91dcd5..27206be 100644 --- a/token/transport.go +++ b/token/transport.go @@ -111,6 +111,10 @@ func metadataSetter(key string, value interface{}, tr *Request) { tr.Metadata[key] = value } +func pathValuesSetter(key string, value interface{}, tr *Request) { + tr.PathValues[key] = value +} + type headerParameterRequestBuilder struct { key string header string @@ -204,6 +208,10 @@ func (prb partnerIDRequestBuilder) Build(original *http.Request, tr *Request) er if len(prb.Metadata) > 0 { tr.Metadata[prb.Metadata] = partnerID } + + if len(prb.PathValue) > 0 { + tr.PathValues[prb.PathValue] = partnerID + } } return nil @@ -282,7 +290,37 @@ func NewRequestBuilders(o Options) (RequestBuilders, error) { } } - if o.PartnerID != nil && (len(o.PartnerID.Claim) > 0 || len(o.PartnerID.Metadata) > 0) { + for _, value := range o.PathValues { + switch { + case len(value.Key) == 0: + return nil, ErrMissingKey + + case len(value.Header) > 0 || len(value.Parameter) > 0: + if len(value.Variable) > 0 { + return nil, ErrVariableNotAllowed + } + + rb = append(rb, + headerParameterRequestBuilder{ + key: value.Key, + header: http.CanonicalHeaderKey(value.Header), + parameter: value.Parameter, + setter: pathValuesSetter, + }, + ) + + case len(value.Variable) > 0: + rb = append(rb, + variableRequestBuilder{ + key: value.Key, + variable: value.Variable, + setter: pathValuesSetter, + }, + ) + } + } + + if o.PartnerID != nil && (len(o.PartnerID.Claim) > 0 || len(o.PartnerID.Metadata) > 0 || len(o.PartnerID.PathValue) > 0) { rb = append(rb, partnerIDRequestBuilder{ PartnerID: *o.PartnerID, @@ -402,3 +440,25 @@ func DecodeRemoteClaimsResponse(_ context.Context, response *http.Response) (int return claims, nil } + +func EncodeRemoteClaimsRequest(c context.Context, r *http.Request, request interface{}) error { + if headerer, ok := request.(kithttp.Headerer); ok { + for k := range headerer.Headers() { + r.Header.Set(k, headerer.Headers().Get(k)) + } + } + + tr := request.(*Request) + for k, v := range tr.PathValues { + r.URL.Path = strings.ReplaceAll(r.URL.Path, fmt.Sprintf("{%s}", k), v.(string)) + } + + b, err := json.Marshal(tr.Metadata) + if err != nil { + return err + } + + r.Body = io.NopCloser(bytes.NewReader(b)) + + return nil +} diff --git a/token/transport_test.go b/token/transport_test.go index 6c96b8d..04c49f5 100644 --- a/token/transport_test.go +++ b/token/transport_test.go @@ -55,6 +55,22 @@ func testNewRequestBuildersInvalidMetadata(t *testing.T) { assert.Empty(rb) } +func testNewRequestBuildersInvalidPathValues(t *testing.T) { + assert := assert.New(t) + rb, err := NewRequestBuilders(Options{ + PathValues: []Value{ + { + Key: "bad", + Header: "xxx", + Parameter: "yyy", + Variable: "zzz", + }, + }, + }) + + assert.Equal(ErrVariableNotAllowed, err) + assert.Empty(rb) +} func testNewRequestBuildersSuccess(t *testing.T) { testData := []struct { options Options @@ -89,16 +105,24 @@ func testNewRequestBuildersSuccess(t *testing.T) { Header: "X-Missing", }, }, + PathValues: []Value{ + { + Key: "fromHeader", + Header: "X-PathVlaue", + }, + }, PartnerID: &PartnerID{ - Claim: "partner-id-claim", - Metadata: "partner-id-metadata", - Header: "X-Midt-Partner-ID", + Claim: "partner-id-claim", + Metadata: "partner-id-metadata", + PathValue: "partner-id-pathValue", + Header: "X-Midt-Partner-ID", }, }, uri: "/test", header: http.Header{ "X-Claim": []string{"foo"}, "X-Metadata": []string{"bar"}, + "X-PathVlaue": []string{"foobar"}, "X-Midt-Partner-ID": []string{"test"}, }, expected: &Request{ @@ -111,6 +135,10 @@ func testNewRequestBuildersSuccess(t *testing.T) { "fromHeader": "bar", "partner-id-metadata": "test", }, + PathValues: map[string]any{ + "fromHeader": "foobar", + "partner-id-pathValue": "test", + }, }, }, { @@ -135,13 +163,24 @@ func testNewRequestBuildersSuccess(t *testing.T) { Parameter: "missing", }, }, + PathValues: []Value{ + { + Key: "fromParameter", + Parameter: "pathValue", + }, + { + Key: "missing", + Parameter: "missing", + }, + }, PartnerID: &PartnerID{ Claim: "partner-id-claim", Metadata: "partner-id-metadata", + PathValue: "partner-id-pathValue", Parameter: "pid", }, }, - uri: "/test?pid=test&claim=foo&metadata=bar", + uri: "/test?pid=test&claim=foo&metadata=bar&pathValue=foobar", expected: &Request{ Logger: sallust.Default(), Claims: map[string]interface{}{ @@ -152,6 +191,10 @@ func testNewRequestBuildersSuccess(t *testing.T) { "fromParameter": "bar", "partner-id-metadata": "test", }, + PathValues: map[string]any{ + "fromParameter": "foobar", + "partner-id-pathValue": "test", + }, }, }, { @@ -168,17 +211,25 @@ func testNewRequestBuildersSuccess(t *testing.T) { Variable: "metadata", }, }, + PathValues: []Value{ + { + Key: "fromVariable", + Variable: "pathValues", + }, + }, PartnerID: &PartnerID{ Claim: "partner-id-claim", Metadata: "partner-id-metadata", + PathValue: "partner-id-pathValue", Parameter: "pid", Default: "test", }, }, uri: "/test/foo/bar", urlVariables: map[string]string{ - "claim": "foo", - "metadata": "bar", + "claim": "foo", + "metadata": "bar", + "pathValues": "foobar", }, expected: &Request{ Logger: sallust.Default(), @@ -190,6 +241,10 @@ func testNewRequestBuildersSuccess(t *testing.T) { "fromVariable": "bar", "partner-id-metadata": "test", }, + PathValues: map[string]any{ + "fromVariable": "foobar", + "partner-id-pathValue": "test", + }, }, }, { @@ -206,11 +261,18 @@ func testNewRequestBuildersSuccess(t *testing.T) { Variable: "metadata", }, }, + PathValues: []Value{ + { + Key: "fromVariable", + Variable: "pathValue", + }, + }, }, uri: "/test/foo/bar", urlVariables: map[string]string{ - "claim": "foo", - "metadata": "bar", + "claim": "foo", + "metadata": "bar", + "pathValue": "foobar", }, expected: &Request{ Logger: sallust.Default(), @@ -220,6 +282,9 @@ func testNewRequestBuildersSuccess(t *testing.T) { Metadata: map[string]interface{}{ "fromVariable": "bar", }, + PathValues: map[string]any{ + "fromVariable": "foobar", + }, }, }, } @@ -316,6 +381,7 @@ func testNewRequestBuildersInvalidPartnerID(t *testing.T) { func TestNewRequestBuilders(t *testing.T) { t.Run("InvalidClaim", testNewRequestBuildersInvalidClaim) t.Run("InvalidMetadata", testNewRequestBuildersInvalidMetadata) + t.Run("InvalidPathValues", testNewRequestBuildersInvalidPathValues) t.Run("MissingVariable", testNewRequestBuildersMissingVariable) t.Run("InvalidPartnerID", testNewRequestBuildersInvalidPartnerID) t.Run("Success", testNewRequestBuildersSuccess) @@ -341,9 +407,10 @@ func testBuildRequestSuccess(t *testing.T) { }), }, expected: &Request{ - Logger: sallust.Default(), - Claims: map[string]interface{}{"claim": []int{1, 2, 3}}, - Metadata: make(map[string]interface{}), + Logger: sallust.Default(), + Claims: map[string]interface{}{"claim": []int{1, 2, 3}}, + Metadata: make(map[string]interface{}), + PathValues: make(map[string]interface{}), }, }, { @@ -354,9 +421,10 @@ func testBuildRequestSuccess(t *testing.T) { }), }, expected: &Request{ - Logger: sallust.Default(), - Claims: make(map[string]interface{}), - Metadata: map[string]interface{}{"metadata": -75.8}, + Logger: sallust.Default(), + Claims: make(map[string]interface{}), + Metadata: map[string]interface{}{"metadata": -75.8}, + PathValues: make(map[string]interface{}), }, }, { @@ -376,9 +444,10 @@ func testBuildRequestSuccess(t *testing.T) { }), }, expected: &Request{ - Logger: sallust.Default(), - Claims: map[string]interface{}{"claim1": 238947123, "claim2": []byte{1, 2, 3}}, - Metadata: map[string]interface{}{"metadata1": "value1", "metadata2": 15.7}, + Logger: sallust.Default(), + Claims: map[string]interface{}{"claim1": 238947123, "claim2": []byte{1, 2, 3}}, + Metadata: map[string]interface{}{"metadata1": "value1", "metadata2": 15.7}, + PathValues: make(map[string]interface{}), }, }, } @@ -569,9 +638,10 @@ func testDecodeServerRequestSuccess(t *testing.T) { require.IsType((*Request)(nil), v) assert.Equal( Request{ - Logger: sallust.Default(), - Claims: map[string]interface{}{"claim": "value"}, - Metadata: make(map[string]interface{}), + Logger: sallust.Default(), + Claims: map[string]interface{}{"claim": "value"}, + Metadata: make(map[string]interface{}), + PathValues: make(map[string]interface{}), }, *v.(*Request), )