Skip to content

Commit

Permalink
Merge pull request #52 from appuio/feat/member-rbac
Browse files Browse the repository at this point in the history
Add option to bind ClusterRoles to all organization members
  • Loading branch information
glrf authored Mar 28, 2022
2 parents 695b9e8 + feac3ce commit 9d5b15a
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 2 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ all: build ## Invokes the build target

.PHONY: test
test: ## Run tests
go test ./... -coverprofile cover.out
go test ./... -coverprofile cover.tmp.out
cat cover.tmp.out | grep -v "zz_generated.deepcopy.go" > cover.out

.PHONY: build
build: generate fmt vet $(BIN_FILENAME) ## Build manager binary
Expand Down
66 changes: 66 additions & 0 deletions config/rbac/controller/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ rules:
verbs:
- create
- patch
- apiGroups:
- appuio.io
resources:
- organizationmembers
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- appuio.io
resources:
- organizationmembers/status
verbs:
- get
- patch
- update
- apiGroups:
- appuio.io
resources:
- teams
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- appuio.io
resources:
Expand All @@ -30,6 +60,30 @@ rules:
- get
- patch
- update
- apiGroups:
- organization.appuio.io
resources:
- organizations
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- rbac.appuio.io
resources:
- organizations
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
Expand All @@ -42,3 +96,15 @@ rules:
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
17 changes: 16 additions & 1 deletion controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func ControllerCommand() *cobra.Command {
probeAddr := cmd.Flags().String("health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
usernamePrefix := cmd.Flags().String("username-prefix", "", "Prefix prepended to username claims. Usually the same as \"--oidc-username-prefix\" of the Kubernetes API server")
rolePrefix := cmd.Flags().String("role-prefix", "control-api:user:", "Prefix prepended to generated cluster roles and bindings to prevent name collisions.")
memberRoles := cmd.Flags().StringSlice("member-roles", []string{}, "ClusterRoles to assign to every organization member for its namespace")

cmd.Run = func(*cobra.Command, []string) {
scheme := runtime.NewScheme()
Expand All @@ -58,6 +59,7 @@ func ControllerCommand() *cobra.Command {
mgr, err := setupManager(
*usernamePrefix,
*rolePrefix,
*memberRoles,
ctrl.Options{
Scheme: scheme,
MetricsBindAddress: *metricsAddr,
Expand All @@ -81,7 +83,7 @@ func ControllerCommand() *cobra.Command {
return cmd
}

func setupManager(usernamePrefix, rolePrefix string, opt ctrl.Options) (ctrl.Manager, error) {
func setupManager(usernamePrefix, rolePrefix string, memberRoles []string, opt ctrl.Options) (ctrl.Manager, error) {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opt)
if err != nil {
return nil, err
Expand All @@ -98,6 +100,19 @@ func setupManager(usernamePrefix, rolePrefix string, opt ctrl.Options) (ctrl.Man
if err = ur.SetupWithManager(mgr); err != nil {
return nil, err
}
if len(memberRoles) > 0 {
omr := &controllers.OrganizationMembersReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("organization-members-controller"),

UserPrefix: usernamePrefix,
MemberRoles: memberRoles,
}
if err = omr.SetupWithManager(mgr); err != nil {
return nil, err
}
}
//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
100 changes: 100 additions & 0 deletions controllers/organization_members_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package controllers

import (
"context"

"go.uber.org/multierr"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

controlv1 "github.com/appuio/control-api/apis/v1"
)

// OrganizationMembersReconciler reconciles OrganizationMembers resources
type OrganizationMembersReconciler struct {
client.Client
Recorder record.EventRecorder
Scheme *runtime.Scheme

// UserPrefix is the prefix applied to the user in the RoleBinding.subjects.name.
UserPrefix string
MemberRoles []string
}

//+kubebuilder:rbac:groups=appuio.io,resources=organizationmembers,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=appuio.io,resources=organizationmembers/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// Needed so that we are allowed to delegate common member roles
//+kubebuilder:rbac:groups="rbac.appuio.io",resources=organizations,verbs=get;list;watch;create;delete;patch;update
//+kubebuilder:rbac:groups="organization.appuio.io",resources=organizations,verbs=get;list;watch;create;delete;patch;update
//+kubebuilder:rbac:groups="appuio.io",resources=teams,verbs=get;list;watch;create;delete;patch;update
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete

// Reconcile reacts on changes of users and mirrors these changes to Keycloak
func (r *OrganizationMembersReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.V(4).WithValues("request", req).Info("Reconciling")

memb := controlv1.OrganizationMembers{}
if err := r.Get(ctx, req.NamespacedName, &memb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

if !memb.ObjectMeta.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}

var errGroup error
for _, role := range r.MemberRoles {
err := r.putRoleBinding(ctx, memb, role)
if err != nil {
errGroup = multierr.Append(errGroup, err)
r.Recorder.Event(&memb, "Warning", "RBACUpdateFailed", "Failed to set RBAC for Organization members")
}
}

return ctrl.Result{}, errGroup
}

func (r *OrganizationMembersReconciler) putRoleBinding(ctx context.Context, memb controlv1.OrganizationMembers, role string) error {
rb := rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: role,
Namespace: memb.Namespace,
},
}
op, err := ctrl.CreateOrUpdate(ctx, r.Client, &rb, func() error {
sub := make([]rbacv1.Subject, len(memb.Spec.UserRefs))
for i, ur := range memb.Spec.UserRefs {
sub[i] = rbacv1.Subject{
APIGroup: rbacv1.GroupName,
Kind: "User",
Name: r.UserPrefix + ur.Name,
}
}
rb.Subjects = sub
rb.RoleRef = rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: role,
}
return ctrl.SetControllerReference(&memb, &rb, r.Scheme)
})
log.FromContext(ctx).V(4).Info("reconcile RoleBinding", "operation", op)
return err
}

// SetupWithManager sets up the controller with the Manager.
func (r *OrganizationMembersReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&controlv1.OrganizationMembers{}).
Owns(&rbacv1.RoleBinding{}).
Complete(r)
}
Loading

0 comments on commit 9d5b15a

Please sign in to comment.