Skip to content

Commit

Permalink
Add mutatingwebhook for fleetworkspace
Browse files Browse the repository at this point in the history
Add mutating webhook so that when user creates fleetworkspace they are
automatically assigned to fleetworkspace-admin role in that namespace
  • Loading branch information
Daishan committed Nov 23, 2020
1 parent 720b045 commit 938754f
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 20 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/rancher/dynamiclistener v0.2.1-0.20201110045217-9b1b7d3132e8
github.com/rancher/lasso v0.0.0-20200905045615-7fcb07d6a20b
github.com/rancher/rancher/pkg/apis v0.0.0-20200910005616-198ec5bdf52d
github.com/rancher/wrangler v0.7.3-0.20201110045154-e8db40f4380c
github.com/rancher/wrangler v0.7.3-0.20201113175531-e43374b2929a
github.com/sirupsen/logrus v1.6.0
k8s.io/api v0.19.0
k8s.io/apimachinery v0.19.0
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,6 @@ github.com/rancher/eks-operator v0.1.0-rc22/go.mod h1:TrTF54+K2X6QLhVFspIfawYcPn
github.com/rancher/lasso v0.0.0-20200427171700-e0509f89f319/go.mod h1:6Dw19z1lDIpL887eelVjyqH/mna1hfR61ddCFOG78lw=
github.com/rancher/lasso v0.0.0-20200513231433-d0ce66327a25/go.mod h1:6Dw19z1lDIpL887eelVjyqH/mna1hfR61ddCFOG78lw=
github.com/rancher/lasso v0.0.0-20200515155337-a34e1e26ad91/go.mod h1:G6Vv2aj6xB2YjTVagmu4NkhBvbE8nBcGykHRENH6arI=
github.com/rancher/lasso v0.0.0-20200820172840-0e4cc0ef5cb0 h1:ng7i8n0kzTGnXyvVK+nkb+sLm06BBNdsbd2aqJAP3lM=
github.com/rancher/lasso v0.0.0-20200820172840-0e4cc0ef5cb0/go.mod h1:OhBBBO1pBwYp0hacWdnvSGOj+XE9yMLOLnaypIlic18=
github.com/rancher/lasso v0.0.0-20200905045615-7fcb07d6a20b h1:DQLpu44dsR+2qYvg1wyYadk108MWMHUOqvb2z4274Vs=
github.com/rancher/lasso v0.0.0-20200905045615-7fcb07d6a20b/go.mod h1:OhBBBO1pBwYp0hacWdnvSGOj+XE9yMLOLnaypIlic18=
Expand All @@ -531,8 +530,8 @@ github.com/rancher/wrangler v0.6.2-0.20200427172034-da9b142ae061/go.mod h1:n5Du/
github.com/rancher/wrangler v0.6.2-0.20200515155908-1923f3f8ec3f/go.mod h1:NmtmlLkchboIksYJuBemwcP4RBfv8FpeyhVoWXB9Wdc=
github.com/rancher/wrangler v0.6.2-0.20200714200521-c61fae623942/go.mod h1:8LdIqAQPHysxNlHqmKbUiDIx9ULt9IHUauh9aOnr67k=
github.com/rancher/wrangler v0.6.2-0.20200820173016-2068de651106/go.mod h1:iKqQcYs4YSDjsme52OZtQU4jHPmLlIiM93aj2c8c/W8=
github.com/rancher/wrangler v0.7.3-0.20201110045154-e8db40f4380c h1:pdRBf4DhIYn8bT5LhQUjwRsnf8cx/iUSFjk+mL+a1T0=
github.com/rancher/wrangler v0.7.3-0.20201110045154-e8db40f4380c/go.mod h1:goezjesEKwMxHLfltdjg9DW0xWV7txQee6vOuSDqXAI=
github.com/rancher/wrangler v0.7.3-0.20201113175531-e43374b2929a h1:mlxEam9x6k4dS7pUytXKYwjeMf1qOmAOm2kOOcBZicI=
github.com/rancher/wrangler v0.7.3-0.20201113175531-e43374b2929a/go.mod h1:goezjesEKwMxHLfltdjg9DW0xWV7txQee6vOuSDqXAI=
github.com/rancher/wrangler-api v0.6.1-0.20200427172631-a7c2f09b783e h1:UJpGtw6IKs0dHPTF+6Wd12lskeCZZAejl8/ie/fc1+0=
github.com/rancher/wrangler-api v0.6.1-0.20200427172631-a7c2f09b783e/go.mod h1:2lcWR98q8HU3U4mVETnXc8quNG0uXxrt8vKd6cAa/30=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"

"github.com/rancher/webhook/pkg/server"
_ "github.com/rancher/wrangler/pkg/generated/controllers/admissionregistration.k8s.io"
"github.com/rancher/wrangler/pkg/kubeconfig"
"github.com/rancher/wrangler/pkg/ratelimit"
"github.com/rancher/wrangler/pkg/signals"
Expand Down
10 changes: 8 additions & 2 deletions pkg/clients/clients.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package clients

import (
v1 "github.com/rancher/rancher/pkg/apis/catalog.cattle.io/v1"
"context"

"github.com/rancher/webhook/pkg/auth"
"github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io"
managementv3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3"
"github.com/rancher/wrangler/pkg/clients"
"github.com/rancher/wrangler/pkg/schemes"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/client-go/rest"
rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation"
)
Expand All @@ -18,7 +20,7 @@ type Clients struct {
EscalationChecker *auth.EscalationChecker
}

func New(rest *rest.Config) (*Clients, error) {
func New(ctx context.Context, rest *rest.Config) (*Clients, error) {
clients, err := clients.NewFromConfig(rest, nil)
if err != nil {
return nil, err
Expand All @@ -33,6 +35,10 @@ func New(rest *rest.Config) (*Clients, error) {
return nil, err
}

if err = mgmt.Start(ctx, 5); err != nil {
return nil, err
}

rbacRestGetter := auth.RBACRestGetter{
Roles: clients.RBAC.Role().Cache(),
RoleBindings: clients.RBAC.RoleBinding().Cache(),
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (c *clusterValidator) Admit(response *webhook.Response, request *webhook.Re
ResourceAttributes: &v1.ResourceAttributes{
Verb: "fleetaddcluster",
Version: "v1",
Resource: "namespaces",
Resource: "fleetworkspace",
Name: newCluster.Spec.FleetWorkspaceName,
},
User: request.UserInfo.Username,
Expand Down
183 changes: 183 additions & 0 deletions pkg/resources/fleetworkspace/fleetworkspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package fleetworkspace

import (
"github.com/rancher/rancher/pkg/apis/management.cattle.io"
v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/webhook/pkg/auth"
"github.com/rancher/webhook/pkg/clients"
corev1controller "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
rbacvacontroller "github.com/rancher/wrangler/pkg/generated/controllers/rbac/v1"
"github.com/rancher/wrangler/pkg/webhook"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
fleetAdminRole = "fleetworkspace-admin"
)

func NewMutator(client *clients.Clients) webhook.Handler {
return &mutator{
namespaces: client.Core.Namespace(),
rolebindings: client.RBAC.RoleBinding(),
clusterrolebindings: client.RBAC.ClusterRoleBinding(),
clusterroles: client.RBAC.ClusterRole(),
escalationChecker: client.EscalationChecker,
}
}

type mutator struct {
namespaces corev1controller.NamespaceController
rolebindings rbacvacontroller.RoleBindingController
clusterrolebindings rbacvacontroller.ClusterRoleBindingController
clusterroles rbacvacontroller.ClusterRoleController
escalationChecker *auth.EscalationChecker
}

// When fleetworkspace is created, it will create the following resources:
// 1. Namespace. It will have the same name as fleetworkspace
// 2. fleetworkspace ClusterRole. It will create the cluster role that has * permission only to the current workspace
// 3. Two roleBinding to bind the current user to fleet-admin roles and fleetworkspace roles
func (m *mutator) Admit(response *webhook.Response, request *webhook.Request) error {
if request.DryRun != nil && *request.DryRun {
response.Allowed = true
return nil
}

fw, err := fleetworkspaceObjects(request)
if err != nil {
return err
}

namespace := v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fw.Name,
},
}
ns, err := m.namespaces.Create(&namespace)
if err != nil {
if !errors.IsAlreadyExists(err) {
return err
}
// check if user has enough privilege to create fleet admin rolebinding in the namespace
cr, err := m.clusterroles.Cache().Get(fleetAdminRole)
if err != nil {
return err
}
if err := m.escalationChecker.ConfirmNoEscalation(response, request, cr.Rules, namespace.Name); err != nil {
return err
}
ns, err = m.namespaces.Get(namespace.Name, metav1.GetOptions{})
if err != nil {
return err
}
}

// create rolebinding to bind current user with fleetworkspace-admin role in current namespace
if err := m.createAdminRoleAndBindings(request, fw); err != nil && !errors.IsAlreadyExists(err) {
return err
}

// create an own clusterRole and clusterRoleBindings to make sure the creator has full permission to its own fleetworkspace
if err := m.createOwnRoleAndBinding(request, fw, ns); err != nil {
return err
}

response.Allowed = true
return nil
}

func (m *mutator) createAdminRoleAndBindings(request *webhook.Request, fw *v3.FleetWorkspace) error {
rolebinding := rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "fleetworkspace-admin-binding-" + fw.Name,
Namespace: fw.Name,
},
Subjects: []rbacv1.Subject{
{
Kind: "User",
APIGroup: "rbac.authorization.k8s.io",
Name: request.UserInfo.Username,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: fleetAdminRole,
},
}
if _, err := m.rolebindings.Create(&rolebinding); err != nil {
return err
}
return nil
}

func (m *mutator) createOwnRoleAndBinding(request *webhook.Request, fw *v3.FleetWorkspace, ns *v1.Namespace) error {
clusterrole := rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "fleetworkspace-own-" + fw.Name,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "Namespace",
Name: ns.Name,
UID: ns.UID,
Controller: new(bool),
BlockOwnerDeletion: new(bool),
},
},
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{
management.GroupName,
},
Verbs: []string{
"*",
},
Resources: []string{
"fleetworkspaces",
},
ResourceNames: []string{
fw.Name,
},
},
},
}
if _, err := m.clusterroles.Create(&clusterrole); err != nil && !errors.IsAlreadyExists(err) {
return err
}

rolebindingView := rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "fleetworkspace-own-binding-" + fw.Name,
},
Subjects: []rbacv1.Subject{
{
Kind: "User",
APIGroup: "rbac.authorization.k8s.io",
Name: request.UserInfo.Username,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: clusterrole.Name,
},
}
if _, err := m.clusterrolebindings.Create(&rolebindingView); err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}

func fleetworkspaceObjects(request *webhook.Request) (*v3.FleetWorkspace, error) {
object, err := request.DecodeObject()
if err != nil {
return nil, err
}

return object.(*v3.FleetWorkspace), nil
}
19 changes: 19 additions & 0 deletions pkg/server/mutation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package server

import (
"net/http"

"github.com/rancher/rancher/pkg/apis/management.cattle.io"
v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/webhook/pkg/clients"
"github.com/rancher/webhook/pkg/resources/fleetworkspace"
"github.com/rancher/wrangler/pkg/webhook"
)

func Mutation(client *clients.Clients) (http.Handler, error) {
fleetworkspaceMutator := fleetworkspace.NewMutator(client)

router := webhook.NewRouter()
router.Kind("FleetWorkspace").Group(management.GroupName).Type(&v3.FleetWorkspace{}).Handle(fleetworkspaceMutator)
return router, nil
}
74 changes: 61 additions & 13 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@ import (
)

var (
namespace = "cattle-system"
tlsName = "rancher-webhook.cattle-system.svc"
certName = "cattle-webhook-tls"
caName = "cattle-webhook-ca"
port = int32(443)
validationPath = "/v1/webhook/validation"
mutationPath = "/v1/webhook/mutation"
clusterScope = v1.ClusterScope
namespaceScope = v1.NamespacedScope
failPolicyFail = v1.Fail
failPolicyIgnore = v1.Ignore
sideEffectClassNone = v1.SideEffectClassNone
namespace = "cattle-system"
tlsName = "rancher-webhook.cattle-system.svc"
certName = "cattle-webhook-tls"
caName = "cattle-webhook-ca"
port = int32(443)
validationPath = "/v1/webhook/validation"
mutationPath = "/v1/webhook/mutation"
clusterScope = v1.ClusterScope
namespaceScope = v1.NamespacedScope
failPolicyFail = v1.Fail
failPolicyIgnore = v1.Ignore
sideEffectClassNone = v1.SideEffectClassNone
sideEffectClassNoneOnDryRun = v1.SideEffectClassNoneOnDryRun
)

func ListenAndServe(ctx context.Context, cfg *rest.Config) error {
clients, err := clients.New(cfg)
clients, err := clients.New(ctx, cfg)
if err != nil {
return err
}
Expand All @@ -42,8 +43,14 @@ func ListenAndServe(ctx context.Context, cfg *rest.Config) error {
return err
}

mutation, err := Mutation(clients)
if err != nil {
return err
}

router := mux.NewRouter()
router.Handle(validationPath, validation)
router.Handle(mutationPath, mutation)

return listenAndServe(ctx, clients, router)
}
Expand Down Expand Up @@ -161,9 +168,50 @@ func listenAndServe(ctx context.Context, clients *clients.Clients, handler http.
AdmissionReviewVersions: []string{"v1", "v1beta1"},
},
},
}, &v1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "rancher.cattle.io",
},
Webhooks: []v1.MutatingWebhook{
{
Name: "rancherfleet.cattle.io",
ClientConfig: v1.WebhookClientConfig{
Service: &v1.ServiceReference{
Namespace: namespace,
Name: "rancher-webhook",
Path: &mutationPath,
Port: &port,
},
CABundle: secret.Data[corev1.TLSCertKey],
},
Rules: []v1.RuleWithOperations{
{
Operations: []v1.OperationType{
v1.Create,
},
Rule: v1.Rule{
APIGroups: []string{"management.cattle.io"},
APIVersions: []string{"v3"},
Resources: []string{"fleetworkspaces"},
Scope: &clusterScope,
},
},
},
FailurePolicy: &failPolicyFail,
SideEffects: &sideEffectClassNoneOnDryRun,
AdmissionReviewVersions: []string{"v1", "v1beta1"},
},
},
})
})

defer func() {
if rErr != nil {
return
}
rErr = clients.Start(ctx)
}()

return server.ListenAndServe(ctx, 9443, 0, handler, &server.ListenOpts{
Secrets: clients.Core.Secret(),
CertNamespace: namespace,
Expand Down

0 comments on commit 938754f

Please sign in to comment.