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() authentication, 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, Permissions: append([]string(nil), authentication.Permissions...), ClientPolicy: authentication.ClientPolicy.Clone(), } switch protection.clientMode { case ClientSourceIP: identity.ClientID = "source:" + source case ClientAuthenticated: if authentication.Principal == "" { return Identity{}, protection.unauthorized(errCredentialRejected) } identity.ClientID = authentication.Principal case ClientAuthenticatedOrSourceIP: identity.ClientID = authentication.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} }