2247d8c3a2
* + 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>
414 lines
13 KiB
Go
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
|
|
}
|