diff --git a/backend/pkg/auth/permissions.go b/backend/pkg/auth/permissions.go new file mode 100644 index 0000000..728b296 --- /dev/null +++ b/backend/pkg/auth/permissions.go @@ -0,0 +1,49 @@ +package auth + +import ( + "net/http" + "slices" + "strings" + + "github.com/gin-gonic/gin" +) + +type AccessControl struct { + Emails []string +} + +type Permissions struct { + AccessControl AccessControl `json:"-"` +} + +func (p Permissions) AllowedEmail(email string) bool { + if len(p.AccessControl.Emails) == 0 { + return true + } + + return slices.Contains(p.AccessControl.Emails, email) +} + +func ClusterPermissions(permissions map[string]Permissions) gin.HandlerFunc { + return func(ctx *gin.Context) { + cluster := ctx.Param("cluster") + if cluster == "" && strings.HasPrefix(ctx.Request.URL.Path, "/proxy/") { + parts := strings.Split(ctx.Request.URL.Path, "/") + cluster = strings.TrimSpace(parts[2]) + } + + if cluster == "" { + ctx.Next() + return + } + + if profile := ProfileFrom(ctx); profile != nil { + if !permissions[cluster].AllowedEmail(profile.Email) { + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + ctx.Next() + } +} diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index e51223b..1dfc8ad 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -118,15 +118,16 @@ func (a Plugin) FromValues(values secrets.Values) Plugin { return a } -// APISetup configuration +// Cluster configuration type Cluster struct { - Name string `mapstructure:"name"` - Host string `mapstructure:"host"` - Plugins []Plugin `mapstructure:"plugins"` - SkipTLS bool `mapstructure:"skipTLS"` - Certificate string `mapstructure:"certificate"` - SecretRef string `mapstructure:"secretRef"` - BasicAuth BasicAuth `mapstructure:"basicAuth"` + Name string `mapstructure:"name"` + Host string `mapstructure:"host"` + Plugins []Plugin `mapstructure:"plugins"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + SecretRef string `mapstructure:"secretRef"` + BasicAuth BasicAuth `mapstructure:"basicAuth"` + AccessControl AccessControl `mapstructure:"accessControl"` } func (a Cluster) FromValues(values secrets.Values) Cluster { diff --git a/backend/pkg/config/mapper.go b/backend/pkg/config/mapper.go index 1d9e9d8..55b05f8 100644 --- a/backend/pkg/config/mapper.go +++ b/backend/pkg/config/mapper.go @@ -3,6 +3,7 @@ package config import ( "github.com/gosimple/slug" + "github.com/kyverno/policy-reporter-ui/pkg/auth" "github.com/kyverno/policy-reporter-ui/pkg/server/api" "github.com/kyverno/policy-reporter-ui/pkg/utils" ) @@ -19,6 +20,9 @@ func MapConfig(c *Config) *api.Config { Name: cl.Name, Slug: slug.Make(cl.Name), Plugins: plugins, + Permissions: auth.Permissions{ + AccessControl: auth.AccessControl(cl.AccessControl), + }, }) } @@ -38,8 +42,8 @@ func MapConfig(c *Config) *api.Config { OAuth: oauth, Banner: c.UI.Banner, Boards: api.Boards{ - Permissions: api.Permissions{ - AccessControl: api.AccessControl{ + Permissions: auth.Permissions{ + AccessControl: auth.AccessControl{ Emails: c.Boards.AccessControl.Emails, }, }, @@ -76,8 +80,8 @@ func MapCustomBoards(customBoards []CustomBoard) map[string]api.CustomBoard { Sources: api.Sources{ List: c.Sources.List, }, - Permissions: api.Permissions{ - AccessControl: api.AccessControl{ + Permissions: auth.Permissions{ + AccessControl: auth.AccessControl{ Emails: c.AccessControl.Emails, }, }, @@ -90,3 +94,14 @@ func MapCustomBoards(customBoards []CustomBoard) map[string]api.CustomBoard { return configs } + +func MapClusterPermissions(c *Config) map[string]auth.Permissions { + permissions := make(map[string]auth.Permissions, len(c.Clusters)) + for _, cluster := range c.Clusters { + permissions[slug.Make(cluster.Name)] = auth.Permissions{ + AccessControl: auth.AccessControl(cluster.AccessControl), + } + } + + return permissions +} diff --git a/backend/pkg/config/resolver.go b/backend/pkg/config/resolver.go index aa4eef3..9746a87 100644 --- a/backend/pkg/config/resolver.go +++ b/backend/pkg/config/resolver.go @@ -315,6 +315,10 @@ func (r *Resolver) Server(ctx context.Context) (*server.Server, error) { middleware = append(middleware, gin.Recovery()) } + if r.config.AuthEnabled() { + middleware = append(middleware, auth.ClusterPermissions(MapClusterPermissions(r.config))) + } + serv := server.NewServer(engine, r.config.Server.Port, middleware) for _, cluster := range r.config.Clusters { @@ -372,10 +376,7 @@ func (r *Resolver) Server(ctx context.Context) (*server.Server, error) { serv.RegisterUI(r.config.UI.Path, uiMiddleware) } - serv.RegisterAPI( - MapConfig(r.config), - MapCustomBoards(r.config.CustomBoards), - ) + serv.RegisterAPI(MapConfig(r.config), MapCustomBoards(r.config.CustomBoards)) return serv, nil } diff --git a/backend/pkg/server/api/handler.go b/backend/pkg/server/api/handler.go index 913787d..b4f557a 100644 --- a/backend/pkg/server/api/handler.go +++ b/backend/pkg/server/api/handler.go @@ -31,6 +31,35 @@ func (h *Handler) Healthz(ctx *gin.Context) { } func (h *Handler) Config(ctx *gin.Context) { + if profile := auth.ProfileFrom(ctx); profile != nil { + cluster := h.config.Default + + clusters := make([]Cluster, 0, len(h.config.Clusters)) + for _, cl := range h.config.Clusters { + access := cl.AllowedEmail(profile.Email) + if access { + clusters = append(clusters, cl) + } + if cluster == cl.Slug && !access { + cluster = "" + } + } + + if cluster == "" && len(clusters) > 0 { + cluster = clusters[0].Slug + } + + ctx.JSON(http.StatusOK, Config{ + Clusters: clusters, + Default: cluster, + User: h.config.User, + Sources: h.config.Sources, + Banner: h.config.Banner, + OAuth: h.config.OAuth, + }) + return + } + ctx.JSON(http.StatusOK, h.config) } diff --git a/backend/pkg/server/api/model.go b/backend/pkg/server/api/model.go index faeffbb..353eec7 100644 --- a/backend/pkg/server/api/model.go +++ b/backend/pkg/server/api/model.go @@ -1,6 +1,6 @@ package api -import "slices" +import "github.com/kyverno/policy-reporter-ui/pkg/auth" type Policy struct { Source string `json:"source,omitempty"` @@ -33,9 +33,10 @@ type Source struct { } type Cluster struct { - Name string `json:"name"` - Slug string `json:"slug"` - Plugins []string `json:"plugins"` + auth.Permissions `json:"-"` + Name string `json:"name"` + Slug string `json:"slug"` + Plugins []string `json:"plugins"` } type PolicyReports struct { @@ -51,34 +52,18 @@ type Sources struct { List []string } -type AccessControl struct { - Emails []string -} - -type Permissions struct { - AccessControl AccessControl `json:"-"` -} - -func (p Permissions) AllowedEmail(email string) bool { - if len(p.AccessControl.Emails) == 0 { - return true - } - - return slices.Contains(p.AccessControl.Emails, email) -} - type CustomBoard struct { - Permissions - Name string `json:"name"` - ID string `json:"id"` - ClusterScope bool `json:"-"` - Namespaces Namespaces `json:"-"` - Sources Sources `json:"-"` - PolicyReports PolicyReports `json:"-"` + auth.Permissions `json:"-"` + Name string `json:"name"` + ID string `json:"id"` + ClusterScope bool `json:"-"` + Namespaces Namespaces `json:"-"` + Sources Sources `json:"-"` + PolicyReports PolicyReports `json:"-"` } type Boards struct { - Permissions + auth.Permissions } type Config struct { diff --git a/frontend/modules/core/components/form/ClusterSelect.vue b/frontend/modules/core/components/form/ClusterSelect.vue index 5116ffc..0c6b41c 100644 --- a/frontend/modules/core/components/form/ClusterSelect.vue +++ b/frontend/modules/core/components/form/ClusterSelect.vue @@ -9,7 +9,7 @@ hide-details density="compact" prepend-inner-icon="mdi-kubernetes" - style="max-width: 140px;" + style="min-width: 140px;" @update:model-value="input" v-if="clusters.length > 1" />