feat: allow bypassing of basic auth from a list of authorized cidrs (#50)
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good

This commit is contained in:
2025-08-05 16:24:41 +02:00
parent 9d10a69b0d
commit a50f926463
7 changed files with 208 additions and 56 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/cidr"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
@ -23,6 +24,16 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
return nil, errors.WithStack(err)
}
matches, err := cidr.MatchAny(r.RemoteAddr, options.AuthorizedCIDRs...)
if err != nil {
return nil, errors.WithStack(err)
}
if matches {
user := authn.NewUser(r.RemoteAddr, map[string]any{})
return user, nil
}
username, password, ok := r.BasicAuth()
unauthorized := func() {

View File

@ -0,0 +1,130 @@
package basic
import (
"encoding/base64"
"net/http/httptest"
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors"
)
func TestAuthenticatorWithCredentials(t *testing.T) {
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", "Basic "+basicAuth("foo", "bar"))
authenticator := &Authenticator{}
layer := &store.Layer{
LayerHeader: store.LayerHeader{
Proxy: "test",
Name: "test",
Revision: 0,
Type: LayerType,
Enabled: true,
},
Options: store.LayerOptions{
"users": []map[string]any{
{
"username": "foo",
"passwordHash": "$2y$10$S3CfWRRMbOrOu3zUapZnfeU8xLtjH.MycWcvMRVHdc9RAty8lnn5q",
},
},
},
}
user, err := authenticator.Authenticate(w, r, layer)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if user == nil {
t.Fatalf("user should not be nil")
}
if e, g := "foo", user.Subject; e != g {
t.Fatalf("user.Subject: expected '%v', got '%v'", e, g)
}
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
r.Header.Set("Authorization", "Basic "+basicAuth("foo", "qsdq;sdqks"))
user, err = authenticator.Authenticate(w, r, layer)
if err == nil {
t.Errorf("err should not be nil")
}
if !errors.Is(err, authn.ErrSkipRequest) {
t.Errorf("err: expected %T, got %T", authn.ErrSkipRequest, err)
}
if user != nil {
t.Errorf("user should be nil")
}
}
func TestAuthenticatorWithAuthorizedRemoteAddr(t *testing.T) {
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
// Authorized address
r.RemoteAddr = "192.168.30.21"
authenticator := &Authenticator{}
layer := &store.Layer{
LayerHeader: store.LayerHeader{
Proxy: "test",
Name: "test",
Revision: 0,
Type: LayerType,
Enabled: true,
},
Options: store.LayerOptions{
"users": []map[string]any{},
"authorizedCIDRs": []string{"192.168.30.1/24"},
},
}
user, err := authenticator.Authenticate(w, r, layer)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if user == nil {
t.Fatalf("user should not be nil")
}
if e, g := "192.168.30.21", user.Subject; e != g {
t.Fatalf("user.Subject: expected '%v', got '%v'", e, g)
}
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
// Unauthorized address
r.RemoteAddr = "192.168.40.36"
user, err = authenticator.Authenticate(w, r, layer)
if err == nil {
t.Errorf("err should not be nil")
}
if !errors.Is(err, authn.ErrSkipRequest) {
t.Errorf("err: expected %T, got %T", authn.ErrSkipRequest, err)
}
if user != nil {
t.Errorf("user should be nil")
}
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}

View File

@ -34,6 +34,14 @@
],
"additionalProperties": false
}
},
"authorizedCIDRs": {
"title": "Liste des adresses réseau d'origine autorisées à contourner l'authentification (au format CIDR)",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false

View File

@ -9,8 +9,9 @@ import (
type LayerOptions struct {
authn.LayerOptions
Users []User `mapstructure:"users"`
Realm string `mapstructure:"realm"`
Users []User `mapstructure:"users"`
Realm string `mapstructure:"realm"`
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
}
type User struct {
@ -21,9 +22,10 @@ type User struct {
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
layerOptions := LayerOptions{
LayerOptions: authn.DefaultLayerOptions(),
Realm: "Restricted area",
Users: make([]User, 0),
LayerOptions: authn.DefaultLayerOptions(),
Realm: "Restricted area",
Users: make([]User, 0),
AuthorizedCIDRs: make([]string, 0),
}
config := mapstructure.DecoderConfig{