diff --git a/internal/authorization/rbac/rbac.go b/internal/authorization/rbac/rbac.go index 383273c..18187e5 100644 --- a/internal/authorization/rbac/rbac.go +++ b/internal/authorization/rbac/rbac.go @@ -22,7 +22,7 @@ const ( // This creates a higher guarantee that your informer’s store has a perfect picture of the resources it is watching. // There are situations where events can be missed entirely and resyncing every so often solves this. // Setting to 0 disables the resync and makes the informer subscribe to individual updates only. - defaultResyncDuration = time.Minute * 10 + defaultResyncDuration = time.Minute * 10 ) // Logger is an interface for basic log output diff --git a/internal/configuration/config.go b/internal/configuration/config.go index d4332bb..a4aaf72 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -46,7 +46,7 @@ type Config struct { UserCookieName string `long:"user-cookie-name" env:"USER_COOKIE_NAME" default:"_forward_auth_name" description:"User Cookie Name"` CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"` ClaimsSessionName string `long:"claims-session-name" env:"CLAIMS_SESSION_NAME" default:"_forward_auth_claims" description:"Name of the claims session"` - DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"` + DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" choice:"allow-with-auth" description:"Default action"` Domains CommaSeparatedList `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"` LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"` Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"` @@ -60,6 +60,7 @@ type Config struct { GroupClaimPrefix string `long:"group-claim-prefix" env:"GROUP_CLAIM_PREFIX" default:"oidc:" description:"prefix oidc group claims with this value"` EncryptionKeyString string `long:"encryption-key" env:"ENCRYPTION_KEY" description:"Encryption key used to encrypt the cookie (required)" json:"-"` GroupsAttributeName string `long:"groups-attribute-name" env:"GROUPS_ATTRIBUTE_NAME" default:"groups" description:"Map the correct attribute that contain the user groups"` + ForwardHeaders string `long:"forward-headers" env:"FORWARD_HEADERS" default:"" description:"Headers to forward to the upstream, separated by a comma. For example: X-CSRF-Token,X-Requested-With"` // RBAC EnableRBAC bool `long:"enable-rbac" env:"ENABLE_RBAC" description:"Indicates that RBAC support should be enabled"` @@ -294,8 +295,8 @@ func (r *Rule) FormattedRule() string { // Validate validates the rule func (r *Rule) Validate() { - if r.Action != "auth" && r.Action != "allow" { - log.Fatal("invalid rule action, must be \"auth\" or \"allow\"") + if r.Action != "auth" && r.Action != "allow" && r.Action != "allow-with-auth" { + log.Fatal("invalid rule action, must be \"auth\", \"allow\" or \"allow-with-auth\"") } } diff --git a/internal/handlers/server.go b/internal/handlers/server.go index d3f4cc1..bad919c 100644 --- a/internal/handlers/server.go +++ b/internal/handlers/server.go @@ -66,9 +66,12 @@ func (s *Server) buildRoutes() { // Let's build a router for name, rule := range s.config.Rules { var err error - if rule.Action == "allow" { + switch rule.Action { + case "allow": err = s.router.AddRoute(rule.FormattedRule(), 1, s.AllowHandler(name)) - } else { + case "allow-with-auth": + err = s.router.AddRoute(rule.FormattedRule(), 1, s.AllowWithAuthHandler(name)) + default: err = s.router.AddRoute(rule.FormattedRule(), 1, s.AuthHandler(name)) } if err != nil { @@ -80,9 +83,12 @@ func (s *Server) buildRoutes() { s.router.Handle(s.config.Path, s.AuthCallbackHandler()) // Add a default handler - if s.config.DefaultAction == "allow" { + switch s.config.DefaultAction { + case "allow": s.router.NewRoute().Handler(s.AllowHandler("default")) - } else { + case "allow-with-auth": + s.router.NewRoute().Handler(s.AllowWithAuthHandler("default")) + default: s.router.NewRoute().Handler(s.AuthHandler("default")) } } @@ -228,6 +234,15 @@ func (s *Server) AuthHandler(rule string) http.HandlerFunc { w.Header().Add(s.config.ForwardTokenHeaderName, s.config.ForwardTokenPrefix+id.Token) } + // Get all headers that we want to forward from the original request + // by reading the config + headers := strings.Split(s.config.ForwardHeaders, ",") + + // Forward headers + for _, header := range headers { + w.Header().Add(header, r.Header.Get(header)) + } + w.WriteHeader(200) } } @@ -515,3 +530,67 @@ func (s *Server) makeKubeUserInfo(email string, groups []string) authorization.U Groups: g, } } + +// AllowWithAuthHandler handles the request as "allow-with-auth", allowing the request through +// but forwarding authentication headers if the user is authenticated +func (s *Server) AllowWithAuthHandler(rule string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger := s.logger(r, rule, "Allow with auth request") + + // Get auth cookie + c, err := r.Cookie(s.config.CookieName) + if err != nil { + // Not authenticated, just return 200 without forwarding headers + w.WriteHeader(200) + return + } + + // Validate cookie + id, err := s.authenticator.ValidateCookie(r, c) + if err != nil { + // Invalid cookie, just return 200 without forwarding headers + logger.Info(fmt.Sprintf("cookie validation failure: %s", err.Error())) + w.WriteHeader(200) + return + } + + // Validate user + valid := s.authenticator.ValidateEmail(id.Email) + if !valid { + // Invalid user, just return 200 without forwarding headers + logger.WithFields(logrus.Fields{ + "email": id.Email, + }).Errorf("Invalid email") + w.WriteHeader(200) + return + } + + // User is authenticated, forward headers + for _, headerName := range s.config.EmailHeaderNames { + w.Header().Set(headerName, id.Email) + } + + if s.config.EnableImpersonation { + // Set impersonation headers + logger.Debug(fmt.Sprintf("setting authorization token and impersonation headers: email: %s", id.Email)) + w.Header().Set("Authorization", fmt.Sprintf("Bearer %s", s.config.ServiceAccountToken)) + w.Header().Set(impersonateUserHeader, id.Email) + w.Header().Set(impersonateGroupHeader, "system:authenticated") + w.Header().Set("Connection", cleanupConnectionHeader(w.Header().Get("Connection"))) + } + + if s.config.ForwardTokenHeaderName != "" && id.Token != "" { + w.Header().Add(s.config.ForwardTokenHeaderName, s.config.ForwardTokenPrefix+id.Token) + } + + // Get all headers that we want to forward from the original request + headers := strings.Split(s.config.ForwardHeaders, ",") + + // Forward headers + for _, header := range headers { + w.Header().Add(header, r.Header.Get(header)) + } + + w.WriteHeader(200) + } +}