Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(teamrolebindings): teamrbac supports single users #927

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions charts/manager/crds/greenhouse.sap_teamrolebindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,50 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
createNamespaces:
default: false
description: CreateNamespaces when the flag is set the controller
will create namespaces defined in the Namespaces field.
type: boolean
namespaces:
description: |-
Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
items:
type: string
type: array
subjects:
description: Subjects define list of subject with this role
items:
description: |-
Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference,
or a value for non-objects such as user and group names.
properties:
apiGroup:
description: |-
APIGroup holds the API group of the referenced subject.
Defaults to "" for ServiceAccount subjects.
Defaults to "rbac.authorization.k8s.io" for User and Group subjects.
type: string
kind:
description: |-
Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount".
If the Authorizer does not recognized the kind value, the Authorizer should report an error.
type: string
name:
description: Name of the object being referenced.
type: string
namespace:
description: |-
Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty
the Authorizer should report an error.
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
type: array
teamRef:
description: TeamRef references a Greenhouse Team by name
type: string
Expand Down
48 changes: 48 additions & 0 deletions docs/reference/api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,19 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBinding">TeamRoleBinding
</tr>
<tr>
<td>
<code>subjects</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#subject-v1-rbac">
[]Kubernetes rbac/v1.Subject
</a>
</em>
</td>
<td>
<p>Subjects define list of subject with this role</p>
</td>
</tr>
<tr>
<td>
<code>clusterName</code><br>
<em>
string
Expand Down Expand Up @@ -3230,6 +3243,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBinding">TeamRoleBinding
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.</p>
</td>
</tr>
<tr>
<td>
<code>createNamespaces</code><br>
<em>
bool
</em>
</td>
<td>
<p>CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.</p>
</td>
</tr>
</table>
</td>
</tr>
Expand Down Expand Up @@ -3290,6 +3314,19 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBindingSpec">TeamRoleBindingSpec
</tr>
<tr>
<td>
<code>subjects</code><br>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#subject-v1-rbac">
[]Kubernetes rbac/v1.Subject
</a>
</em>
</td>
<td>
<p>Subjects define list of subject with this role</p>
</td>
</tr>
<tr>
<td>
<code>clusterName</code><br>
<em>
string
Expand Down Expand Up @@ -3324,6 +3361,17 @@ <h3 id="greenhouse.sap/v1alpha1.TeamRoleBindingSpec">TeamRoleBindingSpec
If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.</p>
</td>
</tr>
<tr>
<td>
<code>createNamespaces</code><br>
<em>
bool
</em>
</td>
<td>
<p>CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.</p>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
27 changes: 27 additions & 0 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1159,11 +1159,38 @@ components:
type: object
type: object
x-kubernetes-map-type: atomic
createNamespaces:
default: false
description: CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.
type: boolean
namespaces:
description: Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.\nIf empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
items:
type: string
type: array
subjects:
description: Subjects define list of subject with this role
items:
description: Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference,\nor a value for non-objects such as user and group names.
properties:
apiGroup:
description: APIGroup holds the API group of the referenced subject.\nDefaults to "" for ServiceAccount subjects.\nDefaults to "rbac.authorization.k8s.io" for User and Group subjects.
type: string
kind:
description: Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount".\nIf the Authorizer does not recognized the kind value, the Authorizer should report an error.
type: string
name:
description: Name of the object being referenced.
type: string
namespace:
description: Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty\nthe Authorizer should report an error.
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
type: array
teamRef:
description: TeamRef references a Greenhouse Team by name
type: string
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/greenhouse/v1alpha1/teamrolebinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package v1alpha1
import (
"slices"

rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

greenhouseapis "github.com/cloudoperators/greenhouse/pkg/apis"
Expand All @@ -17,13 +18,18 @@ type TeamRoleBindingSpec struct {
TeamRoleRef string `json:"teamRoleRef,omitempty"`
// TeamRef references a Greenhouse Team by name
TeamRef string `json:"teamRef,omitempty"`
// Subjects define list of subject with this role
Subjects []rbacv1.Subject `json:"subjects,omitempty"`
// ClusterName is the name of the cluster the rbacv1 resources are created on.
ClusterName string `json:"clusterName,omitempty"`
// ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to.
ClusterSelector metav1.LabelSelector `json:"clusterSelector,omitempty"`
// Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.
// If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace.
Namespaces []string `json:"namespaces,omitempty"`
// CreateNamespaces when the flag is set the controller will create namespaces defined in the Namespaces field.
// +kubebuilder:default:=false
CreateNamespaces bool `json:"createNamespaces,omitempty"`
}

// TeamRoleBindingStatus defines the observed state of the TeamRoleBinding
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 40 additions & 14 deletions pkg/controllers/teamrbac/teamrolebinding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type TeamRoleBindingReconciler struct {
//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamrolebindings/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=greenhouse.sap,resources=teamrolebindings/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=create;get

// SetupWithManager sets up the controller with the Manager.
func (r *TeamRoleBindingReconciler) SetupWithManager(name string, mgr ctrl.Manager) error {
Expand Down Expand Up @@ -198,6 +199,15 @@ func (r *TeamRoleBindingReconciler) EnsureDeleted(ctx context.Context, resource
func (r *TeamRoleBindingReconciler) doReconcile(ctx context.Context, teamRole *greenhousev1alpha1.TeamRole, clusters *greenhousev1alpha1.ClusterList, trb *greenhousev1alpha1.TeamRoleBinding, team *greenhousev1alpha1.Team) error {
failedClusters := []string{}
cr := initRBACClusterRole(teamRole)

if trb.Spec.CreateNamespaces {
err := r.createNamespaces(ctx, trb)
if err != nil {
trb.SetCondition(greenhousev1alpha1.FalseCondition(greenhousev1alpha1.RBACReady, greenhousev1alpha1.RBACReconcileFailed, err.Error()))
return err
}
}

for _, cluster := range clusters.Items {
remoteRestClient, err := clientutil.NewK8sClientFromCluster(ctx, r.Client, &cluster)
if err != nil {
Expand Down Expand Up @@ -438,13 +448,7 @@ func rbacRoleBinding(trb *greenhousev1alpha1.TeamRoleBinding, clusterRole *rbacv
Kind: clusterRole.Kind,
Name: clusterRole.GetName(),
},
Subjects: []rbacv1.Subject{
{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: team.Spec.MappedIDPGroup,
},
},
Subjects: joinSubjectWithDefault(trb.Spec.Subjects, team.Spec.MappedIDPGroup),
}
}

Expand All @@ -463,16 +467,18 @@ func rbacClusterRoleBinding(trb *greenhousev1alpha1.TeamRoleBinding, clusterRole
Kind: clusterRole.Kind,
Name: clusterRole.GetName(),
},
Subjects: []rbacv1.Subject{
{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: team.Spec.MappedIDPGroup,
},
},
Subjects: joinSubjectWithDefault(trb.Spec.Subjects, team.Spec.MappedIDPGroup),
}
}

func joinSubjectWithDefault(subjects []rbacv1.Subject, mappedIDPGroup string) []rbacv1.Subject {
return append(subjects, rbacv1.Subject{
APIGroup: rbacv1.GroupName,
Kind: rbacv1.GroupKind,
Name: mappedIDPGroup,
})
}

// getTeamRole retrieves the Role referenced by the given RoleBinding in the RoleBinding's Namespace
func getTeamRole(ctx context.Context, c client.Client, r record.EventRecorder, teamRoleBinding *greenhousev1alpha1.TeamRoleBinding) (*greenhousev1alpha1.TeamRole, error) {
if teamRoleBinding.Spec.TeamRoleRef == "" {
Expand Down Expand Up @@ -785,3 +791,23 @@ func computeReadyCondition(status greenhousev1alpha1.TeamRoleBindingStatus) gree
readyCondition.Message = "ready"
return readyCondition
}

func (r *TeamRoleBindingReconciler) createNamespaces(ctx context.Context, teamRoleBinding *greenhousev1alpha1.TeamRoleBinding) error {
for _, nampespaceName := range teamRoleBinding.Spec.Namespaces {
namespace := new(corev1.Namespace)
err := r.Client.Get(ctx, types.NamespacedName{Name: nampespaceName}, namespace)
if err == nil {
continue
}
if apierrors.IsNotFound(err) {
err := r.Client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nampespaceName}})
if err != nil {
return err
}
} else {
return err
}
}

return nil
}
36 changes: 34 additions & 2 deletions pkg/controllers/teamrbac/teamrolebinding_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
trb := setup.CreateTeamRoleBinding(test.Ctx, "test-rolebinding",
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterA.Name))
test.WithClusterName(clusterA.Name),
test.WithSubject(rbacv1.Subject{
Kind: rbacv1.UserKind,
APIGroup: rbacv1.GroupName,
Name: "test-user-1",
}))
trbKey := types.NamespacedName{Name: trb.Name, Namespace: trb.Namespace}
By("validating the RoleBinding created on the remote clusterA")
remoteRoleBinding := &rbacv1.ClusterRoleBinding{}
Expand Down Expand Up @@ -146,6 +151,7 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
}).Should(BeTrue(), "there should be no error getting the RoleBinding from ClusterB")
Expect(remoteRoleBinding.RoleRef.Name).To(HavePrefix(greenhouseapis.RBACPrefix))
Expect(remoteRoleBinding.RoleRef.Name).To(ContainSubstring(teamRoleUT.Name))
Expect(remoteRoleBinding.Subjects).To(HaveLen(2))

By("validating the TeamRoleBinding's status is updated")
Eventually(func(g Gomega) {
Expand Down Expand Up @@ -312,7 +318,12 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterA.Name),
test.WithNamespaces(setup.Namespace()))
test.WithNamespaces(setup.Namespace()),
test.WithSubject(rbacv1.Subject{
Kind: rbacv1.UserKind,
APIGroup: rbacv1.GroupName,
Name: "test-user-1",
}))

By("validating the RoleBinding created on the remote cluster")
remoteRoleBinding := &rbacv1.RoleBinding{}
Expand All @@ -326,6 +337,7 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
}).Should(BeTrue(), "there should be no error getting the RoleBinding")
Expect(remoteRoleBinding.RoleRef.Name).To(HavePrefix(greenhouseapis.RBACPrefix))
Expect(remoteRoleBinding.RoleRef.Name).To(ContainSubstring(teamRoleUT.Name))
Expect(remoteRoleBinding.Subjects).To(HaveLen(2))

By("validating the ClusterRole created on the remote cluster")
remoteClusterRole := &rbacv1.ClusterRole{}
Expand Down Expand Up @@ -386,6 +398,26 @@ var _ = Describe("Validate ClusterRole & RoleBinding on Remote Cluster", Ordered
By("cleaning up the test")
test.EventuallyDeleted(test.Ctx, test.K8sClient, trb)
})

It("Should create namespaces when flag is set to true", func() {
By("creating a TeamRoleBinding on the central cluster")
trb := setup.CreateTeamRoleBinding(test.Ctx, "test-rolebinding",
test.WithTeamRoleRef(teamRoleUT.Name),
test.WithTeamRef(teamUT.Name),
test.WithClusterName(clusterA.Name),
test.WithNamespaces("non-existing-namespace"),
test.WithCreateNamespace(true))

By("checking that the Namespace is created")
namespace := &corev1.Namespace{}
Eventually(func(g Gomega) {
err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: "non-existing-namespace"}, namespace)
g.Expect(err).ToNot(HaveOccurred(), "there should be no error getting the non-existing namespace")
}).Should(Succeed())

By("cleaning up the test")
test.EventuallyDeleted(test.Ctx, test.K8sClient, trb)
})
})

Context("When creating a Greenhouse TeamRoleBinding with non-existing namespaces on the central cluster", func() {
Expand Down
12 changes: 12 additions & 0 deletions pkg/test/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ func WithNamespaces(namespaces ...string) func(*greenhousev1alpha1.TeamRoleBindi
}
}

func WithCreateNamespace(createNamespaces bool) func(*greenhousev1alpha1.TeamRoleBinding) {
return func(trb *greenhousev1alpha1.TeamRoleBinding) {
trb.Spec.CreateNamespaces = createNamespaces
}
}

func WithSubject(subject rbacv1.Subject) func(*greenhousev1alpha1.TeamRoleBinding) {
return func(trb *greenhousev1alpha1.TeamRoleBinding) {
trb.Spec.Subjects = append(trb.Spec.Subjects, subject)
}
}

// NewTeamRoleBinding returns a greenhousev1alpha1.TeamRoleBinding object. Opts can be used to set the desired state of the TeamRoleBinding.
func NewTeamRoleBinding(ctx context.Context, name, namespace string, opts ...func(*greenhousev1alpha1.TeamRoleBinding)) *greenhousev1alpha1.TeamRoleBinding {
trb := &greenhousev1alpha1.TeamRoleBinding{
Expand Down
Loading