Compare commits
2 Commits
v2025.3.19
...
v2025.8.5-
Author | SHA1 | Date | |
---|---|---|---|
a50f926463 | |||
9d10a69b0d |
12
Dockerfile
12
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM reg.cadoles.com/proxy_cache/library/golang:1.23 AS BUILD
|
||||
FROM reg.cadoles.com/proxy_cache/library/golang:1.24.2 AS build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y make
|
||||
@ -33,7 +33,7 @@ RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bounc
|
||||
&& yq -i '.bootstrap.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.integrations.kubernetes.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml
|
||||
|
||||
FROM reg.cadoles.com/proxy_cache/library/alpine:3.20 AS RUNTIME
|
||||
FROM reg.cadoles.com/proxy_cache/library/alpine:3.21 AS runtime
|
||||
|
||||
RUN apk add --no-cache ca-certificates dumb-init
|
||||
|
||||
@ -41,10 +41,10 @@ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
|
||||
COPY --from=BUILD /src/layers /usr/share/bouncer/layers
|
||||
COPY --from=BUILD /src/templates /usr/share/bouncer/templates
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml
|
||||
COPY --from=build /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
|
||||
COPY --from=build /src/layers /usr/share/bouncer/layers
|
||||
COPY --from=build /src/templates /usr/share/bouncer/templates
|
||||
COPY --from=build /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml
|
||||
|
||||
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer
|
||||
|
||||
|
42
internal/cidr/match.go
Normal file
42
internal/cidr/match.go
Normal file
@ -0,0 +1,42 @@
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func MatchAny(hostPort string, CIDRs ...string) (bool, error) {
|
||||
var remoteHost string
|
||||
if strings.Contains(hostPort, ":") {
|
||||
var err error
|
||||
remoteHost, _, err = net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
remoteHost = hostPort
|
||||
}
|
||||
|
||||
remoteAddr := net.ParseIP(remoteHost)
|
||||
if remoteAddr == nil {
|
||||
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
|
||||
}
|
||||
|
||||
for _, rawCIDR := range CIDRs {
|
||||
_, net, err := net.ParseCIDR(rawCIDR)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
match := net.Contains(remoteAddr)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
@ -1,15 +1,13 @@
|
||||
package network
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestMatchAuthorizedCIDRs(t *testing.T) {
|
||||
|
||||
func TestMatchAny(t *testing.T) {
|
||||
type testCase struct {
|
||||
RemoteHostPort string
|
||||
AuthorizedCIDRs []string
|
||||
@ -56,14 +54,16 @@ func TestMatchAuthorizedCIDRs(t *testing.T) {
|
||||
},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
RemoteHostPort: "[2001:0db8:0000:85a3:0000:0000:ac1f:8001]:8001",
|
||||
AuthorizedCIDRs: []string{"2000::/3"},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
auth := Authenticator{}
|
||||
ctx := context.Background()
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Case #%d", idx), func(t *testing.T) {
|
||||
result, err := auth.matchAnyAuthorizedCIDRs(ctx, tc.RemoteHostPort, tc.AuthorizedCIDRs)
|
||||
result, err := MatchAny(tc.RemoteHostPort, tc.AuthorizedCIDRs...)
|
||||
|
||||
if g, e := result, tc.ExpectedResult; e != g {
|
||||
t.Errorf("result: expected '%v', got '%v'", e, g)
|
@ -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() {
|
||||
|
130
internal/proxy/director/layer/authn/basic/authenticator_test.go
Normal file
130
internal/proxy/director/layer/authn/basic/authenticator_test.go
Normal 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))
|
||||
}
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -1,16 +1,13 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
@ -18,14 +15,12 @@ type Authenticator struct {
|
||||
|
||||
// Authenticate implements authn.Authenticator.
|
||||
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
matches, err := a.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
|
||||
matches, err := cidr.MatchAny(r.RemoteAddr, options.AuthorizedCIDRs...)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -49,42 +44,6 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
|
||||
var remoteHost string
|
||||
if strings.Contains(remoteHostPort, ":") {
|
||||
var err error
|
||||
remoteHost, _, err = net.SplitHostPort(remoteHostPort)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
remoteHost = remoteHostPort
|
||||
}
|
||||
|
||||
remoteAddr := net.ParseIP(remoteHost)
|
||||
if remoteAddr == nil {
|
||||
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
|
||||
}
|
||||
|
||||
for _, rawCIDR := range CIDRs {
|
||||
_, net, err := net.ParseCIDR(rawCIDR)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
match := net.Contains(remoteAddr)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ authn.Authenticator = &Authenticator{}
|
||||
)
|
||||
|
Reference in New Issue
Block a user