453 lines
16 KiB
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
|
|
}
|