Files
portainer/api/kubernetes/cli/role.go
cong meng f9cf76234f feat(rbac): EE-226 Add a new RBAC "Operator" Role (#191)
* feat(rbac): EE-226 Add a new RBAC "Operator" Role

* feat(rbac): EE-226 prioritize Operator after EndpointAdmin and before Helpdesk

* feat(rbac): EE-226 access viewer shows incorrect effective role after introduce of Operator

* feat(rbac): EE-226 show roles order by priority other than name

* feat(rbac): EE-226 remove OperationK8sVolumeDetailsW authorization from operator role

* feat(rbac): EE-226 always increase bucket next sequence when create a role

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-06 11:34:54 +12:00

431 lines
13 KiB
Go

package cli
import (
portainer "github.com/portainer/portainer/api"
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type (
k8sRoleSet struct {
k8sClusterRoles []portainer.K8sRole
k8sRoles []portainer.K8sRole
}
k8sRoleConfig struct {
isSystem bool
rules []rbacv1.PolicyRule
}
)
func getPortainerK8sRoleMapping() map[portainer.RoleID]k8sRoleSet {
return map[portainer.RoleID]k8sRoleSet{
portainer.RoleIDEndpointAdmin: k8sRoleSet{
k8sClusterRoles: []portainer.K8sRole{
portainer.K8sRoleClusterAdmin,
},
},
portainer.RoleIDHelpdesk: k8sRoleSet{
k8sClusterRoles: []portainer.K8sRole{
portainer.K8sRolePortainerHelpdesk,
},
k8sRoles: []portainer.K8sRole{
portainer.K8sRolePortainerView,
},
},
portainer.RoleIDOperator: k8sRoleSet{
k8sClusterRoles: []portainer.K8sRole{
portainer.K8sRolePortainerHelpdesk,
portainer.K8sRolePortainerOperator,
},
k8sRoles: []portainer.K8sRole{
portainer.K8sRolePortainerView,
},
},
portainer.RoleIDStandardUser: k8sRoleSet{
k8sClusterRoles: []portainer.K8sRole{
portainer.K8sRolePortainerBasic,
},
k8sRoles: []portainer.K8sRole{
portainer.K8sRolePortainerEdit,
portainer.K8sRolePortainerView,
},
},
portainer.RoleIDReadonly: k8sRoleSet{
k8sClusterRoles: []portainer.K8sRole{
portainer.K8sRolePortainerBasic,
},
k8sRoles: []portainer.K8sRole{
portainer.K8sRolePortainerView,
},
},
}
}
func getPortainerDefaultK8sRoles() map[portainer.K8sRole]k8sRoleConfig {
return map[portainer.K8sRole]k8sRoleConfig{
portainer.K8sRoleClusterAdmin: k8sRoleConfig{
isSystem: true,
},
portainer.K8sRolePortainerBasic: k8sRoleConfig{
isSystem: false,
rules: []rbacv1.PolicyRule{
{
Verbs: []string{"list"},
Resources: []string{"namespaces", "nodes"},
APIGroups: []string{""},
},
{
Verbs: []string{"list"},
Resources: []string{"storageclasses"},
APIGroups: []string{"storage.k8s.io"},
},
{
Verbs: []string{"list"},
Resources: []string{"ingresses"},
APIGroups: []string{"networking.k8s.io"},
},
},
},
portainer.K8sRolePortainerHelpdesk: k8sRoleConfig{
isSystem: false,
rules: []rbacv1.PolicyRule{
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"componentstatuses", "endpoints", "events", "namespaces", "nodes"},
APIGroups: []string{""},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"storageclasses"},
APIGroups: []string{"storage.k8s.io"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"ingresses"},
APIGroups: []string{"networking.k8s.io"},
},
},
},
portainer.K8sRolePortainerOperator: k8sRoleConfig{
isSystem: false,
rules: []rbacv1.PolicyRule{
{
Verbs: []string{"update"},
Resources: []string{"configmaps", "secrets"},
APIGroups: []string{""},
},
{
Verbs: []string{"delete"},
Resources: []string{"pods"},
APIGroups: []string{""},
},
{
Verbs: []string{"patch"},
Resources: []string{"deployments"},
APIGroups: []string{"apps"},
},
},
},
portainer.K8sRolePortainerEdit: k8sRoleConfig{
isSystem: false,
rules: []rbacv1.PolicyRule{
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"configmaps", "endpoints", "persistentvolumeclaims", "pods", "pods/attach", "pods/exec", "pods/portforward", "pods/proxy", "replicationcontrollers", "replicationcontrollers/scale", "secrets", "serviceaccounts", "services", "services/proxy"},
APIGroups: []string{""},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"pods/attach", "pods/exec", "pods/portforward", "pods/proxy", "secrets", "services/proxy"},
APIGroups: []string{""},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"daemonsets", "deployments", "deployments/rollback", "deployments/scale", "replicasets", "replicasets/scale", "statefulsets", "statefulsets/scale"},
APIGroups: []string{"apps"},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"horizontalpodautoscalers"},
APIGroups: []string{"autoscaling"},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"cronjobs", "jobs"},
APIGroups: []string{"batch"},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"daemonsets", "deployments", "deployments/rollback", "deployments/scale", "ingresses", "networkpolicies", "replicasets", "replicasets/scale", "replicationcontrollers/scale"},
APIGroups: []string{"extensions"},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"ingresses", "networkpolicies"},
APIGroups: []string{"networking.k8s.io"},
},
{
Verbs: []string{"create", "delete", "deletecollection", "patch", "update"},
Resources: []string{"poddisruptionbudgets"},
APIGroups: []string{"policy"},
},
},
},
portainer.K8sRolePortainerView: k8sRoleConfig{
isSystem: false,
rules: []rbacv1.PolicyRule{
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"bindings", "componentstatuses", "configmaps", "endpoints", "events", "limitranges", "namespaces", "namespaces/status", "persistentvolumeclaims", "persistentvolumeclaims/status", "pods", "pods/log", "pods/status", "replicationcontrollers", "replicationcontrollers/scale", "replicationcontrollers/status", "resourcequotas", "resourcequotas/status", "secrets", "serviceaccounts", "services", "services/status"},
APIGroups: []string{""},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"controllerrevisions", "daemonsets", "daemonsets/status", "deployments", "deployments/scale", "deployments/status", "replicasets", "replicasets/scale", "replicasets/status", "statefulsets", "statefulsets/scale", "statefulsets/status"},
APIGroups: []string{"apps"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"horizontalpodautoscalers", "horizontalpodautoscalers/status"},
APIGroups: []string{"autoscaling"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"cronjobs", "cronjobs/status", "jobs", "jobs/status"},
APIGroups: []string{"batch"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"daemonsets", "daemonsets/status", "deployments", "deployments/scale", "deployments/status", "ingresses", "ingresses/status", "networkpolicies", "replicasets", "replicasets/scale", "replicasets/status", "replicationcontrollers/scale"},
APIGroups: []string{"extensions"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"ingresses", "ingresses/status", "networkpolicies"},
APIGroups: []string{"networking.k8s.io"},
},
{
Verbs: []string{"get", "list", "watch"},
Resources: []string{"poddisruptionbudgets", "poddisruptionbudgets/status"},
APIGroups: []string{"policy"},
},
},
},
}
}
// create all portainer k8s roles (cluster and non-cluster)
func (kcl *KubeClient) createPortainerK8sClusterRoles() error {
for roleName, roleConfig := range getPortainerDefaultK8sRoles() {
// skip the system roles
if roleConfig.isSystem {
continue
}
// creates roles as available across cluster.
// NOTE: the roles API are namespaced, thus use the clusterRoles instead.
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: string(roleName),
},
Rules: roleConfig.rules,
}
_, err := kcl.cli.RbacV1().ClusterRoles().Create(clusterRole)
// ignore error if role exists
if err != nil && !k8serrors.IsAlreadyExists(err) {
return err
}
}
return nil
}
// remove all existing role bindings for a service account
func (kcl *KubeClient) removeRoleBindings(
serviceAccountName string,
) error {
namespaces, err := kcl.GetNamespaces()
if err != nil {
return err
}
for ns := range namespaces {
err := kcl.removeRoleBinding(serviceAccountName, ns)
if err != nil {
return err
}
}
return nil
}
// remove a namespace binding for a service account
func (kcl *KubeClient) removeRoleBinding(
serviceAccountName,
namespace string,
) error {
rbList, err := kcl.cli.RbacV1().RoleBindings(namespace).List(metav1.ListOptions{})
if k8serrors.IsNotFound(err) {
return nil
} else if err != nil {
return err
}
for _, rb := range rbList.Items {
for i, subject := range rb.Subjects {
// match the role binding based on its subject and its name
if subject.Kind == "ServiceAccount" &&
subject.Name == serviceAccountName &&
subject.Namespace == portainerNamespace &&
matchRoleBindingName(rb.Name, namespace, kcl.instanceID) {
// swap out the element for deletion
rb.Subjects[i] = rb.Subjects[len(rb.Subjects)-1]
rb.Subjects = rb.Subjects[:len(rb.Subjects)-1]
if len(rb.Subjects) < 1 {
kcl.cli.RbacV1().RoleBindings(namespace).Delete(rb.Name, metav1.NewDeleteOptions(0))
} else {
kcl.cli.RbacV1().RoleBindings(namespace).Update(&rb)
}
break
}
}
}
return nil
}
// create role binding for a service account
func (kcl *KubeClient) createRoleBinding(
serviceAccountName,
k8sRole string,
namespace string,
isClusterRole bool,
) error {
roleBindingName := namespaceRoleBindingName(k8sRole, namespace, kcl.instanceID)
// try find the role binding
roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
roleKind := "Role"
if isClusterRole {
roleKind = "ClusterRole"
}
// create the rolebinding if not exist
roleBinding = &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleBindingName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: portainerNamespace,
},
},
RoleRef: rbacv1.RoleRef{
Kind: roleKind,
Name: k8sRole,
},
}
_, err = kcl.cli.RbacV1().RoleBindings(namespace).Create(roleBinding)
return err
} else if err != nil {
return err
}
for _, subject := range roleBinding.Subjects {
if subject.Kind == "ServiceAccount" &&
subject.Name == serviceAccountName &&
subject.Namespace == portainerNamespace {
// stops if the service account is already bound
return nil
}
}
roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: portainerNamespace,
})
// update the role binding to include the service account
_, err = kcl.cli.RbacV1().RoleBindings(namespace).Update(roleBinding)
return err
}
// remove all existing cluster role bindings for a service account
func (kcl *KubeClient) removeClusterRoleBindings(
serviceAccountName string,
) error {
crbList, err := kcl.cli.RbacV1().ClusterRoleBindings().List(metav1.ListOptions{})
if err != nil {
return err
}
for _, crb := range crbList.Items {
for i, subject := range crb.Subjects {
// match the cluster role binding based on its subject and its name
if subject.Kind == "ServiceAccount" &&
subject.Name == serviceAccountName &&
subject.Namespace == portainerNamespace &&
matchClusterRoleBindingName(crb.Name, kcl.instanceID) {
// swap out the element for deletion
crb.Subjects[i] = crb.Subjects[len(crb.Subjects)-1]
crb.Subjects = crb.Subjects[:len(crb.Subjects)-1]
if len(crb.Subjects) < 1 {
kcl.cli.RbacV1().ClusterRoleBindings().Delete(crb.Name, metav1.NewDeleteOptions(0))
} else {
kcl.cli.RbacV1().ClusterRoleBindings().Update(&crb)
}
break
}
}
}
return nil
}
// create or update the cluster role bindings related to a service account
func (kcl *KubeClient) createClusterRoleBindings(serviceAccountName string,
k8sRole string) error {
crbName := clusterRoleBindingName(k8sRole, kcl.instanceID)
clusterRoleBinding, err := kcl.cli.RbacV1().ClusterRoleBindings().
Get(crbName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
clusterRoleBinding = &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: crbName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: portainerNamespace,
},
},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: k8sRole,
},
}
_, err := kcl.cli.RbacV1().ClusterRoleBindings().Create(clusterRoleBinding)
return err
} else if err != nil {
return err
}
// if the current cluster role binding already has the desired subject, skip it
for _, subject := range clusterRoleBinding.Subjects {
if subject.Kind == "ServiceAccount" &&
subject.Name == serviceAccountName &&
subject.Namespace == portainerNamespace {
return nil
}
}
// otherwise append the subject to the cluster role binding
clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: portainerNamespace,
})
_, err = kcl.cli.RbacV1().ClusterRoleBindings().Update(clusterRoleBinding)
return err
}