github.com/argoproj/argo-cd/v2@v2.10.9/server/account/account.go (about)

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