411 lines
14 KiB
Go
411 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/authelia/authelia/v4/internal/authorization"
|
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
|
"github.com/authelia/authelia/v4/internal/session"
|
|
"github.com/authelia/authelia/v4/internal/utils"
|
|
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
autha "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
|
|
envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
|
"github.com/gogo/googleapis/google/rpc"
|
|
"github.com/valyala/fasthttp"
|
|
rpcstatus "google.golang.org/genproto/googleapis/rpc/status"
|
|
"google.golang.org/grpc/codes"
|
|
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"context"
|
|
)
|
|
|
|
type AuthzGRCP struct {
|
|
Config *schema.Configuration
|
|
Providers middlewares.Providers
|
|
AuthStrategies []AuthnStrategy
|
|
Authz Authz
|
|
}
|
|
|
|
func NewAuthzGRCP(config *schema.Configuration, providers middlewares.Providers) *AuthzGRCP {
|
|
|
|
// Determine the refresh interval
|
|
authBuilder := NewAuthzBuilder().WithConfig(config)
|
|
|
|
// Only the following strategies are supported. These are hardcoded at the moment and won't be taken from the configuration
|
|
strategies := []AuthnStrategy{NewHeaderProxyAuthorizationAuthnStrategy() /* NewHeaderAuthorizationAuthnStrategy(), */, NewCookieSessionAuthnStrategy(authBuilder.config.RefreshInterval)}
|
|
|
|
return &AuthzGRCP{
|
|
Config: config,
|
|
Providers: providers,
|
|
AuthStrategies: strategies,
|
|
Authz: Authz{strategies: strategies},
|
|
}
|
|
}
|
|
|
|
type GRCPRequestData struct {
|
|
RemoteHost string
|
|
Domain string
|
|
Method string
|
|
Protocol string
|
|
Path string
|
|
AutheliaURL string
|
|
}
|
|
|
|
func (data *GRCPRequestData) String() string {
|
|
return fmt.Sprintf(
|
|
`Extracted data from headers:
|
|
Remote Host = %s
|
|
Domain = %s
|
|
Method = %s
|
|
Protocol = %s
|
|
Path = %s
|
|
AutheliaURL = %s
|
|
`, data.RemoteHost, data.Domain, data.Method, data.Protocol, data.Path, data.AutheliaURL)
|
|
}
|
|
|
|
// GetRequestURI returns a request URI from the provided Data inside this struct.
|
|
// It returns an error if values are missing
|
|
func (data *GRCPRequestData) GetRequestURI() (*url.URL, error) {
|
|
if len(data.Protocol) == 0 || len(data.Domain) == 0 || len(data.Path) == 0 || len(data.Method) == 0 {
|
|
return nil, fmt.Errorf("missing required values for URI. One of 'Protocol', 'Domain', 'Path' or 'Method' was not found")
|
|
}
|
|
|
|
return url.ParseRequestURI(fmt.Sprintf("%s://%s%s", data.Protocol, data.Domain, data.Path))
|
|
}
|
|
|
|
// GetAutheliaURI returns a URI for the authelia protal from the provided Data inside this struct.
|
|
func (data *GRCPRequestData) GetAutheliaURI(provider *session.Session) (*url.URL, error) {
|
|
if len(data.AutheliaURL) == 0 {
|
|
return nil, fmt.Errorf("missing required 'AutheliaURL' for request")
|
|
}
|
|
|
|
// Parse the URL
|
|
uri, err := url.ParseRequestURI(data.AutheliaURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate URL
|
|
if !utils.HasURIDomainSuffix(uri, provider.Config.Domain) {
|
|
return nil, fmt.Errorf("authelia url '%s' is not valid for detected domain '%s' as the url does not have the domain as a suffix", data.AutheliaURL, provider.Config.Domain)
|
|
}
|
|
|
|
return uri, nil
|
|
}
|
|
|
|
// HandleGRPC is the authentication handler for envoy proxies via the gRPC protocoll.
|
|
// It is oriented on handler_authz.go
|
|
func (authz *AuthzGRCP) Check(goCtx context.Context, req *autha.CheckRequest) (*autha.CheckResponse, error) {
|
|
request, data := authz.GetHttpCtxFromGRPC(req)
|
|
ctx := middlewares.NewAutheliaCtx(request, *authz.Config, authz.Providers)
|
|
|
|
// Log the parsed data and the inbound headers
|
|
ctx.Logger.Debug(data)
|
|
b, err := json.MarshalIndent(req.Attributes.Request.Http.Headers, "", " ")
|
|
if err == nil {
|
|
ctx.Logger.Trace("Inbound Headers: ")
|
|
ctx.Logger.Trace((string(b)))
|
|
}
|
|
|
|
// Get request URI
|
|
uri, err := data.GetRequestURI()
|
|
if err != nil {
|
|
ctx.Logger.WithError(err).Error("Error getting Target URL and Request Method")
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad authentication request"), nil
|
|
}
|
|
|
|
// Get authentication object
|
|
object := authorization.NewObject(uri, data.Method)
|
|
if !utils.IsURISecure(object.URL) {
|
|
// Check for redirect
|
|
if !ctx.Configuration.Server.DisableAutoHttpsRedirect {
|
|
// Redirect to https
|
|
return authz.AuthResponseHeader(envoy_type.StatusCode_PermanentRedirect, "", []*core.HeaderValueOption{
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Location",
|
|
Value: fmt.Sprintf("https://%s%s", data.Domain, data.Path),
|
|
},
|
|
},
|
|
}), nil
|
|
}
|
|
ctx.Logger.Errorf("Target URL '%s' has an insecure scheme '%s', only the 'https' and 'wss' schemes are supported so session cookies can be transmitted securely", object.URL.String(), object.URL.Scheme)
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad protocol for authentication request"), nil
|
|
}
|
|
|
|
// Get provider
|
|
provider, err := ctx.GetSessionProviderByTargetURL(object.URL)
|
|
if err != nil {
|
|
ctx.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Target URL does not appear to have a relevant session cookies configuration")
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad domain for authentication request"), nil
|
|
}
|
|
|
|
// Get authelia url
|
|
if len(data.AutheliaURL) == 0 {
|
|
ctx.Logger.Info("Received no authelia URL from headers. Using the default of https://auth.rpjosh.de")
|
|
data.AutheliaURL = "https://auth.rpjosh.de"
|
|
}
|
|
autheliaURI, err := data.GetAutheliaURI(provider)
|
|
if err != nil {
|
|
ctx.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Error occurred trying to determine the external Authelia URL for Target URL")
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad authentication request"), nil
|
|
}
|
|
ctx.Logger.Trace("Using authelia URL " + autheliaURI.String())
|
|
|
|
var (
|
|
authn Authn
|
|
strategy AuthnStrategy
|
|
)
|
|
|
|
// Get strategie
|
|
if authn, strategy, err = authz.Authz.authn(ctx, provider); err != nil {
|
|
authn.Object = object
|
|
|
|
ctx.Logger.WithError(err).Error("Error occurred while attempting to authenticate a request")
|
|
|
|
switch strategy {
|
|
case nil:
|
|
ctx.Logger.Trace("Received no strategy to use")
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_Unauthorized, "Unauthorized"), nil
|
|
default:
|
|
// Because the response is modified directly, we have to catch this and rewrite this function
|
|
if s, ok := strategy.(*HeaderAuthnStrategy); ok {
|
|
ctx.Logger.Trace("Rewriting HeaderAuthnStrategy")
|
|
ctx.Logger.Debugf("Responding %d %s", s.authn, autheliaURI)
|
|
|
|
headers := make([]*core.HeaderValueOption, 0)
|
|
if s.headerAuthenticate != nil {
|
|
headers = append(headers, &core.HeaderValueOption{
|
|
Header: &core.HeaderValue{
|
|
Key: string(s.headerAuthenticate),
|
|
Value: string(headerValueAuthenticateBasic),
|
|
},
|
|
})
|
|
}
|
|
|
|
return authz.AuthResponseHeader(envoy_type.StatusCode(s.statusAuthenticate), "", headers), nil
|
|
} else if _, ok := strategy.(*CookieSessionAuthnStrategy); ok {
|
|
ctx.Logger.Trace("Rewriting CookieSessionAuthnStrategy")
|
|
|
|
}
|
|
|
|
ctx.Logger.Error("Received unsupported auth strategy for gRPC server")
|
|
// strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI))
|
|
}
|
|
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_Unauthorized, "Unauthorized"), nil
|
|
}
|
|
|
|
authn.Object = object
|
|
authn.Method = friendlyMethod(authn.Object.Method)
|
|
|
|
ruleHasSubject, required := ctx.Providers.Authorizer.GetRequiredLevel(
|
|
authorization.Subject{
|
|
Username: authn.Details.Username,
|
|
Groups: authn.Details.Groups,
|
|
IP: net.ParseIP(data.RemoteHost),
|
|
},
|
|
object,
|
|
)
|
|
|
|
switch isAuthzResult(authn.Level, required, ruleHasSubject) {
|
|
case AuthzResultForbidden:
|
|
ctx.Logger.Infof("Access to '%s' is forbidden to user '%s'", object.URL.String(), authn.Username)
|
|
return authz.ErrAuthResponse(envoy_type.StatusCode_Forbidden, "Forbidden"), nil
|
|
case AuthzResultUnauthorized:
|
|
if strategy != nil {
|
|
ctx.Logger.Error("Handling not supported handler")
|
|
//strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI))
|
|
} else {
|
|
ctx.Logger.Debugf("Redirecting user")
|
|
return authz.HandleUnauthorizedRedirect(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)), nil
|
|
}
|
|
case AuthzResultAuthorized:
|
|
ctx.Logger.Debugf("Authorized request")
|
|
return authz.HandleAuthorized(ctx, &authn), nil
|
|
//authz.Authz.handleAuthorized(ctx, &authn)
|
|
}
|
|
|
|
ctx.Logger.Info("Handling default redirection")
|
|
return authz.HandleUnauthorizedRedirect(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)), nil
|
|
}
|
|
|
|
// GetHttpCtxFromGRPC is an Adapter that converts common fields between a grpc request
|
|
// a "normal" http request.
|
|
// It also returns the
|
|
func (authz *AuthzGRCP) GetHttpCtxFromGRPC(req *autha.CheckRequest) (*fasthttp.RequestCtx, *GRCPRequestData) {
|
|
|
|
// Extract headers
|
|
headers := req.Attributes.Request.Http.Headers
|
|
|
|
// Parse the header data
|
|
data := &GRCPRequestData{
|
|
RemoteHost: headers["x-forwarded-for"],
|
|
Domain: headers[":authority"],
|
|
Method: headers[":method"],
|
|
Protocol: headers["x-forwarded-proto"],
|
|
Path: headers[":path"],
|
|
AutheliaURL: headers[""],
|
|
}
|
|
|
|
// Build fasthttp request with common types
|
|
rtc := &fasthttp.RequestCtx{}
|
|
|
|
// General headers
|
|
rtc.Request.Header.Set(fasthttp.HeaderXForwardedFor, data.RemoteHost)
|
|
|
|
// Needed for NewHeaderProxyAuthorizationAuthnStrategy and NewHeaderAuthorizationAuthnStrategy
|
|
authz.setHeaderIfSet(fasthttp.HeaderAuthorization, rtc, &headers)
|
|
authz.setHeaderIfSet(fasthttp.HeaderProxyAuthorization, rtc, &headers)
|
|
authz.setHeaderIfSet(fasthttp.HeaderWWWAuthenticate, rtc, &headers)
|
|
authz.setHeaderIfSet(fasthttp.HeaderProxyAuthenticate, rtc, &headers)
|
|
|
|
// Needed for CookieSesseionauthnStrategy
|
|
rtc.Request.Header.Set("cookie", headers["cookie"])
|
|
|
|
return rtc, data
|
|
}
|
|
|
|
// setHeaderIfSet sets the header in the given fastHttp request if the header from the envoy authentication
|
|
// request was also set
|
|
func (authz *AuthzGRCP) setHeaderIfSet(headerKeyFast string, rtc *fasthttp.RequestCtx, envoyHeaders *map[string]string) {
|
|
// Envoys provided header keys are always lower case
|
|
envoyHeaderKey := strings.ToLower(headerKeyFast)
|
|
|
|
if val, isSet := (*envoyHeaders)[envoyHeaderKey]; isSet {
|
|
rtc.Request.Header.Set(headerKeyFast, val)
|
|
}
|
|
}
|
|
|
|
// ErrAuthResponse returns an authentication error for envoy with the given status code
|
|
// and the given text body
|
|
func (authz *AuthzGRCP) ErrAuthResponse(statuscode envoy_type.StatusCode, body string) *autha.CheckResponse {
|
|
return &autha.CheckResponse{
|
|
Status: &rpcstatus.Status{
|
|
Code: int32(rpc.UNAUTHENTICATED),
|
|
},
|
|
HttpResponse: &autha.CheckResponse_DeniedResponse{
|
|
DeniedResponse: &autha.DeniedHttpResponse{
|
|
Status: &envoy_type.HttpStatus{
|
|
Code: statuscode,
|
|
},
|
|
Body: body,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ErrAuthResponse returns an authentication error for envoy with the given status code
|
|
// and the given text body
|
|
func (authz *AuthzGRCP) AuthResponseHeader(statuscode envoy_type.StatusCode, body string, headers []*core.HeaderValueOption) *autha.CheckResponse {
|
|
return &autha.CheckResponse{
|
|
Status: &rpcstatus.Status{
|
|
Code: int32(rpc.UNAUTHENTICATED),
|
|
},
|
|
HttpResponse: &autha.CheckResponse_DeniedResponse{
|
|
DeniedResponse: &autha.DeniedHttpResponse{
|
|
Status: &envoy_type.HttpStatus{
|
|
Code: statuscode,
|
|
},
|
|
Body: body,
|
|
Headers: headers,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (authz *AuthzGRCP) HandleAuthorized(ctx *middlewares.AutheliaCtx, authn *Authn) *autha.CheckResponse {
|
|
return &autha.CheckResponse{
|
|
Status: &rpcstatus.Status{
|
|
Code: int32(rpc.OK),
|
|
},
|
|
HttpResponse: &autha.CheckResponse_OkResponse{
|
|
OkResponse: &autha.OkHttpResponse{
|
|
Headers: []*core.HeaderValueOption{
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Auth-Handler",
|
|
Value: getAuthType(authn.Type),
|
|
},
|
|
},
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Auth-Username",
|
|
Value: authn.Username,
|
|
},
|
|
},
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Auth-Username-Display",
|
|
Value: authn.Details.DisplayName,
|
|
},
|
|
},
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Auth-Realm",
|
|
Value: authn.Object.Domain,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (authz *AuthzGRCP) HandleUnauthorizedRedirect(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) *autha.CheckResponse {
|
|
return &autha.CheckResponse{
|
|
Status: &rpcstatus.Status{
|
|
Code: int32(rpc.UNAUTHENTICATED),
|
|
},
|
|
HttpResponse: &autha.CheckResponse_DeniedResponse{
|
|
DeniedResponse: &autha.DeniedHttpResponse{
|
|
Status: &envoy_type.HttpStatus{
|
|
Code: 302,
|
|
},
|
|
Headers: []*core.HeaderValueOption{
|
|
{
|
|
Header: &core.HeaderValue{
|
|
Key: "Location",
|
|
Value: redirectionURL.String(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func getAuthType(t AuthnType) string {
|
|
|
|
switch t {
|
|
case AuthnTypeNone:
|
|
return "none"
|
|
case AuthnTypeCookie:
|
|
return "cookie"
|
|
case AuthnTypeProxyAuthorization:
|
|
return "proxy"
|
|
case AuthnTypeAuthorization:
|
|
return "header"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// HealthEndpoint implements a health function to check if the appliction is
|
|
// still alive
|
|
type HealthEndpoint struct{}
|
|
|
|
func (s *HealthEndpoint) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
|
|
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
|
|
}
|
|
|
|
func (s *HealthEndpoint) Watch(in *healthpb.HealthCheckRequest, srv healthpb.Health_WatchServer) error {
|
|
return status.Error(codes.Unimplemented, "Watch is not implemented")
|
|
}
|