mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-30 16:59:55 +00:00
feat: enhance admin role checks to support multiple roles configuration (#43)
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
This commit is contained in:
@@ -78,6 +78,7 @@ type OIDCConfig struct {
|
||||
NameAttribute string `mapstructure:"name_attribute"`
|
||||
RoleAttributePath string `mapstructure:"role_attribute_path"`
|
||||
AdminRole string `mapstructure:"admin_role"`
|
||||
AdminRoles []string `mapstructure:"admin_roles"`
|
||||
TLSSkipVerify bool `mapstructure:"tls_skip_verify"`
|
||||
SessionMaxAge int `mapstructure:"session_max_age"`
|
||||
CookieName string `mapstructure:"cookie_name"`
|
||||
@@ -86,6 +87,29 @@ type OIDCConfig struct {
|
||||
CookieSameSite string `mapstructure:"cookie_same_site"`
|
||||
}
|
||||
|
||||
// EffectiveAdminRoles returns the deduplicated list of admin roles drawn from
|
||||
// both admin_role (legacy single-value) and admin_roles (list). A user is
|
||||
// considered an admin if any of their roles matches any entry in this list.
|
||||
func (o OIDCConfig) EffectiveAdminRoles() []string {
|
||||
seen := make(map[string]struct{}, len(o.AdminRoles)+1)
|
||||
var roles []string
|
||||
add := func(r string) {
|
||||
if r == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[r]; ok {
|
||||
return
|
||||
}
|
||||
seen[r] = struct{}{}
|
||||
roles = append(roles, r)
|
||||
}
|
||||
add(o.AdminRole)
|
||||
for _, r := range o.AdminRoles {
|
||||
add(r)
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// CORSConfig contains CORS settings for frontend communication
|
||||
type CORSConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
@@ -231,6 +255,7 @@ func bindEnvVars() {
|
||||
viper.BindEnv("auth.oidc.name_attribute", "GARAGE_UI_AUTH_OIDC_NAME_ATTRIBUTE")
|
||||
viper.BindEnv("auth.oidc.role_attribute_path", "GARAGE_UI_AUTH_OIDC_ROLE_ATTRIBUTE_PATH")
|
||||
viper.BindEnv("auth.oidc.admin_role", "GARAGE_UI_AUTH_OIDC_ADMIN_ROLE")
|
||||
viper.BindEnv("auth.oidc.admin_roles", "GARAGE_UI_AUTH_OIDC_ADMIN_ROLES")
|
||||
viper.BindEnv("auth.oidc.tls_skip_verify", "GARAGE_UI_AUTH_OIDC_TLS_SKIP_VERIFY")
|
||||
viper.BindEnv("auth.oidc.session_max_age", "GARAGE_UI_AUTH_OIDC_SESSION_MAX_AGE")
|
||||
viper.BindEnv("auth.oidc.cookie_name", "GARAGE_UI_AUTH_OIDC_COOKIE_NAME")
|
||||
@@ -291,11 +316,12 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("oidc scopes are required when oidc is enabled")
|
||||
}
|
||||
// Every authenticated route on this service grants full admin
|
||||
// access — there is no separate authorization layer. An empty
|
||||
// admin_role would therefore promote every user in the IdP realm
|
||||
// to cluster admin. Require operators to opt in explicitly.
|
||||
if c.Auth.OIDC.AdminRole == "" {
|
||||
return fmt.Errorf("oidc admin_role is required when oidc is enabled: leaving it empty would grant cluster-admin access to any authenticated IdP user")
|
||||
// access — there is no separate authorization layer. Empty
|
||||
// admin role configuration would therefore promote every user
|
||||
// in the IdP realm to cluster admin. Require operators to opt
|
||||
// in explicitly via admin_role or admin_roles.
|
||||
if len(c.Auth.OIDC.EffectiveAdminRoles()) == 0 {
|
||||
return fmt.Errorf("oidc admin_role or admin_roles is required when oidc is enabled: leaving them empty would grant cluster-admin access to any authenticated IdP user")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -292,7 +293,33 @@ func TestValidate(t *testing.T) {
|
||||
applyValidOIDC(c)
|
||||
c.Auth.OIDC.AdminRole = ""
|
||||
},
|
||||
wantErrContains: "oidc admin_role is required",
|
||||
wantErrContains: "oidc admin_role or admin_roles is required",
|
||||
},
|
||||
{
|
||||
name: "oidc enabled with admin_roles only is valid",
|
||||
mutate: func(c *Config) {
|
||||
applyValidOIDC(c)
|
||||
c.Auth.OIDC.AdminRole = ""
|
||||
c.Auth.OIDC.AdminRoles = []string{"group1", "group2"}
|
||||
},
|
||||
wantErrContains: "",
|
||||
},
|
||||
{
|
||||
name: "oidc enabled with both admin_role and admin_roles is valid",
|
||||
mutate: func(c *Config) {
|
||||
applyValidOIDC(c)
|
||||
c.Auth.OIDC.AdminRoles = []string{"group2", "group3"}
|
||||
},
|
||||
wantErrContains: "",
|
||||
},
|
||||
{
|
||||
name: "oidc enabled with empty admin_role and empty admin_roles rejected",
|
||||
mutate: func(c *Config) {
|
||||
applyValidOIDC(c)
|
||||
c.Auth.OIDC.AdminRole = ""
|
||||
c.Auth.OIDC.AdminRoles = []string{}
|
||||
},
|
||||
wantErrContains: "oidc admin_role or admin_roles is required",
|
||||
},
|
||||
{
|
||||
name: "oidc fully configured is valid",
|
||||
@@ -498,6 +525,32 @@ func TestValidate_TokenAuthExplicitlyEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveAdminRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
adminRole string
|
||||
adminRoles []string
|
||||
want []string
|
||||
}{
|
||||
{"both empty", "", nil, nil},
|
||||
{"single only", "admin", nil, []string{"admin"}},
|
||||
{"list only", "", []string{"a", "b"}, []string{"a", "b"}},
|
||||
{"merge single + list", "admin", []string{"viewer", "ops"}, []string{"admin", "viewer", "ops"}},
|
||||
{"dedupes overlap", "admin", []string{"admin", "ops"}, []string{"admin", "ops"}},
|
||||
{"dedupes within list", "", []string{"a", "a", "b"}, []string{"a", "b"}},
|
||||
{"skips empty strings in list", "admin", []string{"", "ops", ""}, []string{"admin", "ops"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
o := OIDCConfig{AdminRole: tc.adminRole, AdminRoles: tc.adminRoles}
|
||||
got := o.EffectiveAdminRoles()
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("EffectiveAdminRoles() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProduction(t *testing.T) {
|
||||
tests := []struct {
|
||||
env string
|
||||
|
||||
Reference in New Issue
Block a user