github.com/argoproj/argo-cd@v1.8.7/server/account/account.go (about)

     1  package account
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/google/uuid"
    10  	log "github.com/sirupsen/logrus"
    11  	"golang.org/x/net/context"
    12  	"google.golang.org/grpc/codes"
    13  	"google.golang.org/grpc/status"
    14  	"k8s.io/kubernetes/pkg/util/slice"
    15  
    16  	"github.com/argoproj/argo-cd/common"
    17  	"github.com/argoproj/argo-cd/pkg/apiclient/account"
    18  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    19  	"github.com/argoproj/argo-cd/util/password"
    20  	"github.com/argoproj/argo-cd/util/rbac"
    21  	"github.com/argoproj/argo-cd/util/session"
    22  	"github.com/argoproj/argo-cd/util/settings"
    23  )
    24  
    25  // Server provides a Session service
    26  type Server struct {
    27  	sessionMgr  *session.SessionManager
    28  	settingsMgr *settings.SettingsManager
    29  	enf         *rbac.Enforcer
    30  }
    31  
    32  // NewServer returns a new instance of the Session service
    33  func NewServer(sessionMgr *session.SessionManager, settingsMgr *settings.SettingsManager, enf *rbac.Enforcer) *Server {
    34  	return &Server{sessionMgr, settingsMgr, enf}
    35  }
    36  
    37  // UpdatePassword updates the password of the currently authenticated account or the account specified in the request.
    38  func (s *Server) UpdatePassword(ctx context.Context, q *account.UpdatePasswordRequest) (*account.UpdatePasswordResponse, error) {
    39  	issuer := session.Iss(ctx)
    40  	username := session.Sub(ctx)
    41  	updatedUsername := username
    42  
    43  	if q.Name != "" {
    44  		updatedUsername = q.Name
    45  	}
    46  	// check for permission is user is trying to change someone else's password
    47  	// assuming user is trying to update someone else if username is different or issuer is not Argo CD
    48  	if updatedUsername != username || issuer != session.SessionManagerClaimsIssuer {
    49  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceAccounts, rbacpolicy.ActionUpdate, q.Name); err != nil {
    50  			return nil, err
    51  		}
    52  	}
    53  
    54  	if issuer == session.SessionManagerClaimsIssuer {
    55  		// local user is changing own password or another user password
    56  
    57  		// user is changing own password.
    58  		// ensure token belongs to a user, not project
    59  		if q.Name == "" && rbacpolicy.IsProjectSubject(username) {
    60  			return nil, status.Errorf(codes.InvalidArgument, "password can only be changed for local users, not user %q", username)
    61  		}
    62  
    63  		err := s.sessionMgr.VerifyUsernamePassword(username, q.CurrentPassword)
    64  		if err != nil {
    65  			return nil, status.Errorf(codes.InvalidArgument, "current password does not match")
    66  		}
    67  	} else {
    68  		// SSO user is changing or local user password
    69  
    70  		iat, err := session.Iat(ctx)
    71  		if err != nil {
    72  			return nil, err
    73  		}
    74  		if time.Since(iat) > common.ChangePasswordSSOTokenMaxAge {
    75  			return nil, errors.New("SSO token is too old. Please use 'argocd relogin' to get a new token.")
    76  		}
    77  	}
    78  
    79  	hashedPassword, err := password.HashPassword(q.NewPassword)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	err = s.settingsMgr.UpdateAccount(updatedUsername, func(acc *settings.Account) error {
    85  		acc.PasswordHash = hashedPassword
    86  		now := time.Now().UTC()
    87  		acc.PasswordMtime = &now
    88  		return nil
    89  	})
    90  
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	if updatedUsername == username {
    96  		log.Infof("user '%s' updated password", username)
    97  	} else {
    98  		log.Infof("user '%s' updated password of user '%s'", username, updatedUsername)
    99  	}
   100  	return &account.UpdatePasswordResponse{}, nil
   101  
   102  }
   103  
   104  // CanI checks if the current account has permission to perform an action
   105  func (s *Server) CanI(ctx context.Context, r *account.CanIRequest) (*account.CanIResponse, error) {
   106  	if !slice.ContainsString(rbacpolicy.Actions, r.Action, nil) {
   107  		return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbacpolicy.Actions, r.Action)
   108  	}
   109  	if !slice.ContainsString(rbacpolicy.Resources, r.Resource, nil) {
   110  		return nil, status.Errorf(codes.InvalidArgument, "%v does not contain %s", rbacpolicy.Resources, r.Resource)
   111  	}
   112  	ok := s.enf.Enforce(ctx.Value("claims"), r.Resource, r.Action, r.Subresource)
   113  	if ok {
   114  		return &account.CanIResponse{Value: "yes"}, nil
   115  	} else {
   116  		return &account.CanIResponse{Value: "no"}, nil
   117  	}
   118  }
   119  
   120  func toApiAccount(name string, a settings.Account) *account.Account {
   121  	var capabilities []string
   122  	for _, c := range a.Capabilities {
   123  		capabilities = append(capabilities, string(c))
   124  	}
   125  	var tokens []*account.Token
   126  	for _, t := range a.Tokens {
   127  		tokens = append(tokens, &account.Token{Id: t.ID, ExpiresAt: t.ExpiresAt, IssuedAt: t.IssuedAt})
   128  	}
   129  	sort.Slice(tokens, func(i, j int) bool {
   130  		return tokens[i].IssuedAt > tokens[j].IssuedAt
   131  	})
   132  	return &account.Account{
   133  		Name:         name,
   134  		Enabled:      a.Enabled,
   135  		Capabilities: capabilities,
   136  		Tokens:       tokens,
   137  	}
   138  }
   139  
   140  func (s *Server) ensureHasAccountPermission(ctx context.Context, action string, account string) error {
   141  	// account has always has access to itself
   142  	if session.Sub(ctx) == account && session.Iss(ctx) == session.SessionManagerClaimsIssuer {
   143  		return nil
   144  	}
   145  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceAccounts, action, account); err != nil {
   146  		return err
   147  	}
   148  	return nil
   149  }
   150  
   151  // ListAccounts returns the list of accounts
   152  func (s *Server) ListAccounts(ctx context.Context, r *account.ListAccountRequest) (*account.AccountsList, error) {
   153  	resp := account.AccountsList{}
   154  	accounts, err := s.settingsMgr.GetAccounts()
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	for name, a := range accounts {
   159  		if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionGet, name); err == nil {
   160  			resp.Items = append(resp.Items, toApiAccount(name, a))
   161  		}
   162  	}
   163  	sort.Slice(resp.Items, func(i, j int) bool {
   164  		return resp.Items[i].Name < resp.Items[j].Name
   165  	})
   166  	return &resp, nil
   167  }
   168  
   169  // GetAccount returns an account
   170  func (s *Server) GetAccount(ctx context.Context, r *account.GetAccountRequest) (*account.Account, error) {
   171  	if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionGet, r.Name); err != nil {
   172  		return nil, err
   173  	}
   174  	a, err := s.settingsMgr.GetAccount(r.Name)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	return toApiAccount(r.Name, *a), nil
   179  }
   180  
   181  // CreateToken creates a token
   182  func (s *Server) CreateToken(ctx context.Context, r *account.CreateTokenRequest) (*account.CreateTokenResponse, error) {
   183  	if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionUpdate, r.Name); err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	id := r.Id
   188  	if id == "" {
   189  		uniqueId, err := uuid.NewRandom()
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		id = uniqueId.String()
   194  	}
   195  
   196  	var tokenString string
   197  	err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error {
   198  		if account.TokenIndex(id) > -1 {
   199  			return fmt.Errorf("account already has token with id '%s'", id)
   200  		}
   201  		if !account.HasCapability(settings.AccountCapabilityApiKey) {
   202  			return fmt.Errorf("account '%s' does not have %s capability", r.Name, settings.AccountCapabilityApiKey)
   203  		}
   204  
   205  		now := time.Now()
   206  		var err error
   207  		tokenString, err = s.sessionMgr.Create(r.Name, r.ExpiresIn, id)
   208  		if err != nil {
   209  			return err
   210  		}
   211  
   212  		var expiresAt int64
   213  		if r.ExpiresIn > 0 {
   214  			expiresAt = now.Add(time.Duration(r.ExpiresIn) * time.Second).Unix()
   215  		}
   216  		account.Tokens = append(account.Tokens, settings.Token{
   217  			ID:        id,
   218  			IssuedAt:  now.Unix(),
   219  			ExpiresAt: expiresAt,
   220  		})
   221  		return nil
   222  	})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return &account.CreateTokenResponse{Token: tokenString}, nil
   227  }
   228  
   229  // DeleteToken deletes a token
   230  func (s *Server) DeleteToken(ctx context.Context, r *account.DeleteTokenRequest) (*account.EmptyResponse, error) {
   231  	if err := s.ensureHasAccountPermission(ctx, rbacpolicy.ActionUpdate, r.Name); err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	err := s.settingsMgr.UpdateAccount(r.Name, func(account *settings.Account) error {
   236  		if index := account.TokenIndex(r.Id); index > -1 {
   237  			account.Tokens = append(account.Tokens[:index], account.Tokens[index+1:]...)
   238  			return nil
   239  		}
   240  		return status.Errorf(codes.NotFound, "token with id '%s' does not exist", r.Id)
   241  	})
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	return &account.EmptyResponse{}, nil
   246  }