diff --git a/.gitignore b/.gitignore index e442b052e..a8ff30550 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /dist *.swp .idea +/webhook \ No newline at end of file diff --git a/main.go b/main.go index c5f35bed4..54a2096e8 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +//go:generate go run pkg/codegen/cleanup/main.go +//go:generate go run pkg/codegen/main.go package main import ( diff --git a/pkg/admission/server.go b/pkg/admission/server.go index da5811911..0acdca7ce 100644 --- a/pkg/admission/server.go +++ b/pkg/admission/server.go @@ -12,20 +12,21 @@ import ( v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) var ( - namespace = "cattle-system" - tlsName = "rancher-webhook.cattle-system.svc" - certName = "cattle-webhook-tls" - caName = "cattle-webhook-ca" - port = int32(443) - path = "/v1/webhook/validation" - clusterScope = v1.ClusterScope - failPolicy = v1.Ignore - sideEffect = v1.SideEffectClassNone + namespace = "cattle-system" + tlsName = "rancher-webhook.cattle-system.svc" + certName = "cattle-webhook-tls" + caName = "cattle-webhook-ca" + port = int32(443) + path = "/v1/webhook/validation" + clusterScope = v1.ClusterScope + namespaceScope = v1.NamespacedScope + failPolicyFail = v1.Fail + failPolicyIgnore = v1.Ignore + sideEffect = v1.SideEffectClassNone ) func ListenAndServe(ctx context.Context, cfg *rest.Config) error { @@ -33,13 +34,11 @@ func ListenAndServe(ctx context.Context, cfg *rest.Config) error { return err } - k8s, err := kubernetes.NewForConfig(cfg) + handler, err := Validation(cfg) if err != nil { return err } - handler := Validation(k8s.AuthorizationV1().SubjectAccessReviews()) - return listenAndServe(ctx, cfg, handler) } @@ -91,7 +90,63 @@ func listenAndServe(ctx context.Context, cfg *rest.Config, handler http.Handler) }, }, }, - FailurePolicy: &failPolicy, + FailurePolicy: &failPolicyIgnore, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }, + { + Name: "rancherauth.cattle.io", + ClientConfig: v1.WebhookClientConfig{ + Service: &v1.ServiceReference{ + Namespace: namespace, + Name: "rancher-webhook", + Path: &path, + Port: &port, + }, + CABundle: secret.Data[corev1.TLSCertKey], + }, + Rules: []v1.RuleWithOperations{ + { + Operations: []v1.OperationType{ + v1.Create, + v1.Update, + v1.Delete, + }, + Rule: v1.Rule{ + APIGroups: []string{"management.cattle.io"}, + APIVersions: []string{"v3"}, + Resources: []string{"globalrolebindings"}, + Scope: &clusterScope, + }, + }, + { + Operations: []v1.OperationType{ + v1.Create, + v1.Update, + v1.Delete, + }, + Rule: v1.Rule{ + APIGroups: []string{"management.cattle.io"}, + APIVersions: []string{"v3"}, + Resources: []string{"projectroletemplatebindings"}, + Scope: &namespaceScope, + }, + }, + { + Operations: []v1.OperationType{ + v1.Create, + v1.Update, + v1.Delete, + }, + Rule: v1.Rule{ + APIGroups: []string{"management.cattle.io"}, + APIVersions: []string{"v3"}, + Resources: []string{"clusterroletemplatebindings"}, + Scope: &namespaceScope, + }, + }, + }, + FailurePolicy: &failPolicyFail, SideEffects: &sideEffect, AdmissionReviewVersions: []string{"v1"}, }, diff --git a/pkg/admission/validation.go b/pkg/admission/validation.go index cdf42f289..adf21197f 100644 --- a/pkg/admission/validation.go +++ b/pkg/admission/validation.go @@ -5,15 +5,47 @@ 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/cluster" + mgmtcontrollers "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io" + "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" "github.com/rancher/wrangler/pkg/webhook" - authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Validation(sar authorizationv1.SubjectAccessReviewInterface) http.Handler { - clusters := newClusterValidator(sar) +func Validation(cfg *rest.Config) (http.Handler, error) { + grb, err := mgmtcontrollers.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + + r, err := rbac.NewFactoryFromConfig(cfg) + if err != nil { + return nil, err + } + + globalRoleBindings, err := auth.NewGRBValidator(grb.Management().V3().GlobalRole(), r.Rbac()) + if err != nil { + return nil, err + } + + k8s, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + + sar := k8s.AuthorizationV1().SubjectAccessReviews() + + clusters := cluster.NewClusterValidator(sar) + prtbs := auth.NewPRTBalidator(sar) + crtbs := auth.NewCRTBalidator(sar) router := webhook.NewRouter() router.Kind("Cluster").Group(management.GroupName).Type(&v3.Cluster{}).Handle(clusters) + router.Kind("GlobalRoleBinding").Group(management.GroupName).Type(&v3.GlobalRoleBinding{}).Handle(globalRoleBindings) + router.Kind("ProjectRoleTemplateBinding").Group(management.GroupName).Type(&v3.ProjectRoleTemplateBinding{}).Handle(prtbs) + router.Kind("ClusterRoleTemplateBinding").Group(management.GroupName).Type(&v3.ClusterRoleTemplateBinding{}).Handle(crtbs) - return router + return router, nil } diff --git a/pkg/auth/clusterrtb.go b/pkg/auth/clusterrtb.go new file mode 100644 index 000000000..78135622d --- /dev/null +++ b/pkg/auth/clusterrtb.go @@ -0,0 +1,96 @@ +package auth + +import ( + "net/http" + "time" + + rancherv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/wrangler/pkg/webhook" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/utils/trace" +) + +const adminRole = "admin" + +func NewCRTBalidator(sar authorizationv1.SubjectAccessReviewInterface) webhook.Handler { + return &clusterRoleTemplateBindingValidator{ + sar: sar, + } +} + +type clusterRoleTemplateBindingValidator struct { + sar authorizationv1.SubjectAccessReviewInterface +} + +func (c *clusterRoleTemplateBindingValidator) Admit(response *webhook.Response, request *webhook.Request) error { + listTrace := trace.New("clusterRoleTemplateBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username}) + defer listTrace.LogIfLong(1 * time.Second) + + crtb, err := crtbObject(request) + if err != nil { + return err + } + + if crtb.ClusterName != "local" { + response.Allowed = true + return nil + } + + return adminAccessCheck(c.sar, response, request) +} + +func crtbObject(request *webhook.Request) (*rancherv3.ClusterRoleTemplateBinding, error) { + var crtb runtime.Object + var err error + if request.Operation == admissionv1.Delete { + crtb, err = request.DecodeOldObject() + } else { + crtb, err = request.DecodeObject() + } + return crtb.(*rancherv3.ClusterRoleTemplateBinding), err +} + +func toExtra(extra map[string]authenticationv1.ExtraValue) map[string]v1.ExtraValue { + result := map[string]v1.ExtraValue{} + for k, v := range extra { + result[k] = v1.ExtraValue(v) + } + return result +} + +// adminAccessCheck checks that the user submitting the request has ** access in the local cluster +func adminAccessCheck(sar authorizationv1.SubjectAccessReviewInterface, response *webhook.Response, request *webhook.Request) error { + resp, err := sar.Create(request.Context, &v1.SubjectAccessReview{ + Spec: v1.SubjectAccessReviewSpec{ + ResourceAttributes: &v1.ResourceAttributes{ + Group: "*", + Verb: "*", + Resource: "*", + }, + User: request.UserInfo.Username, + Groups: request.UserInfo.Groups, + Extra: toExtra(request.UserInfo.Extra), + UID: request.UserInfo.UID, + }, + }, metav1.CreateOptions{}) + if err != nil { + return err + } + + if resp.Status.Allowed { + response.Allowed = true + } else { + response.Result = &metav1.Status{ + Status: "Failure", + Message: resp.Status.Reason, + Reason: metav1.StatusReasonUnauthorized, + Code: http.StatusUnauthorized, + } + } + return nil +} diff --git a/pkg/auth/globarolebinding.go b/pkg/auth/globarolebinding.go new file mode 100644 index 000000000..5c1a7b78d --- /dev/null +++ b/pkg/auth/globarolebinding.go @@ -0,0 +1,107 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + "time" + + rancherv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/webhook/pkg/authentication" + v3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3" + "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" + "github.com/rancher/wrangler/pkg/webhook" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + k8srequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/registry/rbac/validation" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" + "k8s.io/utils/trace" +) + +func NewGRBValidator(grClient v3.GlobalRoleClient, r rbac.Interface) (webhook.Handler, error) { + rbacRestGetter := authentication.RBACRestGetter{ + Interface: r, + } + + ruleResolver := rbacregistryvalidation.NewDefaultRuleResolver(rbacRestGetter, rbacRestGetter, rbacRestGetter, rbacRestGetter) + + return &globalRoleBindingValidator{ + globalRoleClient: grClient, + ruleSolver: ruleResolver, + }, nil + +} + +type globalRoleBindingValidator struct { + globalRoleClient v3.GlobalRoleClient + ruleSolver validation.AuthorizationRuleResolver +} + +func (grbv *globalRoleBindingValidator) Admit(response *webhook.Response, request *webhook.Request) error { + listTrace := trace.New("globalRoleBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username}) + defer listTrace.LogIfLong(1 * time.Second) + + newGRB, err := grbObject(request) + if err != nil { + return err + } + + // Pull the global role to get the rules + globalRole, err := grbv.globalRoleClient.Get(newGRB.GlobalRoleName, metav1.GetOptions{}) + if err != nil { + return err + } + + userInfo := &user.DefaultInfo{ + Name: request.UserInfo.Username, + UID: request.UserInfo.UID, + Groups: request.UserInfo.Groups, + Extra: toExtraString(request.UserInfo.Extra), + } + + if err := grbv.ConfirmNoEscalation(globalRole.Rules, userInfo); err != nil { + response.Result = &metav1.Status{ + Status: "Failure", + Message: err.Error(), + Reason: metav1.StatusReasonUnauthorized, + Code: http.StatusUnauthorized, + } + return nil + } + response.Allowed = true + return nil +} + +// ConfirmNoEscalation checks that the user attempting to create the GRB has all the permissions they are attempting +// to grant through the GRB +func (grbv *globalRoleBindingValidator) ConfirmNoEscalation(rules []rbacv1.PolicyRule, userInfo *user.DefaultInfo) error { + globaleCtx := k8srequest.WithNamespace(k8srequest.WithUser(context.Background(), userInfo), "") + if err := rbacregistryvalidation.ConfirmNoEscalation(globaleCtx, grbv.ruleSolver, rules); err != nil { + return fmt.Errorf("failed to validate user: %v", err) + } + return nil +} + +func grbObject(request *webhook.Request) (*rancherv3.GlobalRoleBinding, error) { + var grb runtime.Object + var err error + if request.Operation == admissionv1.Delete { + grb, err = request.DecodeOldObject() + } else { + grb, err = request.DecodeObject() + } + return grb.(*rancherv3.GlobalRoleBinding), err +} + +func toExtraString(extra map[string]authenticationv1.ExtraValue) map[string][]string { + result := make(map[string][]string) + for k, v := range extra { + result[k] = v + } + return result +} diff --git a/pkg/auth/projectrtb.go b/pkg/auth/projectrtb.go new file mode 100644 index 000000000..3800b5b0d --- /dev/null +++ b/pkg/auth/projectrtb.go @@ -0,0 +1,58 @@ +package auth + +import ( + "strings" + "time" + + rancherv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + "github.com/rancher/wrangler/pkg/webhook" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/utils/trace" +) + +func NewPRTBalidator(sar authorizationv1.SubjectAccessReviewInterface) webhook.Handler { + return &projectRoleTemplateBindingValidator{ + sar: sar, + } +} + +type projectRoleTemplateBindingValidator struct { + sar authorizationv1.SubjectAccessReviewInterface +} + +func (p *projectRoleTemplateBindingValidator) Admit(response *webhook.Response, request *webhook.Request) error { + listTrace := trace.New("projectRoleTemplateBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username}) + defer listTrace.LogIfLong(1 * time.Second) + + prtb, err := prtbObject(request) + if err != nil { + return err + } + + clusterID := clusterFromProject(prtb.ProjectName) + + if clusterID != "local" { + response.Allowed = true + return nil + } + + return adminAccessCheck(p.sar, response, request) +} + +func prtbObject(request *webhook.Request) (*rancherv3.ProjectRoleTemplateBinding, error) { + var prtb runtime.Object + var err error + if request.Operation == admissionv1.Delete { + prtb, err = request.DecodeOldObject() + } else { + prtb, err = request.DecodeObject() + } + return prtb.(*rancherv3.ProjectRoleTemplateBinding), err +} + +func clusterFromProject(project string) string { + pieces := strings.Split(project, ":") + return pieces[0] +} diff --git a/pkg/authentication/rolegetter.go b/pkg/authentication/rolegetter.go new file mode 100644 index 000000000..f32c38a51 --- /dev/null +++ b/pkg/authentication/rolegetter.go @@ -0,0 +1,43 @@ +package authentication + +import ( + "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RBACRestGetter struct { + rbac.Interface +} + +func (r RBACRestGetter) GetRole(namespace, name string) (*rbacv1.Role, error) { + return r.Interface.V1().Role().Get(namespace, name, metav1.GetOptions{}) +} + +func (r RBACRestGetter) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) { + rolebindings, err := r.Interface.V1().RoleBinding().List(namespace, metav1.ListOptions{}) + if err != nil { + return nil, err + } + var rbs []*rbacv1.RoleBinding + for i := range rolebindings.Items { + rbs = append(rbs, &rolebindings.Items[i]) + } + return rbs, nil +} + +func (r RBACRestGetter) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { + return r.Interface.V1().ClusterRole().Get(name, metav1.GetOptions{}) +} + +func (r RBACRestGetter) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) { + clusterrolebindings, err := r.Interface.V1().ClusterRoleBinding().List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + var crbs []*rbacv1.ClusterRoleBinding + for i := range clusterrolebindings.Items { + crbs = append(crbs, &clusterrolebindings.Items[i]) + } + return crbs, nil +} diff --git a/pkg/admission/cluster.go b/pkg/cluster/cluster.go similarity index 96% rename from pkg/admission/cluster.go rename to pkg/cluster/cluster.go index e3f55ec7e..1e4be72f3 100644 --- a/pkg/admission/cluster.go +++ b/pkg/cluster/cluster.go @@ -1,4 +1,4 @@ -package admission +package cluster import ( "net/http" @@ -12,7 +12,7 @@ import ( authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" ) -func newClusterValidator(sar authorizationv1.SubjectAccessReviewInterface) webhook.Handler { +func NewClusterValidator(sar authorizationv1.SubjectAccessReviewInterface) webhook.Handler { return &clusterValidator{ sar: sar, } diff --git a/pkg/codegen/cleanup/main.go b/pkg/codegen/cleanup/main.go new file mode 100644 index 000000000..68a6b25a9 --- /dev/null +++ b/pkg/codegen/cleanup/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +func main() { + if err := os.RemoveAll("./pkg/generated"); err != nil { + logrus.Fatal(err) + } +} diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go new file mode 100644 index 000000000..364c99753 --- /dev/null +++ b/pkg/codegen/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + controllergen "github.com/rancher/wrangler/pkg/controller-gen" + "github.com/rancher/wrangler/pkg/controller-gen/args" +) + +func main() { + os.Unsetenv("GOPATH") + controllergen.Run(args.Options{ + OutputPackage: "github.com/rancher/webhook/pkg/generated", + Boilerplate: "scripts/boilerplate.go.txt", + Groups: map[string]args.Group{ + "management.cattle.io": { + Types: []interface{}{ + v3.GlobalRole{}, + }, + }, + }, + }) +}