* 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>
431 lines
13 KiB
Go
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
|
|
}
|