diff --git a/backend/pkg/auth/handler.go b/backend/pkg/auth/handler.go index 4e910b1..2642f80 100644 --- a/backend/pkg/auth/handler.go +++ b/backend/pkg/auth/handler.go @@ -10,11 +10,12 @@ import ( ) type Handler struct { - basePath string + basePath string + groupClaim string } -func NewHandler(basePath string) *Handler { - return &Handler{basePath: basePath} +func NewHandler(basePath, groupClaim string) *Handler { + return &Handler{basePath: basePath, groupClaim: groupClaim} } func (h *Handler) Callback(ctx *gin.Context) { @@ -25,8 +26,11 @@ func (h *Handler) Callback(ctx *gin.Context) { return } + profile := NewProfile(user) + profile.AssignGroups(mapGroups(user, h.groupClaim)) + session := sessions.Default(ctx) - session.Set("profile", NewProfile(user)) + session.Set("profile", profile) if err := session.Save(); err != nil { zap.L().Error("failed to save session", zap.Error(err)) @@ -44,8 +48,11 @@ func (h *Handler) Login(ctx *gin.Context) { return } + profile := NewProfile(user) + profile.AssignGroups(mapGroups(user, h.groupClaim)) + session := sessions.Default(ctx) - session.Set("profile", NewProfile(user)) + session.Set("profile", profile) ctx.Redirect(http.StatusTemporaryRedirect, h.basePath) } diff --git a/backend/pkg/auth/helper.go b/backend/pkg/auth/helper.go new file mode 100644 index 0000000..ce52b39 --- /dev/null +++ b/backend/pkg/auth/helper.go @@ -0,0 +1,31 @@ +package auth + +import ( + "github.com/markbates/goth" +) + +func mapGroups(user goth.User, claim string) []string { + groups := make([]string, 0) + + if claim == "" { + return groups + } + + rawGroups, ok := user.RawData[claim] + if !ok { + return groups + } + + mapped, ok := rawGroups.([]any) + if !ok { + return groups + } + + for _, group := range mapped { + if g, ok := group.(string); ok { + groups = append(groups, g) + } + } + + return groups +} diff --git a/backend/pkg/auth/middleware.go b/backend/pkg/auth/middleware.go index a5a619e..974516d 100644 --- a/backend/pkg/auth/middleware.go +++ b/backend/pkg/auth/middleware.go @@ -26,7 +26,7 @@ func Provider(provider string) gin.HandlerFunc { } } -func Valid(basePath string) gin.HandlerFunc { +func Valid(basePath, groupClaim string) gin.HandlerFunc { return func(ctx *gin.Context) { providerName, err := gothic.GetProviderName(ctx.Request) if err != nil { @@ -71,7 +71,10 @@ func Valid(basePath string) gin.HandlerFunc { return } - session.Set("profile", NewProfile(user)) + newProfile := NewProfile(user) + newProfile.AssignGroups(mapGroups(user, groupClaim)) + + session.Set("profile", newProfile) if err := session.Save(); err != nil { zap.L().Error("failed to save profile session", zap.Error(err)) } diff --git a/backend/pkg/auth/model.go b/backend/pkg/auth/model.go index a0a6ff4..1f1c905 100644 --- a/backend/pkg/auth/model.go +++ b/backend/pkg/auth/model.go @@ -16,6 +16,7 @@ type Profile struct { Firstname string `json:"firstname"` Name string `json:"name"` Email string `json:"email"` + Groups []string `json:"groups"` RefreshToken string `json:"-"` AccessToken string `json:"-"` IDToken string `json:"-"` @@ -34,6 +35,10 @@ func (p *Profile) GetName() string { return p.Email } +func (p *Profile) AssignGroups(groups []string) { + p.Groups = groups +} + func NewProfile(user goth.User) Profile { return Profile{ ID: user.UserID, diff --git a/backend/pkg/auth/permissions.go b/backend/pkg/auth/permissions.go index 728b296..474677f 100644 --- a/backend/pkg/auth/permissions.go +++ b/backend/pkg/auth/permissions.go @@ -6,22 +6,33 @@ import ( "strings" "github.com/gin-gonic/gin" + + "github.com/kyverno/policy-reporter-ui/pkg/utils" ) type AccessControl struct { Emails []string + Groups []string } type Permissions struct { AccessControl AccessControl `json:"-"` } -func (p Permissions) AllowedEmail(email string) bool { - if len(p.AccessControl.Emails) == 0 { +func (p Permissions) Allowed(profile *Profile) bool { + if len(p.AccessControl.Emails) == 0 && len(p.AccessControl.Groups) == 0 { + return true + } + + if len(p.AccessControl.Emails) > 0 && slices.Contains(p.AccessControl.Emails, profile.Email) { + return true + } + + if len(p.AccessControl.Groups) > 0 && utils.Some(p.AccessControl.Groups, profile.Groups) { return true } - return slices.Contains(p.AccessControl.Emails, email) + return false } func ClusterPermissions(permissions map[string]Permissions) gin.HandlerFunc { @@ -38,7 +49,7 @@ func ClusterPermissions(permissions map[string]Permissions) gin.HandlerFunc { } if profile := ProfileFrom(ctx); profile != nil { - if !permissions[cluster].AllowedEmail(profile.Email) { + if !permissions[cluster].Allowed(profile) { ctx.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/backend/pkg/auth/router.go b/backend/pkg/auth/router.go index 4db1150..c834a1a 100644 --- a/backend/pkg/auth/router.go +++ b/backend/pkg/auth/router.go @@ -12,7 +12,7 @@ import ( const SessionKey = "auth-session" -func Setup(engine *gin.Engine, basePath, provider, tempDir string) { +func Setup(engine *gin.Engine, basePath, groupKey, provider, tempDir string) { gob.Register(Profile{}) gob.Register(map[string]any{}) @@ -28,7 +28,7 @@ func Setup(engine *gin.Engine, basePath, provider, tempDir string) { engine.Use(sessions.Sessions(SessionKey, NewStore(authStore))) - handler := NewHandler(basePath) + handler := NewHandler(basePath, groupKey) engine.GET("/login", Provider(provider), handler.Login) engine.GET("/logout", Provider(provider), handler.Logout) diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 1dfc8ad..fd91b9b 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -24,6 +24,7 @@ type OpenIDConnect struct { CallbackURL string `mapstructure:"callbackUrl"` ClientID string `mapstructure:"clientId"` ClientSecret string `mapstructure:"clientSecret"` + GroupClaim string `mapstructure:"groupClaim"` Scopes []string `mapstructure:"scopes"` } @@ -191,6 +192,7 @@ type Source struct { type AccessControl struct { Emails []string `mapstructure:"emails"` + Groups []string `mapstructure:"groups"` } type Boards struct { @@ -243,3 +245,11 @@ func (c *Config) AuthBasePath() string { return c.OpenIDConnect.BasePath() } + +func (c *Config) AuthGroupClaim() string { + if c.OAuth.Enabled { + return "" + } + + return c.OpenIDConnect.GroupClaim +} diff --git a/backend/pkg/config/mapper.go b/backend/pkg/config/mapper.go index 55b05f8..2e7574d 100644 --- a/backend/pkg/config/mapper.go +++ b/backend/pkg/config/mapper.go @@ -43,9 +43,7 @@ func MapConfig(c *Config) *api.Config { Banner: c.UI.Banner, Boards: api.Boards{ Permissions: auth.Permissions{ - AccessControl: auth.AccessControl{ - Emails: c.Boards.AccessControl.Emails, - }, + AccessControl: auth.AccessControl(c.Boards.AccessControl), }, }, Sources: utils.Map(c.Sources, func(s Source) api.Source { @@ -81,9 +79,7 @@ func MapCustomBoards(customBoards []CustomBoard) map[string]api.CustomBoard { List: c.Sources.List, }, Permissions: auth.Permissions{ - AccessControl: auth.AccessControl{ - Emails: c.AccessControl.Emails, - }, + AccessControl: auth.AccessControl(c.AccessControl), }, PolicyReports: api.PolicyReports{ Selector: c.PolicyReports.Selector, diff --git a/backend/pkg/config/resolver.go b/backend/pkg/config/resolver.go index 9746a87..634b8e9 100644 --- a/backend/pkg/config/resolver.go +++ b/backend/pkg/config/resolver.go @@ -248,7 +248,7 @@ func (r *Resolver) SetupOAuth(ctx context.Context, engine *gin.Engine) ([]gin.Ha } goth.UseProviders(provider) - auth.Setup(engine, r.config.OAuth.BasePath(), config.Provider, r.config.TempDir) + auth.Setup(engine, r.config.OAuth.BasePath(), r.config.AuthGroupClaim(), config.Provider, r.config.TempDir) return []gin.HandlerFunc{auth.Provider(r.config.OAuth.Provider), auth.Auth(r.config.OAuth.BasePath())}, nil } @@ -272,7 +272,7 @@ func (r *Resolver) SetupOIDC(ctx context.Context, engine *gin.Engine) ([]gin.Han goth.UseProviders(provider) - auth.Setup(engine, r.config.OpenIDConnect.BasePath(), "openid-connect", r.config.TempDir) + auth.Setup(engine, r.config.OpenIDConnect.BasePath(), r.config.AuthGroupClaim(), "openid-connect", r.config.TempDir) return []gin.HandlerFunc{auth.Provider("openid-connect"), auth.Auth(r.config.OpenIDConnect.BasePath())}, nil } @@ -369,7 +369,7 @@ func (r *Resolver) Server(ctx context.Context) (*server.Server, error) { if !r.config.UI.Disabled { var uiMiddleware []gin.HandlerFunc if r.config.AuthEnabled() { - uiMiddleware = append(uiMiddleware, auth.Valid(r.config.AuthBasePath())) + uiMiddleware = append(uiMiddleware, auth.Valid(r.config.AuthBasePath(), r.config.AuthGroupClaim())) } zap.L().Info("register UI", zap.String("path", r.config.UI.Path)) diff --git a/backend/pkg/server/api/handler.go b/backend/pkg/server/api/handler.go index b4f557a..e0a5638 100644 --- a/backend/pkg/server/api/handler.go +++ b/backend/pkg/server/api/handler.go @@ -36,7 +36,7 @@ func (h *Handler) Config(ctx *gin.Context) { clusters := make([]Cluster, 0, len(h.config.Clusters)) for _, cl := range h.config.Clusters { - access := cl.AllowedEmail(profile.Email) + access := cl.Allowed(profile) if access { clusters = append(clusters, cl) } @@ -69,7 +69,7 @@ func (h *Handler) ListCustomBoards(ctx *gin.Context) { func (h *Handler) ListPolicySources(ctx *gin.Context) { if profile := auth.ProfileFrom(ctx); profile != nil { - if !h.config.Boards.AllowedEmail(profile.Email) { + if !h.config.Boards.Allowed(profile) { ctx.AbortWithStatus(http.StatusUnauthorized) return } @@ -153,7 +153,7 @@ func (h *Handler) GetCustomBoard(ctx *gin.Context) { } if profile := auth.ProfileFrom(ctx); profile != nil { - if !config.AllowedEmail(profile.Email) { + if !config.Allowed(profile) { ctx.AbortWithStatus(http.StatusUnauthorized) return } @@ -252,14 +252,14 @@ func (h *Handler) Layout(ctx *gin.Context) { boards = make(map[string]CustomBoard, len(h.customBoards)) for key, board := range h.customBoards { - if !board.AllowedEmail(profile.Email) { + if !board.Allowed(profile) { continue } boards[key] = board } - if !h.config.Boards.AllowedEmail(profile.Email) { + if !h.config.Boards.Allowed(profile) { sources = nil } } else { @@ -277,7 +277,7 @@ func (h *Handler) Layout(ctx *gin.Context) { func (h *Handler) Dashboard(ctx *gin.Context) { if profile := auth.ProfileFrom(ctx); profile != nil { - if !h.config.Boards.AllowedEmail(profile.Email) { + if !h.config.Boards.Allowed(profile) { ctx.AbortWithStatus(http.StatusUnauthorized) return } @@ -334,7 +334,7 @@ func (h *Handler) Dashboard(ctx *gin.Context) { func (h *Handler) Policies(ctx *gin.Context) { if profile := auth.ProfileFrom(ctx); profile != nil { - if !h.config.Boards.AllowedEmail(profile.Email) { + if !h.config.Boards.Allowed(profile) { ctx.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/backend/pkg/server/api/model.go b/backend/pkg/server/api/model.go index 353eec7..765d729 100644 --- a/backend/pkg/server/api/model.go +++ b/backend/pkg/server/api/model.go @@ -1,6 +1,8 @@ package api -import "github.com/kyverno/policy-reporter-ui/pkg/auth" +import ( + "github.com/kyverno/policy-reporter-ui/pkg/auth" +) type Policy struct { Source string `json:"source,omitempty"` diff --git a/backend/pkg/utils/contains.go b/backend/pkg/utils/contains.go index 2942f21..8e83955 100644 --- a/backend/pkg/utils/contains.go +++ b/backend/pkg/utils/contains.go @@ -9,3 +9,15 @@ func Contains[T comparable](list []T, item T) bool { return false } + +func Some[T comparable](list []T, items []T) bool { + for _, i := range list { + for _, j := range items { + if i == j { + return true + } + } + } + + return false +}