Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-11908] add request signing to OAS upstream authentication #6850

Merged
merged 2 commits into from
Feb 7, 2025

Conversation

pvormste
Copy link
Contributor

@pvormste pvormste commented Jan 30, 2025

User description

TT-11908
Summary [OAS] Upstream authentication: HMAC request signing
Type Story Story
Status In Dev
Points N/A
Labels jira_escalated

This PR adds request signing to OAS upstream authentication. It also refactors the validation of the request signing config in the request signing middleware.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring or add test (improvements in base code or adds test coverage to functionality)

PR Type

Enhancement, Tests


Description

  • Added HMAC request signing for OAS upstream authentication.

  • Introduced validation for request signing configuration.

  • Updated schema to include request signing properties.

  • Added comprehensive unit tests for request signing functionality.


Changes walkthrough 📝

Relevant files
Tests
oas_test.go
Updated test expectations for request signing fields         

apidef/oas/oas_test.go

  • Removed outdated request signing fields from test expectations.
+0/-7     
upstream_test.go
Added tests for upstream request signing functionality     

apidef/oas/upstream_test.go

  • Added unit tests for UpstreamRequestSigning functionality.
  • Covered Fill and ExtractTo methods with test cases.
  • +136/-0 
    mw_request_signing_test.go
    Added tests for request signing configuration validation 

    gateway/mw_request_signing_test.go

  • Added unit tests for request signing configuration validation.
  • Covered edge cases for RSA and non-RSA algorithms.
  • +85/-0   
    Enhancement
    upstream.go
    Added request signing support to upstream authentication 

    apidef/oas/upstream.go

  • Added RequestSigning configuration to UpstreamAuth.
  • Implemented Fill and ExtractTo methods for request signing.
  • Added helper methods for request signing handling.
  • +78/-12 
    mw_request_signing.go
    Refactored request signing validation logic                           

    gateway/mw_request_signing.go

  • Refactored request signing validation into a helper method.
  • Improved error handling for invalid configurations.
  • +18/-1   
    x-tyk-api-gateway.json
    Updated schema to include request signing configuration   

    apidef/oas/schema/x-tyk-api-gateway.json

  • Added X-Tyk-UpstreamRequestSigning schema definition.
  • Updated X-Tyk-UpstreamAuthentication to include request signing.
  • +35/-0   

    Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • @buger
    Copy link
    Member

    buger commented Jan 30, 2025

    I'm a bot and I 👍 this PR title. 🤖

    Copy link
    Contributor

    github-actions bot commented Jan 30, 2025

    API Changes

    --- prev.txt	2025-02-07 09:25:34.061455287 +0000
    +++ current.txt	2025-02-07 09:25:29.614445518 +0000
    @@ -4330,15 +4330,17 @@
     	BasicAuth *UpstreamBasicAuth `bson:"basicAuth,omitempty" json:"basicAuth,omitempty"`
     	// OAuth contains the configuration for OAuth2 Client Credentials flow.
     	OAuth *UpstreamOAuth `bson:"oauth,omitempty" json:"oauth,omitempty"`
    +	// RequestSigning holds the configuration for generating signed requests to an upstream API.
    +	RequestSigning *UpstreamRequestSigning `bson:"requestSigning,omitempty" json:"requestSigning,omitempty"`
     }
         UpstreamAuth holds the configurations related to upstream API
         authentication.
     
    -func (u *UpstreamAuth) ExtractTo(api *apidef.UpstreamAuth)
    -    ExtractTo extracts *UpstreamAuth into *apidef.UpstreamAuth.
    +func (u *UpstreamAuth) ExtractTo(api *apidef.APIDefinition)
    +    ExtractTo extracts *UpstreamAuth into *apidef.APIDefinition.
     
    -func (u *UpstreamAuth) Fill(api apidef.UpstreamAuth)
    -    Fill fills *UpstreamAuth from apidef.UpstreamAuth.
    +func (u *UpstreamAuth) Fill(api apidef.APIDefinition)
    +    Fill fills *UpstreamAuth from apidef.APIDefinition.
     
     type UpstreamBasicAuth struct {
     	// Enabled enables upstream basic authentication.
    @@ -4374,6 +4376,33 @@
     
     func (u *UpstreamOAuth) Fill(api apidef.UpstreamOAuth)
     
    +type UpstreamRequestSigning struct {
    +	// Enabled determines if request signing is enabled or disabled.
    +	Enabled bool `bson:"enabled" json:"enabled"` // required
    +	// SignatureHeader specifies the HTTP header name for the signature.
    +	SignatureHeader string `bson:"signatureHeader,omitempty" json:"signatureHeader,omitempty"`
    +	// Algorithm represents the signing algorithm used (e.g., HMAC-SHA256).
    +	Algorithm string `bson:"algorithm,omitempty" json:"algorithm,omitempty"`
    +	// KeyID identifies the key used for signing purposes.
    +	KeyID string `bson:"keyId,omitempty" json:"keyId,omitempty"`
    +	// Headers contains a list of headers included in the signature calculation.
    +	Headers []string `bson:"headers,omitempty" json:"headers,omitempty"`
    +	// Secret holds the secret used for signing when applicable.
    +	Secret string `bson:"secret,omitempty" json:"secret,omitempty"`
    +	// CertificateID specifies the certificate ID used in signing operations.
    +	CertificateID string `bson:"certificateId,omitempty" json:"certificateId,omitempty"`
    +}
    +    UpstreamRequestSigning represents configuration for generating signed
    +    requests to an upstream API.
    +
    +func (l *UpstreamRequestSigning) ExtractTo(api *apidef.APIDefinition)
    +    ExtractTo populates the given apidef.APIDefinition RequestSigning fields
    +    with values from the UpstreamRequestSigning.
    +
    +func (l *UpstreamRequestSigning) Fill(api apidef.APIDefinition)
    +    Fill populates the UpstreamRequestSigning fields from the given
    +    apidef.APIDefinition configuration.
    +
     type ValidateRequest struct {
     	// Enabled is a boolean flag, if set to `true`, it enables request validation.
     	Enabled bool `bson:"enabled" json:"enabled"`

    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 PR contains tests
    🔒 Security concerns

    Sensitive information exposure:
    The Secret field in UpstreamRequestSigning is a potential security concern. Ensure that this field is handled securely, avoiding accidental logging or exposure in error messages.

    ⚡ Recommended focus areas for review

    Validation Logic

    The new isRequestSigningConfigValid function introduces validation logic for request signing configurations. Ensure that the logic comprehensively covers all edge cases and aligns with the intended behavior of the feature.

    func (s *RequestSigning) isRequestSigningConfigValid() bool {
    	if s.Spec.RequestSigning.KeyId == "" || s.Spec.RequestSigning.Algorithm == "" {
    		return false
    	}
    
    	isRSAAlgorithm := strings.HasPrefix(s.Spec.RequestSigning.Algorithm, "rsa")
    	if isRSAAlgorithm && s.Spec.RequestSigning.CertificateId == "" {
    		return false
    	}
    
    	if !isRSAAlgorithm && s.Spec.RequestSigning.Secret == "" {
    		return false
    	}
    
    	return true
    Request Signing Integration

    The integration of RequestSigning into UpstreamAuth and its Fill and ExtractTo methods should be reviewed for correctness and consistency with the existing authentication mechanisms.

    type UpstreamAuth struct {
    	// Enabled enables upstream API authentication.
    	Enabled bool `bson:"enabled" json:"enabled"`
    	// BasicAuth holds the basic authentication configuration for upstream API authentication.
    	BasicAuth *UpstreamBasicAuth `bson:"basicAuth,omitempty" json:"basicAuth,omitempty"`
    	// OAuth contains the configuration for OAuth2 Client Credentials flow.
    	OAuth *UpstreamOAuth `bson:"oauth,omitempty" json:"oauth,omitempty"`
    	// RequestSigning holds the configuration for generating signed requests to an upstream API.
    	RequestSigning *UpstreamRequestSigning `bson:"requestSigning,omitempty" json:"requestSigning,omitempty"`
    }
    
    // Fill fills *UpstreamAuth from apidef.APIDefinition.
    func (u *UpstreamAuth) Fill(api apidef.APIDefinition) {
    	u.Enabled = api.UpstreamAuth.Enabled
    
    	if u.BasicAuth == nil {
    		u.BasicAuth = &UpstreamBasicAuth{}
    	}
    	u.BasicAuth.Fill(api.UpstreamAuth.BasicAuth)
    	if ShouldOmit(u.BasicAuth) {
    		u.BasicAuth = nil
    	}
    
    	if u.OAuth == nil {
    		u.OAuth = &UpstreamOAuth{}
    	}
    	u.OAuth.Fill(api.UpstreamAuth.OAuth)
    	if ShouldOmit(u.OAuth) {
    		u.OAuth = nil
    	}
    
    	u.fillRequestSigning(api)
    }
    
    // ExtractTo extracts *UpstreamAuth into *apidef.APIDefinition.
    func (u *UpstreamAuth) ExtractTo(api *apidef.APIDefinition) {
    	api.UpstreamAuth.Enabled = u.Enabled
    
    	if u.BasicAuth == nil {
    		u.BasicAuth = &UpstreamBasicAuth{}
    		defer func() {
    			u.BasicAuth = nil
    		}()
    	}
    	u.BasicAuth.ExtractTo(&api.UpstreamAuth.BasicAuth)
    
    	if u.OAuth == nil {
    		u.OAuth = &UpstreamOAuth{}
    		defer func() {
    			u.OAuth = nil
    		}()
    	}
    	u.OAuth.ExtractTo(&api.UpstreamAuth.OAuth)
    
    	u.requestSigningExtractTo(api)
    }
    
    func (u *UpstreamAuth) fillRequestSigning(api apidef.APIDefinition) {
    	if u.RequestSigning == nil {
    		u.RequestSigning = &UpstreamRequestSigning{}
    	}
    	u.RequestSigning.Fill(api)
    	if ShouldOmit(u.RequestSigning) {
    		u.RequestSigning = nil
    	}
    }
    
    func (u *UpstreamAuth) requestSigningExtractTo(api *apidef.APIDefinition) {
    	if u.RequestSigning == nil {
    		u.RequestSigning = &UpstreamRequestSigning{}
    		defer func() {
    			u.BasicAuth = nil
    		}()
    	}
    	u.RequestSigning.ExtractTo(api)
    }
    
    // UpstreamBasicAuth holds upstream basic authentication configuration.
    type UpstreamBasicAuth struct {
    	// Enabled enables upstream basic authentication.
    	Enabled bool `bson:"enabled" json:"enabled"`
    	// Header contains configurations for the header value.
    	Header *AuthSource `bson:"header,omitempty" json:"header,omitempty"`
    	// Username is the username to be used for upstream basic authentication.
    	Username string `bson:"username" json:"username"`
    	// Password is the password to be used for upstream basic authentication.
    	Password string `bson:"password" json:"password"`
    }
    
    // Fill fills *UpstreamBasicAuth from apidef.UpstreamBasicAuth.
    func (u *UpstreamBasicAuth) Fill(api apidef.UpstreamBasicAuth) {
    	u.Enabled = api.Enabled
    	u.Username = api.Username
    	u.Password = api.Password
    
    	if u.Header == nil {
    		u.Header = &AuthSource{}
    	}
    	u.Header.Fill(api.Header.Enabled, api.Header.Name)
    	if ShouldOmit(u.Header) {
    		u.Header = nil
    	}
    }
    
    // ExtractTo extracts *UpstreamBasicAuth into *apidef.UpstreamBasicAuth.
    func (u *UpstreamBasicAuth) ExtractTo(api *apidef.UpstreamBasicAuth) {
    	api.Enabled = u.Enabled
    	api.Enabled = u.Enabled
    	api.Username = u.Username
    	api.Password = u.Password
    
    	if u.Header == nil {
    		u.Header = &AuthSource{}
    		defer func() {
    			u.Header = nil
    		}()
    	}
    	u.Header.ExtractTo(&api.Header.Enabled, &api.Header.Name)
    }
    
    // UpstreamOAuth holds the configuration for OAuth2 Client Credentials flow.
    type UpstreamOAuth struct {
    	// Enabled activates upstream OAuth2 authentication.
    	Enabled bool `bson:"enabled" json:"enabled"`
    	// AllowedAuthorizeTypes specifies the allowed authorization types for upstream OAuth2 authentication.
    	AllowedAuthorizeTypes []string `bson:"allowedAuthorizeTypes" json:"allowedAuthorizeTypes"`
    	// ClientCredentials holds the configuration for OAuth2 Client Credentials flow.
    	ClientCredentials *ClientCredentials `bson:"clientCredentials,omitempty" json:"clientCredentials,omitempty"`
    	// PasswordAuthentication holds the configuration for upstream OAauth password authentication flow.
    	PasswordAuthentication *PasswordAuthentication `bson:"password,omitempty" json:"password,omitempty"`
    }
    
    // PasswordAuthentication holds the configuration for upstream OAuth2 password authentication flow.
    type PasswordAuthentication struct {
    	ClientAuthData
    	// Header holds the configuration for the custom header to be used for OAuth authentication.
    	Header *AuthSource `bson:"header" json:"header"`
    	// Username is the username to be used for upstream OAuth2 password authentication.
    	Username string `bson:"username" json:"username"`
    	// Password is the password to be used for upstream OAuth2 password authentication.
    	Password string `bson:"password" json:"password"`
    	// TokenURL is the resource server's token endpoint
    	// URL. This is a constant specific to each server.
    	TokenURL string `bson:"tokenUrl" json:"tokenUrl"`
    	// Scopes specifies optional requested permissions.
    	Scopes []string `bson:"scopes" json:"scopes,omitempty"`
    	// ExtraMetadata holds the keys that we want to extract from the token and pass to the upstream.
    	ExtraMetadata []string `bson:"extraMetadata" json:"extraMetadata,omitempty"`
    }
    
    // ClientAuthData holds the client ID and secret for OAuth2 authentication.
    type ClientAuthData struct {
    	// ClientID is the application's ID.
    	ClientID string `bson:"clientId" json:"clientId"`
    	// ClientSecret is the application's secret.
    	ClientSecret string `bson:"clientSecret,omitempty" json:"clientSecret,omitempty"` // client secret is optional for password flow
    }
    
    // ClientCredentials holds the configuration for OAuth2 Client Credentials flow.
    type ClientCredentials struct {
    	ClientAuthData
    	// Header holds the configuration for the custom header to be used for OAuth authentication.
    	Header *AuthSource `bson:"header" json:"header"`
    	// TokenURL is the resource server's token endpoint
    	// URL. This is a constant specific to each server.
    	TokenURL string `bson:"tokenUrl" json:"tokenUrl"`
    	// Scopes specifies optional requested permissions.
    	Scopes []string `bson:"scopes,omitempty" json:"scopes,omitempty"`
    	// ExtraMetadata holds the keys that we want to extract from the token and pass to the upstream.
    	ExtraMetadata []string `bson:"extraMetadata" json:"extraMetadata,omitempty"`
    }
    
    func (c *ClientCredentials) Fill(api apidef.ClientCredentials) {
    	c.ClientID = api.ClientID
    	c.ClientSecret = api.ClientSecret
    	c.TokenURL = api.TokenURL
    	c.Scopes = api.Scopes
    	c.ExtraMetadata = api.ExtraMetadata
    
    	if c.Header == nil {
    		c.Header = &AuthSource{}
    	}
    	c.Header.Fill(api.Header.Enabled, api.Header.Name)
    	if ShouldOmit(c.Header) {
    		c.Header = nil
    	}
    }
    
    func (p *PasswordAuthentication) Fill(api apidef.PasswordAuthentication) {
    	p.ClientID = api.ClientID
    	p.ClientSecret = api.ClientSecret
    	p.Username = api.Username
    	p.Password = api.Password
    	p.TokenURL = api.TokenURL
    	p.Scopes = api.Scopes
    	p.ExtraMetadata = api.ExtraMetadata
    	if p.Header == nil {
    		p.Header = &AuthSource{}
    	}
    	p.Header.Fill(api.Header.Enabled, api.Header.Name)
    	if ShouldOmit(p.Header) {
    		p.Header = nil
    	}
    }
    
    func (u *UpstreamOAuth) Fill(api apidef.UpstreamOAuth) {
    	u.Enabled = api.Enabled
    	u.AllowedAuthorizeTypes = api.AllowedAuthorizeTypes
    
    	if u.ClientCredentials == nil {
    		u.ClientCredentials = &ClientCredentials{}
    	}
    	u.ClientCredentials.Fill(api.ClientCredentials)
    	if ShouldOmit(u.ClientCredentials) {
    		u.ClientCredentials = nil
    	}
    
    	if u.PasswordAuthentication == nil {
    		u.PasswordAuthentication = &PasswordAuthentication{}
    	}
    	u.PasswordAuthentication.Fill(api.PasswordAuthentication)
    	if ShouldOmit(u.PasswordAuthentication) {
    		u.PasswordAuthentication = nil
    	}
    }
    
    func (c *ClientCredentials) ExtractTo(api *apidef.ClientCredentials) {
    	api.ClientID = c.ClientID
    	api.ClientSecret = c.ClientSecret
    	api.TokenURL = c.TokenURL
    	api.Scopes = c.Scopes
    	api.ExtraMetadata = c.ExtraMetadata
    
    	if c.Header == nil {
    		c.Header = &AuthSource{}
    		defer func() {
    			c.Header = nil
    		}()
    	}
    	c.Header.ExtractTo(&api.Header.Enabled, &api.Header.Name)
    }
    
    func (p *PasswordAuthentication) ExtractTo(api *apidef.PasswordAuthentication) {
    	api.ClientID = p.ClientID
    	api.ClientSecret = p.ClientSecret
    	api.Username = p.Username
    	api.Password = p.Password
    	api.TokenURL = p.TokenURL
    	api.Scopes = p.Scopes
    	api.ExtraMetadata = p.ExtraMetadata
    
    	if p.Header == nil {
    		p.Header = &AuthSource{}
    		defer func() {
    			p.Header = nil
    		}()
    	}
    	p.Header.ExtractTo(&api.Header.Enabled, &api.Header.Name)
    }
    
    func (u *UpstreamOAuth) ExtractTo(api *apidef.UpstreamOAuth) {
    	api.Enabled = u.Enabled
    	api.AllowedAuthorizeTypes = u.AllowedAuthorizeTypes
    	if u.ClientCredentials == nil {
    		u.ClientCredentials = &ClientCredentials{}
    		defer func() {
    			u.ClientCredentials = nil
    		}()
    	}
    	u.ClientCredentials.ExtractTo(&api.ClientCredentials)
    
    	if u.PasswordAuthentication == nil {
    		u.PasswordAuthentication = &PasswordAuthentication{}
    		defer func() {
    			u.PasswordAuthentication = nil
    		}()
    	}
    	u.PasswordAuthentication.ExtractTo(&api.PasswordAuthentication)
    }
    
    // UpstreamRequestSigning represents configuration for generating signed requests to an upstream API.
    type UpstreamRequestSigning struct {
    	// Enabled determines if request signing is enabled or disabled.
    	Enabled bool `bson:"enabled" json:"enabled"` // required
    	// SignatureHeader specifies the HTTP header name for the signature.
    	SignatureHeader string `bson:"signatureHeader,omitempty" json:"signatureHeader,omitempty"`
    	// Algorithm represents the signing algorithm used (e.g., HMAC-SHA256).
    	Algorithm string `bson:"algorithm,omitempty" json:"algorithm,omitempty"`
    	// KeyID identifies the key used for signing purposes.
    	KeyID string `bson:"keyId,omitempty" json:"keyId,omitempty"`
    	// Headers contains a list of headers included in the signature calculation.
    	Headers []string `bson:"headers,omitempty" json:"headers,omitempty"`
    	// Secret holds the secret used for signing when applicable.
    	Secret string `bson:"secret,omitempty" json:"secret,omitempty"`
    	// CertificateID specifies the certificate ID used in signing operations.
    	CertificateID string `bson:"certificateId,omitempty" json:"certificateId,omitempty"`
    }
    
    // Fill populates the UpstreamRequestSigning fields from the given apidef.APIDefinition configuration.
    func (l *UpstreamRequestSigning) Fill(api apidef.APIDefinition) {
    	l.Enabled = api.RequestSigning.IsEnabled
    	l.SignatureHeader = api.RequestSigning.SignatureHeader
    	l.Algorithm = api.RequestSigning.Algorithm
    	l.KeyID = api.RequestSigning.KeyId
    	l.Headers = api.RequestSigning.HeaderList
    	l.Secret = api.RequestSigning.Secret
    	l.CertificateID = api.RequestSigning.CertificateId
    }
    
    // ExtractTo populates the given apidef.APIDefinition RequestSigning fields with values from the UpstreamRequestSigning.
    func (l *UpstreamRequestSigning) ExtractTo(api *apidef.APIDefinition) {
    	api.RequestSigning.IsEnabled = l.Enabled
    	api.RequestSigning.SignatureHeader = l.SignatureHeader
    	api.RequestSigning.Algorithm = l.Algorithm
    	api.RequestSigning.KeyId = l.KeyID
    	api.RequestSigning.HeaderList = l.Headers
    	api.RequestSigning.Secret = l.Secret
    	api.RequestSigning.CertificateId = l.CertificateID
    }
    Test Coverage

    The new test cases for UpstreamRequestSigning should be reviewed to ensure they cover all possible scenarios, including edge cases for both Fill and ExtractTo methods.

    func TestUpstreamRequestSigning(t *testing.T) {
    	t.Parallel()
    	t.Run("fill", func(t *testing.T) {
    		t.Parallel()
    		testcases := []struct {
    			title    string
    			input    apidef.APIDefinition
    			expected *UpstreamAuth
    		}{
    			{
    				title: "request signing disabled and everything else is empty should omit",
    				input: apidef.APIDefinition{
    					RequestSigning: apidef.RequestSigningMeta{
    						IsEnabled:       false,
    						Secret:          "",
    						KeyId:           "",
    						Algorithm:       "",
    						HeaderList:      nil,
    						CertificateId:   "",
    						SignatureHeader: "",
    					},
    				},
    				expected: nil,
    			},
    			{
    				title: "request signing enabled and values are set",
    				input: apidef.APIDefinition{
    					RequestSigning: apidef.RequestSigningMeta{
    						IsEnabled:       true,
    						Secret:          "secret",
    						KeyId:           "key-1",
    						Algorithm:       "hmac-sha256",
    						HeaderList:      []string{"header1", "header2"},
    						CertificateId:   "cert-1",
    						SignatureHeader: "Signature",
    					},
    				},
    				expected: &UpstreamAuth{
    					RequestSigning: &UpstreamRequestSigning{
    						Enabled:         true,
    						SignatureHeader: "Signature",
    						Algorithm:       "hmac-sha256",
    						KeyID:           "key-1",
    						Headers:         []string{"header1", "header2"},
    						Secret:          "secret",
    						CertificateID:   "cert-1",
    					},
    				},
    			},
    		}
    
    		for _, tc := range testcases {
    			tc := tc
    			t.Run(tc.title, func(t *testing.T) {
    				t.Parallel()
    
    				g := new(Upstream)
    				g.Fill(tc.input)
    
    				assert.Equal(t, tc.expected, g.Authentication)
    			})
    		}
    	})
    
    	t.Run("extractTo", func(t *testing.T) {
    		t.Parallel()
    
    		testcases := []struct {
    			title                  string
    			input                  *UpstreamRequestSigning
    			expectedRequestSigning apidef.RequestSigningMeta
    		}{
    			{
    				title: "request signing disabled and everything else is empty",
    				input: &UpstreamRequestSigning{
    					Enabled:         false,
    					SignatureHeader: "",
    					Algorithm:       "",
    					KeyID:           "",
    					Headers:         nil,
    					Secret:          "",
    					CertificateID:   "",
    				},
    				expectedRequestSigning: apidef.RequestSigningMeta{
    					IsEnabled:       false,
    					Secret:          "",
    					KeyId:           "",
    					Algorithm:       "",
    					HeaderList:      nil,
    					CertificateId:   "",
    					SignatureHeader: "",
    				},
    			},
    			{
    				title: "request signing enabled and values are set",
    				input: &UpstreamRequestSigning{
    					Enabled:         true,
    					SignatureHeader: "Signature",
    					Algorithm:       "hmac-sha256",
    					KeyID:           "key-1",
    					Headers:         []string{"header1", "header2"},
    					Secret:          "secret",
    					CertificateID:   "cert-1",
    				},
    				expectedRequestSigning: apidef.RequestSigningMeta{
    					IsEnabled:       true,
    					Secret:          "secret",
    					KeyId:           "key-1",
    					Algorithm:       "hmac-sha256",
    					HeaderList:      []string{"header1", "header2"},
    					CertificateId:   "cert-1",
    					SignatureHeader: "Signature",
    				},
    			},
    		}
    
    		for _, tc := range testcases {
    			tc := tc // Creating a new 'tc' scoped to the loop
    			t.Run(tc.title, func(t *testing.T) {
    				t.Parallel()
    
    				g := new(Upstream)
    				g.Authentication = &UpstreamAuth{
    					RequestSigning: tc.input,
    				}
    
    				var apiDef apidef.APIDefinition
    				apiDef.RequestSigning.HeaderList = []string{"headerOld1", "headerOld2"}
    				g.ExtractTo(&apiDef)
    
    				assert.Equal(t, tc.expectedRequestSigning, apiDef.RequestSigning)
    			})
    		}
    	})

    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Possible issue
    Correct deferred reset logic for RequestSigning

    Fix the requestSigningExtractTo function to correctly reset RequestSigning instead
    of BasicAuth in the deferred function, as this appears to be a copy-paste error.

    apidef/oas/upstream.go [654-656]

     defer func() {
    -    u.BasicAuth = nil
    +    u.RequestSigning = nil
     }()
    Suggestion importance[1-10]: 10

    Why: The suggestion fixes a clear bug where BasicAuth is incorrectly reset instead of RequestSigning. This correction is crucial for ensuring the proper functionality of the requestSigningExtractTo method.

    10
    General
    Add edge case tests for validation

    Add test cases to TestRequestSigning_isRequestSigningConfigValid to cover edge cases
    like missing both Secret and CertificateId for non-RSA algorithms, ensuring
    comprehensive validation.

    gateway/mw_request_signing_test.go [652-659]

     {
    -    name: "non-RSA and empty secret",
    +    name: "non-RSA and missing both secret and certificate ID",
         conf: apidef.RequestSigningMeta{
             Secret:        "",
             KeyId:         "keyID",
             Algorithm:     "hmac-sha256",
    -        CertificateId: "certID",
    +        CertificateId: "",
         },
         expected: false,
     },
    Suggestion importance[1-10]: 7

    Why: Adding edge case tests enhances the robustness of the validation logic by ensuring comprehensive coverage. This is a valuable improvement, though it does not address a critical bug.

    7
    Add test for incomplete configurations

    Include a test case in TestUpstreamRequestSigning to verify behavior when
    RequestSigning is partially configured (e.g., missing Algorithm but with other
    fields set), ensuring robust handling of incomplete configurations.

    apidef/oas/upstream_test.go [569-587]

     {
    -    title: "request signing enabled and values are set",
    +    title: "request signing enabled but missing algorithm",
         input: apidef.APIDefinition{
             RequestSigning: apidef.RequestSigningMeta{
                 IsEnabled:       true,
                 Secret:          "secret",
                 KeyId:           "key-1",
    -            Algorithm:       "hmac-sha256",
    +            Algorithm:       "",
                 HeaderList:      []string{"header1", "header2"},
                 CertificateId:   "cert-1",
                 SignatureHeader: "Signature",
             },
         },
    -    expected: &UpstreamAuth{
    -        RequestSigning: &UpstreamRequestSigning{
    -            Enabled:         true,
    -            SignatureHeader: "Signature",
    -            Algorithm:       "hmac-sha256",
    -            KeyID:           "key-1",
    -            Headers:         []string{"header1", "header2"},
    -            Secret:          "secret",
    -            CertificateID:   "cert-1",
    -        },
    -    },
    +    expected: nil,
     },
    Suggestion importance[1-10]: 7

    Why: Including a test for partially configured RequestSigning improves test coverage and ensures the system handles incomplete configurations correctly. This is a useful enhancement but not critical.

    7
    Improve error logging for invalid configurations

    Ensure the isRequestSigningConfigValid function logs detailed information about
    which specific field is invalid when the validation fails, to aid debugging and
    troubleshooting.

    gateway/mw_request_signing.go [94-96]

     if !s.isRequestSigningConfigValid() {
    -    log.Error("Fields required for signing the request are missing")
    -    return errors.New("Fields required for signing the request are missing"), http.StatusInternalServerError
    +    log.Error("Request signing configuration is invalid: missing required fields or invalid values")
    +    return errors.New("Request signing configuration is invalid"), http.StatusInternalServerError
     }
    Suggestion importance[1-10]: 6

    Why: The suggestion improves error logging by providing more detailed information about invalid configurations, which can aid debugging. However, it does not address a critical issue, so the impact is moderate.

    6

    @pvormste pvormste force-pushed the feat/TT-11908/oas-hmac-request-signing branch from 8ca39c9 to 35c23c3 Compare February 7, 2025 09:24
    @pvormste pvormste enabled auto-merge (squash) February 7, 2025 09:25
    Copy link

    sonarqubecloud bot commented Feb 7, 2025

    Quality Gate Failed Quality Gate failed

    Failed conditions
    E Security Rating on New Code (required ≥ A)

    See analysis details on SonarQube Cloud

    Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

    @pvormste pvormste merged commit 1f91b24 into master Feb 7, 2025
    27 of 41 checks passed
    @pvormste pvormste deleted the feat/TT-11908/oas-hmac-request-signing branch February 7, 2025 09:48
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    3 participants