shdsbilling/on-premise/main/rancher_v2.9.1/source/rancher-2.9.1/pkg/impersonation/impersonation.go

453 lines
16 KiB
Go

// Package impersonation sets up service accounts that are permitted to act on behalf of a Rancher user on a cluster.
package impersonation
import (
"context"
"fmt"
"reflect"
"sort"
"time"
authcommon "github.com/rancher/rancher/pkg/auth/providers/common"
v3 "github.com/rancher/rancher/pkg/generated/norman/management.cattle.io/v3"
"github.com/rancher/rancher/pkg/serviceaccounttoken"
"github.com/rancher/rancher/pkg/types/config"
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/util/retry"
)
const (
impersonationLabel = "authz.cluster.cattle.io/impersonator"
// ImpersonationNamespace is the namespace where impersonation service accounts live.
ImpersonationNamespace = "cattle-impersonation-system"
// ImpersonationPrefix is the prefix for impersonation roles, bindings, and service accounts.
ImpersonationPrefix = "cattle-impersonation-"
)
// Impersonator contains data for the user being impersonated.
type Impersonator struct {
user user.Info
clusterContext *config.UserContext
userLister v3.UserLister
userAttributeLister v3.UserAttributeLister
secretsCache corecontrollers.SecretCache
}
// New creates an Impersonator from a kubernetes user.Info object and a UserContext for the cluster.
func New(userInfo user.Info, clusterContext *config.UserContext) (Impersonator, error) {
impersonator := Impersonator{
clusterContext: clusterContext,
userLister: clusterContext.Management.Management.Users("").Controller().Lister(),
userAttributeLister: clusterContext.Management.Management.UserAttributes("").Controller().Lister(),
secretsCache: clusterContext.Corew.Secret().Cache(),
}
user, err := impersonator.getUser(userInfo)
impersonator.user = user
if err != nil {
return Impersonator{}, err
}
return impersonator, nil
}
// SetUpImpersonation creates a service account on a cluster with a clusterrole and clusterrolebinding allowing it to impersonate a Rancher user.
// Returns a reference to the service account, which can be used by GetToken to retrieve the account token, or an error if creating any of the resources failed.
func (i *Impersonator) SetUpImpersonation() (*corev1.ServiceAccount, error) {
rules := i.rulesForUser()
logrus.Tracef("impersonation: checking role for user %s", i.user.GetName())
role, err := i.checkAndUpdateRole(rules)
if err != nil {
return nil, err
}
roleBinding, err := i.getRoleBinding()
if err != nil && !apierrors.IsNotFound(err) {
return nil, err
}
if role != nil && roleBinding != nil {
sa, err := i.getServiceAccount()
// in case the role exists but we were interrupted before creating the service account, proceed to create resources
if err == nil || !apierrors.IsNotFound(err) {
return sa, err
}
}
logrus.Tracef("impersonation: creating impersonation namespace")
err = i.createNamespace()
if err != nil {
return nil, err
}
logrus.Tracef("impersonation: creating role for user %s", i.user.GetName())
role, err = i.createRole(rules)
if err != nil {
return nil, err
}
logrus.Tracef("impersonation: creating service account for user %s", i.user.GetName())
sa, err := i.createServiceAccount(role)
if err != nil {
return nil, err
}
logrus.Tracef("impersonation: creating role binding for user %s", i.user.GetName())
err = i.createRoleBinding(role, sa)
if err != nil {
return nil, err
}
logrus.Tracef("impersonation: waiting for service account to become active for user %s", i.user.GetName())
return i.waitForServiceAccount(sa)
}
// GetToken accepts a service account and returns the service account's token.
func (i *Impersonator) GetToken(sa *corev1.ServiceAccount) (string, error) {
secret, err := serviceaccounttoken.EnsureSecretForServiceAccount(context.Background(), i.secretsCache, i.clusterContext.K8sClient, sa)
if err != nil {
return "", fmt.Errorf("error getting secret: %w", err)
}
token, ok := secret.Data["token"]
if !ok {
return "", fmt.Errorf("error getting token: invalid secret object")
}
return string(token), nil
}
func (i *Impersonator) getServiceAccount() (*corev1.ServiceAccount, error) {
name := ImpersonationPrefix + i.user.GetUID()
sa, err := i.clusterContext.Core.ServiceAccounts("").Controller().Lister().Get(ImpersonationNamespace, name)
if err != nil {
if logrus.GetLevel() >= logrus.TraceLevel {
logrus.Tracef("impersonation: error getting service account %s/%s: %v", ImpersonationNamespace, name, err)
sas, debugErr := i.clusterContext.Core.ServiceAccounts("").Controller().Lister().List(ImpersonationNamespace, labels.NewSelector())
if i.clusterContext == nil {
logrus.Tracef("impersonation: cluster context is empty")
} else {
logrus.Tracef("impersonation: using context for cluster %s", i.clusterContext.ClusterName)
}
if debugErr != nil {
logrus.Tracef("impersonation: encountered error listing cached service accounts: %v", debugErr)
} else {
logrus.Tracef("impersonation: cached service accounts: %+v", sas)
}
}
return nil, fmt.Errorf("failed to get service account: %s/%s, error: %w", ImpersonationNamespace, name, err)
}
return sa, nil
}
func (i *Impersonator) createServiceAccount(role *rbacv1.ClusterRole) (*corev1.ServiceAccount, error) {
name := ImpersonationPrefix + i.user.GetUID()
sa, err := i.clusterContext.Core.ServiceAccounts("").Controller().Lister().Get(ImpersonationNamespace, name)
if err != nil && !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("impersonation: error getting service account [%s:%s]: %w", ImpersonationNamespace, name, err)
}
if apierrors.IsNotFound(err) {
logrus.Debugf("impersonation: creating service account %s", name)
sa, err = i.clusterContext.Core.ServiceAccounts(ImpersonationNamespace).Create(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
impersonationLabel: "true",
},
// Use the clusterrole as the owner for the purposes of automatic cleanup
OwnerReferences: []metav1.OwnerReference{{
Name: role.Name,
UID: role.UID,
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
}},
},
})
if apierrors.IsAlreadyExists(err) {
// in case cache isn't synced yet, use raw client
sa, err = i.clusterContext.Core.ServiceAccounts(ImpersonationNamespace).Get(name, metav1.GetOptions{})
}
if err != nil {
return nil, fmt.Errorf("impersonation: error getting service account [%s:%s]: %w", ImpersonationNamespace, name, err)
}
}
// create secret for service account if it was not automatically generated
_, err = serviceaccounttoken.EnsureSecretForServiceAccount(context.Background(), i.secretsCache, i.clusterContext.K8sClient, sa)
if err != nil {
return nil, fmt.Errorf("impersonation: error ensuring secret for service account %s: %w", name, err)
}
return sa, nil
}
func (i *Impersonator) createNamespace() error {
_, err := i.clusterContext.Core.Namespaces("").Controller().Lister().Get("", ImpersonationNamespace)
if apierrors.IsNotFound(err) {
logrus.Debugf("impersonation: creating namespace %s", ImpersonationNamespace)
_, err = i.clusterContext.Core.Namespaces("").Create(&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ImpersonationNamespace,
Labels: map[string]string{
impersonationLabel: "true",
},
},
})
if apierrors.IsAlreadyExists(err) {
return nil
}
}
return err
}
// checkAndUpdateRole checks whether the impersonation clusterrole already exists and whether it has the correct rules.
// If the role does not exist, the method returns nil for the role and createRole must be called.
// If the role does exist, the rules are updated if necessary and a reference to the role is returned.
func (i *Impersonator) checkAndUpdateRole(rules []rbacv1.PolicyRule) (*rbacv1.ClusterRole, error) {
name := ImpersonationPrefix + i.user.GetUID()
var role *rbacv1.ClusterRole
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
var err error
role, err = i.clusterContext.RBAC.ClusterRoles("").Controller().Lister().Get("", name)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if !reflect.DeepEqual(role.Rules, rules) {
role.Rules = rules
role, err = i.clusterContext.RBAC.ClusterRoles("").Update(role)
return err
}
return nil
})
if err != nil {
return nil, err
}
return role, nil
}
func (i *Impersonator) createRole(rules []rbacv1.PolicyRule) (*rbacv1.ClusterRole, error) {
name := ImpersonationPrefix + i.user.GetUID()
role, err := i.clusterContext.RBAC.ClusterRoles("").Controller().Lister().Get("", name)
if apierrors.IsNotFound(err) {
logrus.Debugf("impersonation: creating role %s", name)
role, err = i.clusterContext.RBAC.ClusterRoles("").Create(&rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: ImpersonationPrefix + i.user.GetUID(),
Labels: map[string]string{
impersonationLabel: "true",
},
},
Rules: rules,
AggregationRule: nil,
})
if apierrors.IsAlreadyExists(err) {
// in case cache isn't synced yet, use raw client
return i.clusterContext.RBAC.ClusterRoles("").Get(name, metav1.GetOptions{})
}
return role, nil
}
return role, err
}
func (i *Impersonator) rulesForUser() []rbacv1.PolicyRule {
rules := []rbacv1.PolicyRule{{
Verbs: []string{"impersonate"},
APIGroups: []string{""},
Resources: []string{"users"},
ResourceNames: []string{i.user.GetUID()},
}}
if groups := i.user.GetGroups(); len(groups) > 0 {
rules = append(rules, rbacv1.PolicyRule{
Verbs: []string{"impersonate"},
APIGroups: []string{""},
Resources: []string{"groups"},
ResourceNames: groups,
})
}
extras := i.user.GetExtra()
if principalids, ok := extras[authcommon.UserAttributePrincipalID]; ok {
rules = append(rules, rbacv1.PolicyRule{
Verbs: []string{"impersonate"},
APIGroups: []string{"authentication.k8s.io"},
Resources: []string{"userextras/principalid"},
ResourceNames: principalids,
})
}
if usernames, ok := extras[authcommon.UserAttributeUserName]; ok {
rules = append(rules, rbacv1.PolicyRule{
Verbs: []string{"impersonate"},
APIGroups: []string{"authentication.k8s.io"},
Resources: []string{"userextras/username"},
ResourceNames: usernames,
})
}
return rules
}
func (i *Impersonator) getRoleBinding() (*rbacv1.ClusterRoleBinding, error) {
name := ImpersonationPrefix + i.user.GetUID()
return i.clusterContext.RBAC.ClusterRoleBindings("").Controller().Lister().Get("", name)
}
func (i *Impersonator) createRoleBinding(role *rbacv1.ClusterRole, sa *corev1.ServiceAccount) error {
name := ImpersonationPrefix + i.user.GetUID()
_, err := i.clusterContext.RBAC.ClusterRoleBindings("").Controller().Lister().Get("", name)
if apierrors.IsNotFound(err) {
logrus.Debugf("impersonation: creating role binding %s", name)
_, err = i.clusterContext.RBAC.ClusterRoleBindings("").Create(&rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: name,
// Use the clusterrole as the owner for the purposes of automatic cleanup
OwnerReferences: []metav1.OwnerReference{{
Name: role.Name,
UID: role.UID,
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
}},
Labels: map[string]string{
impersonationLabel: "true",
},
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
APIGroup: "",
Name: sa.Name,
Namespace: sa.Namespace,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: role.Name,
},
})
if apierrors.IsAlreadyExists(err) {
return nil
}
}
return err
}
func (i *Impersonator) waitForServiceAccount(sa *corev1.ServiceAccount) (*corev1.ServiceAccount, error) {
logrus.Debugf("impersonation: waiting for service account %s/%s to be ready", sa.Namespace, sa.Name)
backoff := wait.Backoff{
Duration: 200 * time.Millisecond,
Factor: 1,
Jitter: 0,
Steps: 10,
}
var ret *corev1.ServiceAccount
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
var err error
ret, err = i.clusterContext.Core.ServiceAccounts("").Controller().Lister().Get(ImpersonationNamespace, sa.Name)
if apierrors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
secret, err := serviceaccounttoken.ServiceAccountSecret(context.Background(), sa, i.secretsCache.List, i.clusterContext.K8sClient.CoreV1().Secrets(sa.Namespace))
if err != nil {
return false, err
}
if secret == nil {
return false, nil
}
if _, found := secret.Data[corev1.ServiceAccountTokenKey]; found {
return true, nil
}
return false, nil
})
if err != nil {
if logrus.GetLevel() >= logrus.TraceLevel {
logrus.Tracef("impersonation: error waiting for service account %s/%s: %v", sa.Namespace, sa.Name, err)
sas, debugErr := i.clusterContext.Core.ServiceAccounts("").Controller().Lister().List(ImpersonationNamespace, labels.NewSelector())
if i.clusterContext == nil {
logrus.Tracef("impersonation: cluster context is empty")
} else {
logrus.Tracef("impersonation: using context for cluster %s", i.clusterContext.ClusterName)
}
if debugErr != nil {
logrus.Tracef("impersonation: encountered error listing cached service accounts: %v", debugErr)
} else {
logrus.Tracef("impersonation: cached service accounts: %+v", sas)
}
}
return nil, fmt.Errorf("failed to get secret for service account: %s/%s, error: %w", sa.Namespace, sa.Name, err)
}
return ret, nil
}
func (i *Impersonator) getUser(userInfo user.Info) (user.Info, error) {
u, err := i.userLister.Get("", userInfo.GetUID())
if err != nil {
return &user.DefaultInfo{}, err
}
groups := []string{"system:authenticated", "system:cattle:authenticated"}
extras := make(map[string][]string)
attribs, err := i.userAttributeLister.Get("", userInfo.GetUID())
if err != nil && !apierrors.IsNotFound(err) {
return &user.DefaultInfo{}, err
}
if attribs == nil { // system users do not have userattributes, but principalid and username are on the user
// See https://github.com/rancher/rancher/blob/7ce603ea90ca656f5baa29b0149c19c8d7f73e8f/pkg/auth/requests/authenticate.go#L185-L194
// If the extras are not in userattributes, use displayName and principalIDs from the user.
if u.DisplayName != "" {
extras[authcommon.UserAttributeUserName] = []string{u.DisplayName}
}
if len(u.PrincipalIDs) > 0 {
extras[authcommon.UserAttributePrincipalID] = u.PrincipalIDs
}
} else { // real users have groups and extras in userattributes
for _, gps := range attribs.GroupPrincipals {
for _, groupPrincipal := range gps.Items {
if !isInList(groupPrincipal.Name, groups) {
groups = append(groups, groupPrincipal.Name)
}
}
}
for _, exs := range attribs.ExtraByProvider {
if usernames, ok := exs[authcommon.UserAttributeUserName]; ok && len(usernames) > 0 {
if _, ok := extras[authcommon.UserAttributeUserName]; !ok {
extras[authcommon.UserAttributeUserName] = make([]string, 0)
}
extras[authcommon.UserAttributeUserName] = append(extras[authcommon.UserAttributeUserName], usernames...)
}
if principalids, ok := exs[authcommon.UserAttributePrincipalID]; ok && len(principalids) > 0 {
if _, ok := extras[authcommon.UserAttributePrincipalID]; !ok {
extras[authcommon.UserAttributePrincipalID] = make([]string, 0)
}
extras[authcommon.UserAttributePrincipalID] = append(extras[authcommon.UserAttributePrincipalID], principalids...)
}
}
}
// sort to make comparable
sort.Strings(groups)
if _, ok := extras[authcommon.UserAttributeUserName]; ok {
sort.Strings(extras[authcommon.UserAttributeUserName])
}
if _, ok := extras[authcommon.UserAttributePrincipalID]; ok {
sort.Strings(extras[authcommon.UserAttributePrincipalID])
}
user := &user.DefaultInfo{
UID: u.GetName(),
Name: u.Username,
Groups: groups,
Extra: extras,
}
return user, nil
}
func isInList(item string, list []string) bool {
for _, s := range list {
if item == s {
return true
}
}
return false
}