123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package httpsecurity
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
)
|
|
|
|
type Protection struct {
|
|
resolver *ClientIPResolver
|
|
allow cidrMatcher
|
|
allowAll bool
|
|
authentication authenticator
|
|
clientMode string
|
|
semantics Semantics
|
|
admitter Admitter
|
|
}
|
|
|
|
func New(config Config, admitter Admitter) (*Protection, error) {
|
|
if config.Semantics != APIAuthSemantics && config.Semantics != ProxySemantics {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
resolver, err := NewClientIPResolver(config.TrustedProxies)
|
|
if err != nil {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
allow, err := newCIDRMatcher(config.AllowCIDRs)
|
|
if err != nil {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
authentication, err := buildAuthenticator(config.Authentication, config.Semantics)
|
|
if err != nil {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
clientMode := config.ClientIdentification
|
|
if clientMode == "" {
|
|
clientMode = ClientSourceIP
|
|
}
|
|
if clientMode != ClientSourceIP && clientMode != ClientAuthenticated && clientMode != ClientAuthenticatedOrSourceIP {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
if clientMode == ClientAuthenticated && (config.Authentication.Mode == "" || config.Authentication.Mode == ModeNone) {
|
|
return nil, ErrInvalidConfig
|
|
}
|
|
return &Protection{
|
|
resolver: resolver, allow: allow, allowAll: len(config.AllowCIDRs) == 0,
|
|
authentication: authentication, clientMode: clientMode, semantics: config.Semantics,
|
|
admitter: admitter,
|
|
}, nil
|
|
}
|
|
|
|
func (protection *Protection) Resolve(request *http.Request) (Identity, error) {
|
|
if request == nil {
|
|
return Identity{}, newHTTPError(http.StatusBadRequest, "INVALID_SOURCE", nil, errors.New("request is required"))
|
|
}
|
|
return protection.evaluate(request.Context(), request)
|
|
}
|
|
|
|
func (protection *Protection) Check(ctx context.Context, request *http.Request) error {
|
|
_, err := protection.evaluate(ctx, request)
|
|
return err
|
|
}
|
|
|
|
func (protection *Protection) evaluate(ctx context.Context, request *http.Request) (Identity, error) {
|
|
if protection == nil || protection.resolver == nil || protection.authentication == nil || request == nil {
|
|
return Identity{}, newHTTPError(http.StatusInternalServerError, "SECURITY_NOT_CONFIGURED", nil, ErrInvalidConfig)
|
|
}
|
|
address, err := protection.resolver.Resolve(request)
|
|
if err != nil {
|
|
return Identity{}, newHTTPError(http.StatusBadRequest, "INVALID_SOURCE", nil, err)
|
|
}
|
|
if !protection.allowAll && !protection.allow.match(address) {
|
|
return Identity{}, newHTTPError(http.StatusForbidden, "FORBIDDEN", nil, errSourceRejected)
|
|
}
|
|
source := address.String()
|
|
principal, err := protection.authentication.authenticate(request, source)
|
|
if err != nil {
|
|
if errors.Is(err, errSourceRejected) {
|
|
return Identity{}, newHTTPError(http.StatusForbidden, "FORBIDDEN", nil, err)
|
|
}
|
|
return Identity{}, protection.unauthorized(err)
|
|
}
|
|
identity := Identity{SourceIP: source}
|
|
switch protection.clientMode {
|
|
case ClientSourceIP:
|
|
identity.ClientID = "source:" + source
|
|
case ClientAuthenticated:
|
|
if principal == "" {
|
|
return Identity{}, protection.unauthorized(errCredentialRejected)
|
|
}
|
|
identity.ClientID = principal
|
|
case ClientAuthenticatedOrSourceIP:
|
|
identity.ClientID = principal
|
|
if identity.ClientID == "" {
|
|
identity.ClientID = "source:" + source
|
|
}
|
|
}
|
|
if protection.admitter != nil {
|
|
if err := protection.admitter.Admit(ctx, identity.ClientID); err != nil {
|
|
return Identity{}, newHTTPError(http.StatusTooManyRequests, "RATE_LIMITED", nil, err)
|
|
}
|
|
}
|
|
return identity, nil
|
|
}
|
|
|
|
func (protection *Protection) unauthorized(cause error) *HTTPError {
|
|
status := http.StatusUnauthorized
|
|
headerName := "WWW-Authenticate"
|
|
if protection.semantics == ProxySemantics {
|
|
status = http.StatusProxyAuthRequired
|
|
headerName = "Proxy-Authenticate"
|
|
}
|
|
header := make(http.Header)
|
|
for _, challenge := range protection.authentication.challenges() {
|
|
header.Add(headerName, challenge)
|
|
}
|
|
return newHTTPError(status, "UNAUTHORIZED", header, cause)
|
|
}
|
|
|
|
func newHTTPError(status int, code string, header http.Header, cause error) *HTTPError {
|
|
return &HTTPError{StatusCode: status, Code: code, Header: header, Cause: cause}
|
|
}
|