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

feat: forward basic auth user #3312

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion api/v1alpha1/basic_auth_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

package v1alpha1

import gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
import (
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

const BasicAuthUsersSecretKey = ".htpasswd"

Expand All @@ -24,4 +27,10 @@ type BasicAuth struct {
//
// Note: The secret must be in the same namespace as the SecurityPolicy.
Users gwapiv1b1.SecretObjectReference `json:"users"`

// The name of the HTTP header that will be used to forward the username to the upstream server.
// Please note that the header name is a global setting for all the routes in a Gateway.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserNameToHeader can also be per route, but will need some upstream work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo we should wait until per route support is added, else a per route setting will enable this at the global level

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's wait for the upstream.

// If multiple routes have different header names, a random one will be chosen.
// +optional
UserNameToHeader *gwapiv1.HeaderName `json:"userNameToHeader,omitempty"`
}
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ spec:
description: BasicAuth defines the configuration for the HTTP Basic
Authentication.
properties:
userNameToHeader:
description: |-
The name of the HTTP header that will be used to forward the username to the upstream server.
Please note that the header name is a global setting for all the routes in a Gateway.
If multiple routes have different header names, a random one will be chosen.
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
users:
description: |-
The Kubernetes secret which contains the username-password pairs in
Expand Down
16 changes: 11 additions & 5 deletions internal/gatewayapi/securitypolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,9 +735,10 @@ func (t *Translator) buildBasicAuth(
resources *Resources,
) (*ir.BasicAuth, error) {
var (
basicAuth = policy.Spec.BasicAuth
usersSecret *v1.Secret
err error
basicAuth = policy.Spec.BasicAuth
usersSecret *v1.Secret
userNameToHeader *string
err error
)

from := crossNamespaceFrom{
Expand All @@ -757,9 +758,14 @@ func (t *Translator) buildBasicAuth(
usersSecret.Namespace, usersSecret.Name)
}

if basicAuth.UserNameToHeader != nil {
userNameToHeader = (*string)(ptr.To(*basicAuth.UserNameToHeader))
}

return &ir.BasicAuth{
Name: irConfigName(policy),
Users: usersSecretBytes,
Name: irConfigName(policy),
Users: usersSecretBytes,
UserNameToHeader: userNameToHeader,
}, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ securityPolicies:
basicAuth:
users:
name: "users-secret1"
userNameToHeader: x-basic-auth-user
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ securityPolicies:
namespace: default
spec:
basicAuth:
userNameToHeader: x-basic-auth-user
users:
group: null
kind: null
Expand Down Expand Up @@ -246,6 +247,7 @@ xdsIR:
security:
basicAuth:
name: securitypolicy/default/policy-for-http-route-1
userNameToHeader: x-basic-auth-user
users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
- backendWeights:
invalid: 0
Expand All @@ -269,6 +271,7 @@ xdsIR:
security:
basicAuth:
name: securitypolicy/default/policy-for-http-route-1
userNameToHeader: x-basic-auth-user
users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
- backendWeights:
invalid: 0
Expand Down
3 changes: 3 additions & 0 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,9 @@ type BasicAuth struct {

// The username-password pairs in htpasswd format.
Users []byte `json:"users,omitempty" yaml:"users,omitempty"`

// The HTTP header field that will be used to send the extracted username to the backend.
UserNameToHeader *string `json:"userNameToHeader,omitempty" yaml:"userNameToHeader,omitempty"`
}

// ExtAuth defines the schema for the external authorization.
Expand Down
5 changes: 5 additions & 0 deletions internal/ir/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 17 additions & 7 deletions internal/xds/translator/basicauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,37 @@ func (*basicAuth) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTP
}

var (
irBasicAuth *ir.BasicAuth
filter *hcmv3.HttpFilter
err error
irBasicAuth *ir.BasicAuth
userNameToHeader *string
filter *hcmv3.HttpFilter
err error
)

for _, route := range irListener.Routes {
if route.Security != nil && route.Security.BasicAuth != nil {
irBasicAuth = route.Security.BasicAuth
break
// if any route has a UserNameToHeader set, forward the username to the upstream server.
if irBasicAuth.UserNameToHeader != nil {
userNameToHeader = irBasicAuth.UserNameToHeader
break
}
}
}
if irBasicAuth == nil {
return nil
}

// We use the first route that contains the basicAuth config to build the filter.
// We use a random route that contains the basicAuth config to build the filter.
// The HCM-level filter config doesn't matter since it is overridden at the route level.
if filter, err = buildHCMBasicAuthFilter(irBasicAuth); err != nil {
if filter, err = buildHCMBasicAuthFilter(irBasicAuth, userNameToHeader); err != nil {
return err
}
mgr.HttpFilters = append(mgr.HttpFilters, filter)
return err
}

// buildHCMBasicAuthFilter returns a basic_auth HTTP filter from the provided IR HTTPRoute.
func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth) (*hcmv3.HttpFilter, error) {
func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth, userNameToHeader *string) (*hcmv3.HttpFilter, error) {
var (
basicAuthProto *basicauthv3.BasicAuth
basicAuthAny *anypb.Any
Expand All @@ -84,6 +89,11 @@ func buildHCMBasicAuthFilter(basicAuth *ir.BasicAuth) (*hcmv3.HttpFilter, error)
},
},
}

if userNameToHeader != nil {
basicAuthProto.ForwardUsernameHeader = *userNameToHeader
}

if err = basicAuthProto.ValidateAll(); err != nil {
return nil, err
}
Expand Down
2 changes: 2 additions & 0 deletions internal/xds/translator/testdata/in/xds-ir/basic-auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ http:
basicAuth:
name: securitypolicy/default/policy-for-http-route-1
users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
userNameToHeader: x-basic-auth-user
- name: httproute/default/httproute-1/rule/1/match/0/www_foo_com
backendWeights:
hostname: www.foo.com
Expand All @@ -55,6 +56,7 @@ http:
basicAuth:
name: securitypolicy/default/policy-for-http-route-1
users: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
userNameToHeader: x-basic-auth-user
- name: httproute/default/httproute-2/rule/0/match/0/www_bar_com
hostname: www.bar.com
isHTTP2: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
name: envoy.filters.http.basic_auth
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
forwardUsernameHeader: x-basic-auth-user
users:
inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=
- name: envoy.filters.http.router
Expand Down
1 change: 1 addition & 0 deletions site/content/en/latest/api/extension_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ _Appears in:_
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `users` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the username-password pairs in<br />htpasswd format, used to verify user credentials in the "Authorization"<br />header.<br /><br />This is an Opaque secret. The username-password pairs should be stored in<br />the key ".htpasswd". As the key name indicates, the value needs to be the<br />htpasswd format, for example: "user1:{SHA}hashed_user1_password".<br />Right now, only SHA hash algorithm is supported.<br />Reference to https://httpd.apache.org/docs/2.4/programs/htpasswd.html<br />for more details.<br /><br />Note: The secret must be in the same namespace as the SecurityPolicy. |
| `userNameToHeader` | _[HeaderName](#headername)_ | false | The name of the HTTP header that will be used to forward the username to the upstream server.<br />Please note that the header name is a global setting for all the routes in a Gateway.<br />If multiple routes have different header names, a random one will be chosen. |


#### BootstrapType
Expand Down
1 change: 1 addition & 0 deletions test/e2e/testdata/basic-auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ spec:
basicAuth:
users:
name: "basic-auth-users-secret-1"
userNameToHeader: x-basic-auth-user
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/tests/basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ var BasicAuthTest = suite.ConformanceTest{
"Authorization": "Basic dXNlcjE6dGVzdDE=", // user1:test1
},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Path: "/basic-auth-1",
Headers: map[string]string{
"x-basic-auth-user": "user1",
},
},
},
Response: http.Response{
StatusCode: 200,
},
Expand Down