Files
portainer/api/kubernetes/cli/role.go
T
Yi Chen 2247d8c3a2 (feat)k8s/RBAC: Provide Portainer RBAC functionality for Kubernetes endpoints (#35)
* + endpoint and namespace level authorizations
+ user namespace authorization API
+ k8s client setup service account with k8s roles and policies by portainer role
* User authorization changes refresh token cache
* rbac authorizes k8s requests
* CE to EE migrator to include new authorizations

* code clean up
* comments

* * merge in the RestrictDefaultNamespace changes

* - remove unnecessary check for default namespace

* + updates namespace access policies when generating token

* * updates namespace access policies when querying the user namespace endpoint

* + k8s rule in rbac.go for endpoint access test
+ missing k8s cluster rules for different roles

* feat(rbac): update kube rbac

* feat(rbac): use the authorization directive

* feat(rbac): Update namespace access policies when user/team is deleted

* refactor(app): use new angular-multi-select capabilities

* feat(rbac): fix authorizations

* feat(rbac): fix userAccessPolicies update bug

* feat(rbac): add W applications authorizations

* feat(rbac): add application details W authorizations

* feat(rbac): add configurations W autohorizations

* feat(rbac): add configuration details W authorizations

* feat(rbac): add volumes W authorizations

* feat(rbac): add volume details W authorizations

* feat(rbac): add componentstatus to portainer-view role and add cluster/node authorizations

* fix(rbac): disable application note for non authorized user

* fix(rbac): add endpoints list and components status to portainer-basic

* fix(rbac): allow user to access default namespace when restrict default namespace isn't activated

* fix(rbac): remove default namespace from useraccesspolicies when restrict default namespace isn't activated

* fix(rbac): change some things

* fix(rbac): allow standard user to access container console

* - removed unused parameter

* fix(rbac): fix team authorizations

Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
Co-authored-by: xAt0mZ <baron_l@epitech.eu>
2020-11-03 22:08:09 +13:00

414 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,
portainer.K8sRolePortainerView,
},
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{"componentstatuses", "endpoints", "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{"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.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
}
// create all portainer k8s roles (cluster and non-cluster)
func (kcl *KubeClient) createPortainerK8sRoles(namespace string) error {
for roleName, roleConfig := range getPortainerDefaultK8sRoles() {
// skip the system roles
if roleConfig.isSystem {
continue
}
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: string(roleName),
},
Rules: roleConfig.rules,
}
_, err := kcl.cli.RbacV1().Roles(namespace).Create(role)
// 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]
kcl.cli.RbacV1().RoleBindings(namespace).Update(&rb)
continue
}
}
}
return nil
}
// create role binding for a service account
func (kcl *KubeClient) createRoleBinding(
serviceAccountName,
k8sRole string,
namespace string,
) 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) {
// 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: "Role",
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]
kcl.cli.RbacV1().ClusterRoleBindings().Update(&crb)
continue
}
}
}
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
}