// 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 }