mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 18:09:58 +00:00
feat(config): add ignore user groups property (#1586)
* feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(config): add ignore user groups property Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ linters:
|
||||
- unparam
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- interfacebloat
|
||||
- noinlineerr
|
||||
- revive
|
||||
settings:
|
||||
|
||||
@@ -14,6 +14,9 @@ type CapsuleConfigurationSpec struct {
|
||||
// Names of the groups for Capsule users.
|
||||
// +kubebuilder:default={capsule.clastix.io}
|
||||
UserGroups []string `json:"userGroups,omitempty"`
|
||||
// Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
IgnoreUserWithGroups []string `json:"ignoreUserWithGroups,omitempty"`
|
||||
// Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix,
|
||||
// separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
|
||||
// +kubebuilder:default=false
|
||||
|
||||
@@ -122,6 +122,11 @@ func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec)
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.IgnoreUserWithGroups != nil {
|
||||
in, out := &in.IgnoreUserWithGroups, &out.IgnoreUserWithGroups
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
out.CapsuleResources = in.CapsuleResources
|
||||
if in.NodeMetadata != nil {
|
||||
in, out := &in.NodeMetadata, &out.NodeMetadata
|
||||
|
||||
@@ -52,6 +52,13 @@ spec:
|
||||
Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix,
|
||||
separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
|
||||
type: boolean
|
||||
ignoreUserWithGroups:
|
||||
description: |-
|
||||
Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nodeMetadata:
|
||||
description: |-
|
||||
Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant.
|
||||
|
||||
@@ -9,11 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-group", Label("config"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-assigned-custom-group",
|
||||
@@ -29,6 +32,8 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
@@ -36,6 +41,17 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should fail using a User non matching the capsule-user-group flag", func() {
|
||||
@@ -68,4 +84,16 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
})
|
||||
|
||||
It("should fail when group is ignored", func() {
|
||||
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
||||
configuration.Spec.UserGroups = []string{"projectcapsule.dev"}
|
||||
configuration.Spec.IgnoreUserWithGroups = []string{"projectcapsule.dev"}
|
||||
})
|
||||
|
||||
ns := NewNamespace("")
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
@@ -19,6 +20,8 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("modifying node labels and annotations", Label("config", "nodes"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-node-user-metadata-forbidden",
|
||||
@@ -72,6 +75,8 @@ var _ = Describe("modifying node labels and annotations", Label("config", "nodes
|
||||
Skip(fmt.Sprintf("Node webhook is disabled for current version %s", version.String()))
|
||||
}
|
||||
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
@@ -110,6 +115,17 @@ var _ = Describe("modifying node labels and annotations", Label("config", "nodes
|
||||
return k8sClient.Update(context.Background(), node)
|
||||
})
|
||||
}).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should allow", func() {
|
||||
@@ -171,6 +187,19 @@ var _ = Describe("modifying node labels and annotations", Label("config", "nodes
|
||||
})
|
||||
})
|
||||
It("should fail", func() {
|
||||
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
||||
configuration.Spec.NodeMetadata = &capsulev1beta2.NodeMetadata{
|
||||
ForbiddenLabels: api.ForbiddenListSpec{
|
||||
Exact: []string{"foo", "bar"},
|
||||
Regex: "^gatsby-.*$",
|
||||
},
|
||||
ForbiddenAnnotations: api.ForbiddenListSpec{
|
||||
Exact: []string{"foo", "bar"},
|
||||
Regex: "^gatsby-.*$",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Expect(ModifyNode(func(node *corev1.Node) error {
|
||||
node.Labels["foo"] = "bar"
|
||||
node.Labels["gatsby-foo"] = "bar"
|
||||
|
||||
@@ -9,11 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
var _ = Describe("creating a Namespace with a protected Namespace regex enabled", Label("namespace"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-protected-namespace",
|
||||
@@ -29,6 +32,8 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled"
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
@@ -36,6 +41,17 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled"
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should succeed and be available in Tenant namespaces list", func() {
|
||||
|
||||
@@ -27,8 +27,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeoutInterval = 40 * time.Second
|
||||
defaultPollInterval = time.Second
|
||||
defaultTimeoutInterval = 40 * time.Second
|
||||
defaultPollInterval = time.Second
|
||||
defaultConfigurationName = "default"
|
||||
)
|
||||
|
||||
func NewService(svc types.NamespacedName) *corev1.Service {
|
||||
@@ -100,7 +101,7 @@ func EventuallyCreation(f interface{}) AsyncAssertion {
|
||||
|
||||
func ModifyCapsuleConfigurationOpts(fn func(configuration *capsulev1beta2.CapsuleConfiguration)) {
|
||||
config := &capsulev1beta2.CapsuleConfiguration{}
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "default"}, config)).ToNot(HaveOccurred())
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: defaultConfigurationName}, config)).ToNot(HaveOccurred())
|
||||
|
||||
fn(config)
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ func (c *capsuleConfiguration) UserGroups() []string {
|
||||
return c.retrievalFn().Spec.UserGroups
|
||||
}
|
||||
|
||||
func (c *capsuleConfiguration) IgnoreUserWithGroups() []string {
|
||||
return c.retrievalFn().Spec.IgnoreUserWithGroups
|
||||
}
|
||||
|
||||
func (c *capsuleConfiguration) ForbiddenUserNodeLabels() *capsuleapi.ForbiddenListSpec {
|
||||
if c.retrievalFn().Spec.NodeMetadata == nil {
|
||||
return nil
|
||||
|
||||
@@ -24,6 +24,7 @@ type Configuration interface {
|
||||
ValidatingWebhookConfigurationName() string
|
||||
TenantCRDName() string
|
||||
UserGroups() []string
|
||||
IgnoreUserWithGroups() []string
|
||||
ForbiddenUserNodeLabels() *capsuleapi.ForbiddenListSpec
|
||||
ForbiddenUserNodeAnnotations() *capsuleapi.ForbiddenListSpec
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func (r *freezedHandler) OnDelete(c client.Client, _ admission.Decoder, recorder
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name)
|
||||
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
@@ -106,7 +106,7 @@ func (r *freezedHandler) OnUpdate(c client.Client, decoder admission.Decoder, re
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups()) {
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, c, r.configuration.UserGroups(), r.configuration.IgnoreUserWithGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName())
|
||||
|
||||
response := admission.Denied("the selected Tenant is freezed")
|
||||
|
||||
@@ -62,7 +62,7 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client,
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups()) {
|
||||
if tnt.Spec.Cordoned && utils.IsCapsuleUser(ctx, req, clt, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation)))
|
||||
|
||||
response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))
|
||||
|
||||
@@ -26,9 +26,10 @@ type handler struct {
|
||||
handlers []webhook.Handler
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (h *handler) OnCreate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,9 +43,10 @@ func (h *handler) OnCreate(client client.Client, decoder admission.Decoder, reco
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (h *handler) OnDelete(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -58,9 +60,10 @@ func (h *handler) OnDelete(client client.Client, decoder admission.Decoder, reco
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (h *handler) OnUpdate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups()) {
|
||||
if !IsCapsuleUser(ctx, req, client, h.configuration.UserGroups(), h.configuration.IgnoreUserWithGroups()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string) bool {
|
||||
func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client, userGroups []string, ignoreGroups []string) bool {
|
||||
groupList := utils.NewUserGroupList(req.UserInfo.Groups)
|
||||
// if the user is a ServiceAccount belonging to the kube-system namespace, definitely, it's not a Capsule user
|
||||
// and we can skip the check in case of Capsule user group assigned to system:authenticated
|
||||
@@ -44,6 +44,14 @@ func IsCapsuleUser(ctx context.Context, req admission.Request, clt client.Client
|
||||
|
||||
for _, group := range userGroups {
|
||||
if groupList.Find(group) {
|
||||
if len(ignoreGroups) > 0 {
|
||||
for _, ignoreGroup := range ignoreGroups {
|
||||
if groupList.Find(ignoreGroup) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user