authelia/internal/handlers/handler_authz_grpc.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")
}