From 7ecf73c9e6ab059c864016608b2d3466d0b17586 Mon Sep 17 00:00:00 2001 From: Peter Matseykanets Date: Mon, 11 May 2026 10:32:09 -0400 Subject: [PATCH] Make LDAP/AD authconfig UserIDAttribute and GroupIDAttribute fields immutable Complements and dependes on rancher/rancher#48699 --- docs.md | 11 + go.mod | 18 +- go.sum | 44 +- .../v3/authconfig/Authconfig.md | 11 + .../v3/authconfig/validator.go | 54 +++ .../v3/authconfig/validator_test.go | 386 ++++++++++++++++++ 6 files changed, 494 insertions(+), 30 deletions(-) diff --git a/docs.md b/docs.md index a5bb74174d..1788ca5406 100644 --- a/docs.md +++ b/docs.md @@ -192,11 +192,22 @@ When an LDAP (`openldap`, `freeipa`) or ActiveDirectory (`activedirectory`) auth - `groupDNAttribute` - `groupMemberUserAttribute` - `groupMemberMappingAttribute` + - `userIDAttribute` + - `groupIDAttribute` - If set, the following fields should have a valid LDAP filter expression according to RFC4515 - `userLoginFilter` - `userSearchFilter` - `groupSearchFilter` +#### Update + +When an enabled LDAP or ActiveDirectory authconfig is updated and remains enabled, the following fields cannot be changed: + +- `userIDAttribute` +- `groupIDAttribute` + +To change these fields, first disable the provider, apply the change, then re-enable it. + ## Cluster diff --git a/go.mod b/go.mod index 823913792b..7b1834e12c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.25.0 toolchain go1.25.9 +replace github.com/rancher/rancher/pkg/apis => github.com/pmatseykanets/rancher/pkg/apis v0.0.0-20260504145013-06d2cbb05095 + replace ( k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.35.4 k8s.io/cli-runtime => k8s.io/cli-runtime v0.35.4 @@ -42,7 +44,7 @@ require ( github.com/rancher/rancher/pkg/apis v0.0.0-20260211194119-d0c9ffaf3cb0 github.com/rancher/rancher/pkg/plan v0.0.0-20260413094914-0404acf59a23 github.com/rancher/rke v1.8.13 - github.com/rancher/wrangler/v3 v3.5.0-rc.2 + github.com/rancher/wrangler/v3 v3.6.0-rc.2 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 @@ -103,11 +105,11 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect - github.com/rancher/aks-operator v1.13.0-rc.4 // indirect - github.com/rancher/ali-operator v1.13.0-rc.2 // indirect - github.com/rancher/eks-operator v1.13.0-rc.4 // indirect - github.com/rancher/fleet/pkg/apis v0.15.0-alpha.6 // indirect - github.com/rancher/gke-operator v1.13.0-rc.3 // indirect + github.com/rancher/aks-operator v1.14.0-rc.2 // indirect + github.com/rancher/ali-operator v1.14.0-rc.1 // indirect + github.com/rancher/eks-operator v1.14.0-rc.5 // indirect + github.com/rancher/fleet/pkg/apis v0.15.0-beta.4 // indirect + github.com/rancher/gke-operator v1.14.0-rc.3 // indirect github.com/rancher/norman v0.9.3 github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -119,7 +121,7 @@ require ( golang.org/x/crypto v0.50.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect @@ -130,7 +132,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.35.0 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect k8s.io/code-generator v0.35.4 // indirect k8s.io/component-base v0.35.4 // indirect k8s.io/component-helpers v0.35.0 // indirect diff --git a/go.sum b/go.sum index 88ba63e04e..337aee5ce4 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -117,14 +117,16 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmatseykanets/rancher/pkg/apis v0.0.0-20260504145013-06d2cbb05095 h1:cQW8LvFYt5O8KAgiLr3WdsyzxsAw7NvCgAEs8FKu5eo= +github.com/pmatseykanets/rancher/pkg/apis v0.0.0-20260504145013-06d2cbb05095/go.mod h1:bxDaTMCaeSGQV1IiOJlPLKV6rqpoeI593cTneOvOPZY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -136,32 +138,30 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/rancher/aks-operator v1.13.0-rc.4 h1:tc7p2gZmRg4c6VBwWTQJYwmh1hlN68kftjoBIdGCnqw= -github.com/rancher/aks-operator v1.13.0-rc.4/go.mod h1:1ZjZB6zGHK+NGchN9KLplq+xPxRRi+q6Uzet5bjFwxo= -github.com/rancher/ali-operator v1.13.0-rc.2 h1:a0biHGez+Np9XybJVh3yKN4RGPdaCzfM6D6cAXJac6o= -github.com/rancher/ali-operator v1.13.0-rc.2/go.mod h1:s5HznpxsN9LsgtX6u5UoW9dZNKnDLuXcwzQRAEoDcog= +github.com/rancher/aks-operator v1.14.0-rc.2 h1:KhXvWn+ZuHlnHb1acCYtT1q0YlAZSX6KBv2ZsWXL0fo= +github.com/rancher/aks-operator v1.14.0-rc.2/go.mod h1:WqkM8U5n+w8fNTkYmq4rm7OGZRqGA2eTLnHQYjAmkoM= +github.com/rancher/ali-operator v1.14.0-rc.1 h1:K/9sFAdo7QM8rD+ZpPM8YkwhLee4NHqvu5psNEdZArU= +github.com/rancher/ali-operator v1.14.0-rc.1/go.mod h1:96zTYfGWn5kQ6bkVimm2NlsEUYB5FzAYHmn+hlqs4Hc= github.com/rancher/dynamiclistener v0.8.0 h1:V+Au9B37jY5LTVanpIVOgUnMHERVmJR9dIZRtpAzz/I= github.com/rancher/dynamiclistener v0.8.0/go.mod h1:0dO+pAb3FHrGIR6IaOjXqGXSRLMsecVdz64x7aRjBoU= -github.com/rancher/eks-operator v1.13.0-rc.4 h1:XowN8+m3QZTIBOBLzar4frtz0xtREb9kcX6KXhF4eas= -github.com/rancher/eks-operator v1.13.0-rc.4/go.mod h1:SbaKX2ttFWCxGOYkrKYeWH/6E4oToq2rRTcrMa2Mmdk= -github.com/rancher/fleet/pkg/apis v0.15.0-alpha.6 h1:T9ELFwdKQMgkvSEfxxIGQu0G/ek3hqZb97iwUX6AcuY= -github.com/rancher/fleet/pkg/apis v0.15.0-alpha.6/go.mod h1:fPTA5s4eDfrQ2u6E1AqWSVX78P0uDIBLHOm/UnA6Iqc= -github.com/rancher/gke-operator v1.13.0-rc.3 h1:a6U+7+XIbJPH2CE7/vFUx6RpThNbFl7fqIqkEBb6zmA= -github.com/rancher/gke-operator v1.13.0-rc.3/go.mod h1:TroxpmqMh63Hf4H5bC+2GYcgOCQp9kIUDfyKdNAMo6Q= +github.com/rancher/eks-operator v1.14.0-rc.5 h1:aA7Kjxi3VfeveTR1sHpLN3YQiPb98f6r0+6yoDifvbc= +github.com/rancher/eks-operator v1.14.0-rc.5/go.mod h1:bqHtTtuREMFHFH/lUbE6Knmylm+2uemVE8gcxzx2iMc= +github.com/rancher/fleet/pkg/apis v0.15.0-beta.4 h1:AogMaC49Bz2DXZ6ve09+A1m2ae1msv0slj+GOmtVeMY= +github.com/rancher/fleet/pkg/apis v0.15.0-beta.4/go.mod h1:rpQ28vD81DN0Dz6QDakZlgtsOYOKBZSRJJ/LbFVHsA0= +github.com/rancher/gke-operator v1.14.0-rc.3 h1:l6x6IFyRNEbQoCBW+UL5MAg1eWsk2pjggW/l98GysQM= +github.com/rancher/gke-operator v1.14.0-rc.3/go.mod h1:fJiULH+JDH0qxXFezmq+A1tPHTRxQAuIItId/Ffzeqo= github.com/rancher/jsonpath v0.0.0-20260423141252-c4e0c565a09f h1:eXPhsmHBuA6Xroj9rwsqieGD+J0eqwPavkYQrpS0hJk= github.com/rancher/jsonpath v0.0.0-20260423141252-c4e0c565a09f/go.mod h1:xavYr3cNyyAeA72nMVB60+q/EJ8Anu+loQWFJyXOeP8= github.com/rancher/lasso v0.2.8 h1:AhAk1AKz9ZpvPlFj07JKRYGzIxaPNywUs2LpSwcyGDU= github.com/rancher/lasso v0.2.8/go.mod h1:HyENYowaiGY9jnsyxLnhGfoOBZlU3y+lws8qCYyuBOc= github.com/rancher/norman v0.9.3 h1:dxNbxtea6GEXWshrCcxQxjodZNSuyIxBlJ472OBiMcQ= github.com/rancher/norman v0.9.3/go.mod h1:la3oS4sgFfRvQVxAQxAwSkXnO8kaAIyhBscnHIVtqN8= -github.com/rancher/rancher/pkg/apis v0.0.0-20260211194119-d0c9ffaf3cb0 h1:ZH9VeY2iUVZN0YFU59dhBiHbdOpFJMvVo91k0fwwMME= -github.com/rancher/rancher/pkg/apis v0.0.0-20260211194119-d0c9ffaf3cb0/go.mod h1:fEL3ATWhwqdn7VYVAQBhmdpKehY5lDeGNlq2NnOpUPk= github.com/rancher/rancher/pkg/plan v0.0.0-20260413094914-0404acf59a23 h1:dDz+8F29pcBC6RtXxr4nYl0fdsWu4ZFdNL4oVchytSo= github.com/rancher/rancher/pkg/plan v0.0.0-20260413094914-0404acf59a23/go.mod h1:eFUzIOmWOB309XK/99EIE5YcqaQGda8bSpxoeN6uehw= github.com/rancher/rke v1.8.13 h1:DDJ6bj1ucPRmmCQkJ39tiP40st3UlVduBp4/tuhk2L0= github.com/rancher/rke v1.8.13/go.mod h1:EaAkq796bgmmx/s15Xz0TvCkBOfepMOqO8tFockOmis= -github.com/rancher/wrangler/v3 v3.5.0-rc.2 h1:actO3J77tGT8Ub0/VsI573iRLJf+BTnmi307xV+sBhY= -github.com/rancher/wrangler/v3 v3.5.0-rc.2/go.mod h1:bRcdkdwRTwoXVSWVGtJdSBNXkKbInlo+PVkCtkUCi4s= +github.com/rancher/wrangler/v3 v3.6.0-rc.2 h1:2M3pCJ18jiZwH9embvueYYZcVJs6BvO5CWHjPdloJAw= +github.com/rancher/wrangler/v3 v3.6.0-rc.2/go.mod h1:1E82h3MBVRWTvwgrspnj/7nMh6nb73h76gCL8u/nOjw= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -210,8 +210,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= diff --git a/pkg/resources/management.cattle.io/v3/authconfig/Authconfig.md b/pkg/resources/management.cattle.io/v3/authconfig/Authconfig.md index fb538b1858..bd807d6ba3 100644 --- a/pkg/resources/management.cattle.io/v3/authconfig/Authconfig.md +++ b/pkg/resources/management.cattle.io/v3/authconfig/Authconfig.md @@ -19,7 +19,18 @@ When an LDAP (`openldap`, `freeipa`) or ActiveDirectory (`activedirectory`) auth - `groupDNAttribute` - `groupMemberUserAttribute` - `groupMemberMappingAttribute` + - `userIDAttribute` + - `groupIDAttribute` - If set, the following fields should have a valid LDAP filter expression according to RFC4515 - `userLoginFilter` - `userSearchFilter` - `groupSearchFilter` + +### Update + +When an enabled LDAP or ActiveDirectory authconfig is updated and remains enabled, the following fields cannot be changed: + +- `userIDAttribute` +- `groupIDAttribute` + +To change these fields, first disable the provider, apply the change, then re-enable it. diff --git a/pkg/resources/management.cattle.io/v3/authconfig/validator.go b/pkg/resources/management.cattle.io/v3/authconfig/validator.go index 4fc8b39501..b3d1307767 100644 --- a/pkg/resources/management.cattle.io/v3/authconfig/validator.go +++ b/pkg/resources/management.cattle.io/v3/authconfig/validator.go @@ -86,9 +86,51 @@ func (a *admitter) admitCreate(request *admission.Request, newAuthConfig *v3.Aut } func (a *admitter) admitUpdate(request *admission.Request, oldAuthConfig, newAuthConfig *v3.AuthConfig) (*admissionv1.AdmissionResponse, error) { + if oldAuthConfig.Enabled && newAuthConfig.Enabled { + if err := validateIDAttributeImmutability(oldAuthConfig.Type, request); err != nil { + return admission.ResponseBadRequest(err.Error()), nil + } + } return a.admitCommonCreateUpdate(request, oldAuthConfig, newAuthConfig) } +func validateIDAttributeImmutability(authConfigType string, request *admission.Request) error { + var err error + + switch authConfigType { + case "openLdapConfig", "freeIpaConfig": + var oldConfig, newConfig v3.LdapConfig + if e := json.Unmarshal(request.OldObject.Raw, &oldConfig); e != nil { + return fmt.Errorf("failed to unmarshal old %T: %w", oldConfig, e) + } + if e := json.Unmarshal(request.Object.Raw, &newConfig); e != nil { + return fmt.Errorf("failed to unmarshal new %T: %w", newConfig, e) + } + if oldConfig.UserIDAttribute != newConfig.UserIDAttribute { + err = errors.Join(err, field.Invalid(field.NewPath("userIDAttribute"), newConfig.UserIDAttribute, "field is immutable when the provider is enabled")) + } + if oldConfig.GroupIDAttribute != newConfig.GroupIDAttribute { + err = errors.Join(err, field.Invalid(field.NewPath("groupIDAttribute"), newConfig.GroupIDAttribute, "field is immutable when the provider is enabled")) + } + case "activeDirectoryConfig": + var oldConfig, newConfig v3.ActiveDirectoryConfig + if e := json.Unmarshal(request.OldObject.Raw, &oldConfig); e != nil { + return fmt.Errorf("failed to unmarshal old %T: %w", oldConfig, e) + } + if e := json.Unmarshal(request.Object.Raw, &newConfig); e != nil { + return fmt.Errorf("failed to unmarshal new %T: %w", newConfig, e) + } + if oldConfig.UserIDAttribute != newConfig.UserIDAttribute { + err = errors.Join(err, field.Invalid(field.NewPath("userIDAttribute"), newConfig.UserIDAttribute, "field is immutable when the provider is enabled")) + } + if oldConfig.GroupIDAttribute != newConfig.GroupIDAttribute { + err = errors.Join(err, field.Invalid(field.NewPath("groupIDAttribute"), newConfig.GroupIDAttribute, "field is immutable when the provider is enabled")) + } + } + + return err +} + func (a *admitter) admitCommonCreateUpdate(request *admission.Request, _, newAuthConfig *v3.AuthConfig) (*admissionv1.AdmissionResponse, error) { var err error @@ -166,6 +208,12 @@ func validateLDAPConfig(request *admission.Request) error { if config.GroupMemberMappingAttribute != "" && !IsValidLdapAttr(config.GroupMemberMappingAttribute) { err = errors.Join(err, field.Forbidden(field.NewPath("groupMemberMappingAttribute"), "invalid value")) } + if config.UserIDAttribute != "" && !IsValidLdapAttr(config.UserIDAttribute) { + err = errors.Join(err, field.Forbidden(field.NewPath("userIDAttribute"), "invalid value")) + } + if config.GroupIDAttribute != "" && !IsValidLdapAttr(config.GroupIDAttribute) { + err = errors.Join(err, field.Forbidden(field.NewPath("groupIDAttribute"), "invalid value")) + } if config.UserLoginFilter != "" { if _, fieldErr := ldapv3.CompileFilter(config.UserLoginFilter); fieldErr != nil { @@ -237,6 +285,12 @@ func validateActiveDirectoryConfig(request *admission.Request) error { if config.GroupMemberMappingAttribute != "" && !IsValidLdapAttr(config.GroupMemberMappingAttribute) { err = errors.Join(err, field.Forbidden(field.NewPath("groupMemberMappingAttribute"), "invalid value")) } + if config.UserIDAttribute != "" && !IsValidLdapAttr(config.UserIDAttribute) { + err = errors.Join(err, field.Forbidden(field.NewPath("userIDAttribute"), "invalid value")) + } + if config.GroupIDAttribute != "" && !IsValidLdapAttr(config.GroupIDAttribute) { + err = errors.Join(err, field.Forbidden(field.NewPath("groupIDAttribute"), "invalid value")) + } if config.UserLoginFilter != "" { if _, fieldErr := ldapv3.CompileFilter(config.UserLoginFilter); fieldErr != nil { diff --git a/pkg/resources/management.cattle.io/v3/authconfig/validator_test.go b/pkg/resources/management.cattle.io/v3/authconfig/validator_test.go index 5d3b54593d..f0c4c7bcb7 100644 --- a/pkg/resources/management.cattle.io/v3/authconfig/validator_test.go +++ b/pkg/resources/management.cattle.io/v3/authconfig/validator_test.go @@ -39,6 +39,8 @@ func TestValidateLdapConfig(t *testing.T) { GroupDNAttribute: "entryDN", GroupMemberUserAttribute: "entryDN", GroupMemberMappingAttribute: "member", + UserIDAttribute: "uid", + GroupIDAttribute: "cn", UserLoginFilter: "(&(status=active)(canLogin=true))", UserSearchFilter: "(status=active)", GroupSearchFilter: "(depNo=123)", @@ -179,6 +181,22 @@ func TestValidateLdapConfig(t *testing.T) { return fields }, }, + { + desc: "invalid UserIDAttribute", + fields: func() v3.LdapFields { + fields := fields + fields.UserIDAttribute = invalidAttr + return fields + }, + }, + { + desc: "invalid GroupIDAttribute", + fields: func() v3.LdapFields { + fields := fields + fields.GroupIDAttribute = invalidAttr + return fields + }, + }, { desc: "invalid UserLoginFilter", fields: func() v3.LdapFields { @@ -240,6 +258,8 @@ func TestValidateActiveDirectoryConfig(t *testing.T) { GroupDNAttribute: "distinguishedName", GroupMemberUserAttribute: "member", GroupMemberMappingAttribute: "distinguishedName", + UserIDAttribute: "sAMAccountName", + GroupIDAttribute: "sAMAccountName", UserLoginFilter: "(&(status=active)(canLogin=true))", UserSearchFilter: "(status=active)", GroupSearchFilter: "(depNo=123)", @@ -375,6 +395,22 @@ func TestValidateActiveDirectoryConfig(t *testing.T) { return config }, }, + { + desc: "invalid UserIDAttribute", + config: func() v3.ActiveDirectoryConfig { + config := config + config.UserIDAttribute = invalidAttr + return config + }, + }, + { + desc: "invalid GroupIDAttribute", + config: func() v3.ActiveDirectoryConfig { + config := config + config.GroupIDAttribute = invalidAttr + return config + }, + }, { desc: "invalid UserLoginFilter", config: func() v3.ActiveDirectoryConfig { @@ -417,6 +453,356 @@ func TestValidateActiveDirectoryConfig(t *testing.T) { } } +func TestIDAttributeImmutability(t *testing.T) { + t.Parallel() + validator := authconfig.NewValidator() + + t.Run("ActiveDirectory", func(t *testing.T) { + t.Parallel() + base := v3.ActiveDirectoryConfig{ + Servers: []string{"ad.example.com"}, + UserIDAttribute: "sAMAccountName", + GroupIDAttribute: "sAMAccountName", + } + base.Name = "activedirectory" + base.Type = "activeDirectoryConfig" + + tests := []struct { + desc string + old v3.ActiveDirectoryConfig + new v3.ActiveDirectoryConfig + allowed bool + }{ + { + desc: "set UserIDAttribute on first enable", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = false + c.UserIDAttribute = "" + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "unchanged UserIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "changed UserIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + c.UserIDAttribute = "uid" + return c + }(), + }, + { + desc: "changed UserIDAttribute while disabling provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = false + c.UserIDAttribute = "uid" + return c + }(), + allowed: true, + }, + { + desc: "cleared UserIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + c.UserIDAttribute = "" + return c + }(), + }, + { + desc: "set GroupIDAttribute on first enable", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = false + c.GroupIDAttribute = "" + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "unchanged GroupIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "changed GroupIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + c.GroupIDAttribute = "cn" + return c + }(), + }, + { + desc: "changed GroupIDAttribute while disabling provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = false + c.GroupIDAttribute = "cn" + return c + }(), + allowed: true, + }, + { + desc: "cleared GroupIDAttribute on enabled provider", + old: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.ActiveDirectoryConfig { + c := base + c.Enabled = true + c.GroupIDAttribute = "" + return c + }(), + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + testAdmit(t, validator, v1.Update, test.old, test.new, test.allowed) + }) + } + }) + + for _, provider := range []struct { + name string + typeName string + }{ + {"OpenLDAP", "openLdapConfig"}, + {"FreeIPA", "freeIpaConfig"}, + } { + t.Run(provider.name, func(t *testing.T) { + t.Parallel() + base := v3.OpenLdapConfig{} + base.Name = provider.name + base.Type = provider.typeName + base.Servers = []string{"ldap.example.com"} + base.UserIDAttribute = "uid" + base.GroupIDAttribute = "cn" + + tests := []struct { + desc string + old v3.OpenLdapConfig + new v3.OpenLdapConfig + allowed bool + }{ + { + desc: "set UserIDAttribute on first enable", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = false + c.UserIDAttribute = "" + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "unchanged UserIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "changed UserIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + c.UserIDAttribute = "sAMAccountName" + return c + }(), + }, + { + desc: "changed UserIDAttribute while disabling provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = false + c.UserIDAttribute = "sAMAccountName" + return c + }(), + allowed: true, + }, + { + desc: "cleared UserIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + c.UserIDAttribute = "" + return c + }(), + }, + { + desc: "set GroupIDAttribute on first enable", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = false + c.GroupIDAttribute = "" + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "unchanged GroupIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + allowed: true, + }, + { + desc: "changed GroupIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + c.GroupIDAttribute = "entryDN" + return c + }(), + }, + { + desc: "changed GroupIDAttribute while disabling provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = false + c.GroupIDAttribute = "entryDN" + return c + }(), + allowed: true, + }, + { + desc: "cleared GroupIDAttribute on enabled provider", + old: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + return c + }(), + new: func() v3.OpenLdapConfig { + c := base + c.Enabled = true + c.GroupIDAttribute = "" + return c + }(), + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + testAdmit(t, validator, v1.Update, test.old, test.new, test.allowed) + }) + } + }) + } +} + func TestIsValidLdapAttr(t *testing.T) { t.Parallel()