diff --git a/cmd/app/run.go b/cmd/app/run.go index b298ae417..7c36adf15 100644 --- a/cmd/app/run.go +++ b/cmd/app/run.go @@ -2,6 +2,7 @@ package app import ( + "fmt" "os" "strconv" @@ -14,11 +15,10 @@ import ( "github.com/Improwised/kube-oidc-proxy/cmd/app/options" "github.com/Improwised/kube-oidc-proxy/pkg/probe" "github.com/Improwised/kube-oidc-proxy/pkg/proxy" + "github.com/Improwised/kube-oidc-proxy/pkg/proxy/rbac" "github.com/Improwised/kube-oidc-proxy/pkg/proxy/subjectaccessreview" "github.com/Improwised/kube-oidc-proxy/pkg/proxy/tokenreview" "github.com/Improwised/kube-oidc-proxy/pkg/util" - - rbacvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" ) func NewRunCommand(stopCh <-chan struct{}) *cobra.Command { @@ -55,17 +55,27 @@ func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Comma return err } - // check if the cluster role config file exists - if _, err := os.Stat(opts.App.Cluster.RoleConfig); err != nil { + // Validate cluster config + if err := clusterConfigValidation(clustersConfig); err != nil { return err } - clustersRoleConfigMap, err := util.LoadRBACConfig(opts.App.Cluster.RoleConfig) - if err != nil { - return err + var clustersRoleConfigMap map[string]util.RBAC + if opts.App.Cluster.RoleConfig != "" { + // check if the cluster role config file exists + if _, err := os.Stat(opts.App.Cluster.RoleConfig); err != nil { + return err + } + + clustersRoleConfigMap, err = util.LoadRBACConfig(opts.App.Cluster.RoleConfig) + if err != nil { + return err + } } + isRBACLoded := make(map[string]bool) for _, cluster := range clustersConfig { + isRBACLoded[cluster.Name] = false ConfigFlags := &genericclioptions.ConfigFlags{ KubeConfig: &cluster.Path, } @@ -118,11 +128,11 @@ func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Comma } subectAccessReviewer, err := subjectaccessreview.New(kubeclient.AuthorizationV1().SubjectAccessReviews()) - kubeclient.AuthorizationV1().RESTClient() if err != nil { return err } + cluster.Kubeclient = kubeclient cluster.SubjectAccessReviewer = subectAccessReviewer } @@ -130,8 +140,20 @@ func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Comma for clusterName, RBACConfig := range clustersRoleConfigMap { for _, cluster := range clustersConfig { if cluster.Name == clusterName { - _, StaticRoles := rbacvalidation.NewTestRuleResolver(RBACConfig.Roles, RBACConfig.RoleBindings, RBACConfig.ClusterRoles, RBACConfig.ClusterRoleBindings) - cluster.Authorizer = util.NewAuthorizer(StaticRoles) + isRBACLoded[cluster.Name] = true + err := rbac.LoadRBAC(RBACConfig, cluster) + if err != nil { + return err + } + } + } + } + + for _, cluster := range clustersConfig { + if !isRBACLoded[cluster.Name] { + err := rbac.LoadRBAC(util.RBAC{}, cluster) + if err != nil { + return err } } } @@ -200,3 +222,18 @@ func LoadClusterConfig(path string) ([]*proxy.ClusterConfig, error) { } return clusterList, nil } + +func clusterConfigValidation(clusterConfig []*proxy.ClusterConfig) error { + // check if the cluster name is not empty and unique + clusterNames := make(map[string]bool) + for _, cluster := range clusterConfig { + if cluster.Name == "" { + return fmt.Errorf("cluster name is empty") + } + if _, ok := clusterNames[cluster.Name]; ok { + return fmt.Errorf("cluster name %s is repeated", cluster.Name) + } + clusterNames[cluster.Name] = true + } + return nil +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index a595552ef..fef0f20fa 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -27,6 +27,7 @@ import ( genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/transport" "k8s.io/klog/v2" @@ -59,6 +60,7 @@ type ClusterConfig struct { Name string Path string RestConfig *rest.Config + Kubeclient *kubernetes.Clientset proxyHandler *httputil.ReverseProxy TokenReviewer *tokenreview.TokenReview SubjectAccessReviewer *subjectaccessreview.SubjectAccessReview diff --git a/pkg/proxy/rbac/rbac.go b/pkg/proxy/rbac/rbac.go new file mode 100644 index 000000000..2b811aa0c --- /dev/null +++ b/pkg/proxy/rbac/rbac.go @@ -0,0 +1,149 @@ +package rbac + +import ( + "context" + "fmt" + + "github.com/Improwised/kube-oidc-proxy/pkg/proxy" + "github.com/Improwised/kube-oidc-proxy/pkg/util" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/rbac/v1" + apisv1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + rbacvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +var defalutRole = map[string]v1.PolicyRule{ + "devops": { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + "developer": { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"pods", "pods/log", "pods/exec"}, + }, + "developer-portforward": { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"pods", "pods/log", "pods/exec", "pods/portforward"}, + }, + "watcher": { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, +} + +func LoadRBAC(RBACConfig util.RBAC, cluster *proxy.ClusterConfig) error { + // Watch for namespace + // if namespace is created then create role and rolebinding + watchNamespace, err := cluster.Kubeclient.CoreV1().Namespaces().Watch(context.Background(), apisv1.ListOptions{Watch: true}) + if err != nil { + return err + } + + for roleName, role := range defalutRole { + // Create ClusterRole + RBACConfig.ClusterRoles = append(RBACConfig.ClusterRoles, &v1.ClusterRole{ + TypeMeta: apisv1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: apisv1.ObjectMeta{ + Name: fmt.Sprintf("%s:%s", cluster.Name, roleName), + }, + Rules: []v1.PolicyRule{role}, + }) + // Create ClusterRoleBinding + RBACConfig.ClusterRoleBindings = append(RBACConfig.ClusterRoleBindings, &v1.ClusterRoleBinding{ + TypeMeta: apisv1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: apisv1.ObjectMeta{ + Name: fmt.Sprintf("%s:%s", cluster.Name, roleName), + }, + Subjects: []v1.Subject{ + { + Kind: "Group", + Name: fmt.Sprintf("%s:%s", cluster.Name, roleName), + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: v1.RoleRef{ + Kind: "ClusterRole", + Name: fmt.Sprintf("%s:%s", cluster.Name, roleName), + }, + }) + } + + go func() { + for e := range watchNamespace.ResultChan() { + switch e.Type { + // If namespace is created then create role and rolebinding + case watch.Added: + for role, policy := range defalutRole { + // Create Role + RBACConfig.Roles = append(RBACConfig.Roles, &v1.Role{ + TypeMeta: apisv1.TypeMeta{ + Kind: "Role", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: apisv1.ObjectMeta{ + Namespace: e.Object.(*corev1.Namespace).Name, + Name: fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name), + }, + Rules: []v1.PolicyRule{policy}, + }) + // Create RoleBinding + RBACConfig.RoleBindings = append(RBACConfig.RoleBindings, &v1.RoleBinding{ + TypeMeta: apisv1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: apisv1.ObjectMeta{ + Namespace: e.Object.(*corev1.Namespace).Name, + Name: fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name), + }, + Subjects: []v1.Subject{ + { + Kind: "Group", + Name: fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name), + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: v1.RoleRef{ + Kind: "Role", + Name: fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name), + }, + }) + } + // If namespace is deleted then delete role and rolebinding + case watch.Deleted: + for role := range defalutRole { + // Delete Role + for i, r := range RBACConfig.Roles { + if r.Name == fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name) { + RBACConfig.Roles = append(RBACConfig.Roles[:i], RBACConfig.Roles[i+1:]...) + } + } + // Delete RoleBinding + for i, r := range RBACConfig.RoleBindings { + if r.Name == fmt.Sprintf("%s:%s:%s", cluster.Name, role, e.Object.(*corev1.Namespace).Name) { + RBACConfig.RoleBindings = append(RBACConfig.RoleBindings[:i], RBACConfig.RoleBindings[i+1:]...) + } + } + } + case watch.Modified: + continue + + } + _, StaticRoles := rbacvalidation.NewTestRuleResolver(RBACConfig.Roles, RBACConfig.RoleBindings, RBACConfig.ClusterRoles, RBACConfig.ClusterRoleBindings) + cluster.Authorizer = util.NewAuthorizer(StaticRoles) + + } + }() + return nil +}