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

     1  package project
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/argoproj/pkg/sync"
    10  	"github.com/dgrijalva/jwt-go/v4"
    11  	"github.com/google/uuid"
    12  	log "github.com/sirupsen/logrus"
    13  	"google.golang.org/grpc/codes"
    14  	"google.golang.org/grpc/status"
    15  	v1 "k8s.io/api/core/v1"
    16  	apierr "k8s.io/apimachinery/pkg/api/errors"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	"k8s.io/apimachinery/pkg/fields"
    19  	"k8s.io/client-go/kubernetes"
    20  	"k8s.io/client-go/tools/cache"
    21  
    22  	"github.com/argoproj/argo-cd/common"
    23  	"github.com/argoproj/argo-cd/pkg/apiclient/project"
    24  	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    25  	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
    26  	listersv1alpha1 "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
    27  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    28  	"github.com/argoproj/argo-cd/util/argo"
    29  	jwtutil "github.com/argoproj/argo-cd/util/jwt"
    30  	"github.com/argoproj/argo-cd/util/rbac"
    31  	"github.com/argoproj/argo-cd/util/session"
    32  	"github.com/argoproj/argo-cd/util/settings"
    33  )
    34  
    35  const (
    36  	// JWTTokenSubFormat format of the JWT token subject that Argo CD vends out.
    37  	JWTTokenSubFormat = "proj:%s:%s"
    38  )
    39  
    40  // Server provides a Project service
    41  type Server struct {
    42  	ns            string
    43  	enf           *rbac.Enforcer
    44  	policyEnf     *rbacpolicy.RBACPolicyEnforcer
    45  	appclientset  appclientset.Interface
    46  	kubeclientset kubernetes.Interface
    47  	auditLogger   *argo.AuditLogger
    48  	projectLock   sync.KeyLock
    49  	sessionMgr    *session.SessionManager
    50  	projInformer  cache.SharedIndexInformer
    51  	settingsMgr   *settings.SettingsManager
    52  }
    53  
    54  // NewServer returns a new instance of the Project service
    55  func NewServer(ns string, kubeclientset kubernetes.Interface, appclientset appclientset.Interface, enf *rbac.Enforcer, projectLock sync.KeyLock, sessionMgr *session.SessionManager, policyEnf *rbacpolicy.RBACPolicyEnforcer,
    56  	projInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager) *Server {
    57  	auditLogger := argo.NewAuditLogger(ns, kubeclientset, "argocd-server")
    58  	return &Server{enf: enf, policyEnf: policyEnf, appclientset: appclientset, kubeclientset: kubeclientset, ns: ns, projectLock: projectLock, auditLogger: auditLogger, sessionMgr: sessionMgr,
    59  		projInformer: projInformer, settingsMgr: settingsMgr}
    60  }
    61  
    62  func validateProject(proj *v1alpha1.AppProject) error {
    63  	err := proj.ValidateProject()
    64  	if err != nil {
    65  		return err
    66  	}
    67  	err = rbac.ValidatePolicy(proj.ProjectPoliciesString())
    68  	if err != nil {
    69  		return status.Errorf(codes.InvalidArgument, "policy syntax error: %s", err.Error())
    70  	}
    71  	return nil
    72  }
    73  
    74  // CreateToken creates a new token to access a project
    75  func (s *Server) CreateToken(ctx context.Context, q *project.ProjectTokenCreateRequest) (*project.ProjectTokenResponse, error) {
    76  	prj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project, metav1.GetOptions{})
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	err = validateProject(prj)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	s.projectLock.Lock(q.Project)
    86  	defer s.projectLock.Unlock(q.Project)
    87  
    88  	role, _, err := prj.GetRoleByName(q.Role)
    89  	if err != nil {
    90  		return nil, status.Errorf(codes.NotFound, "project '%s' does not have role '%s'", q.Project, q.Role)
    91  	}
    92  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionUpdate, q.Project); err != nil {
    93  		if !jwtutil.IsMember(jwtutil.Claims(ctx.Value("claims")), role.Groups, s.policyEnf.GetScopes()) {
    94  			return nil, err
    95  		}
    96  	}
    97  	id := q.Id
    98  	if err := prj.ValidateJWTTokenID(q.Role, q.Id); err != nil {
    99  		return nil, status.Errorf(codes.InvalidArgument, err.Error())
   100  	}
   101  	if id == "" {
   102  		uniqueId, _ := uuid.NewRandom()
   103  		id = uniqueId.String()
   104  	}
   105  	subject := fmt.Sprintf(JWTTokenSubFormat, q.Project, q.Role)
   106  	jwtToken, err := s.sessionMgr.Create(subject, q.ExpiresIn, id)
   107  	if err != nil {
   108  		return nil, status.Error(codes.InvalidArgument, err.Error())
   109  	}
   110  	parser := &jwt.Parser{
   111  		ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
   112  	}
   113  	claims := jwt.StandardClaims{}
   114  	_, _, err = parser.ParseUnverified(jwtToken, &claims)
   115  	if err != nil {
   116  		return nil, status.Error(codes.InvalidArgument, err.Error())
   117  	}
   118  	var issuedAt, expiresAt int64
   119  	if claims.IssuedAt != nil {
   120  		issuedAt = claims.IssuedAt.Unix()
   121  	}
   122  	if claims.ExpiresAt != nil {
   123  		expiresAt = claims.ExpiresAt.Unix()
   124  	}
   125  	id = claims.ID
   126  
   127  	items := append(prj.Status.JWTTokensByRole[q.Role].Items, v1alpha1.JWTToken{IssuedAt: issuedAt, ExpiresAt: expiresAt, ID: id})
   128  	if _, found := prj.Status.JWTTokensByRole[q.Role]; found {
   129  		prj.Status.JWTTokensByRole[q.Role] = v1alpha1.JWTTokens{Items: items}
   130  	} else {
   131  		tokensMap := make(map[string]v1alpha1.JWTTokens)
   132  		tokensMap[q.Role] = v1alpha1.JWTTokens{Items: items}
   133  		prj.Status.JWTTokensByRole = tokensMap
   134  	}
   135  
   136  	prj.NormalizeJWTTokens()
   137  
   138  	_, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, prj, metav1.UpdateOptions{})
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	s.logEvent(prj, ctx, argo.EventReasonResourceCreated, "created token")
   143  	return &project.ProjectTokenResponse{Token: jwtToken}, nil
   144  
   145  }
   146  
   147  // DeleteToken deletes a token in a project
   148  func (s *Server) DeleteToken(ctx context.Context, q *project.ProjectTokenDeleteRequest) (*project.EmptyResponse, error) {
   149  	prj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project, metav1.GetOptions{})
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	err = validateProject(prj)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	s.projectLock.Lock(q.Project)
   159  	defer s.projectLock.Unlock(q.Project)
   160  
   161  	role, roleIndex, err := prj.GetRoleByName(q.Role)
   162  	if err != nil {
   163  		return &project.EmptyResponse{}, nil
   164  	}
   165  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionUpdate, q.Project); err != nil {
   166  		if !jwtutil.IsMember(jwtutil.Claims(ctx.Value("claims")), role.Groups, s.policyEnf.GetScopes()) {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	err = prj.RemoveJWTToken(roleIndex, q.Iat, q.Id)
   172  	if err != nil {
   173  		return &project.EmptyResponse{}, nil
   174  	}
   175  
   176  	_, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, prj, metav1.UpdateOptions{})
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	s.logEvent(prj, ctx, argo.EventReasonResourceDeleted, "deleted token")
   181  
   182  	return &project.EmptyResponse{}, nil
   183  }
   184  
   185  // Create a new project
   186  func (s *Server) Create(ctx context.Context, q *project.ProjectCreateRequest) (*v1alpha1.AppProject, error) {
   187  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionCreate, q.Project.Name); err != nil {
   188  		return nil, err
   189  	}
   190  	q.Project.NormalizePolicies()
   191  	err := validateProject(q.Project)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Create(ctx, q.Project, metav1.CreateOptions{})
   196  	if apierr.IsAlreadyExists(err) {
   197  		existing, getErr := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project.Name, metav1.GetOptions{})
   198  		if getErr != nil {
   199  			return nil, status.Errorf(codes.Internal, "unable to check existing project details: %v", getErr)
   200  		}
   201  		if q.GetUpsert() {
   202  			if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionUpdate, q.GetProject().Name); err != nil {
   203  				return nil, err
   204  			}
   205  			existing.Spec = q.GetProject().Spec
   206  			res, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, existing, metav1.UpdateOptions{})
   207  		} else {
   208  			if !reflect.DeepEqual(existing.Spec, q.GetProject().Spec) {
   209  				return nil, status.Errorf(codes.InvalidArgument, "existing project spec is different, use upsert flag to force update")
   210  			}
   211  			return existing, nil
   212  		}
   213  	}
   214  	if err == nil {
   215  		s.logEvent(res, ctx, argo.EventReasonResourceCreated, "created project")
   216  	}
   217  	return res, err
   218  }
   219  
   220  // List returns list of projects
   221  func (s *Server) List(ctx context.Context, q *project.ProjectQuery) (*v1alpha1.AppProjectList, error) {
   222  	list, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).List(ctx, metav1.ListOptions{})
   223  	if list != nil {
   224  		newItems := make([]v1alpha1.AppProject, 0)
   225  		for i := range list.Items {
   226  			project := list.Items[i]
   227  			if s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionGet, project.Name) {
   228  				newItems = append(newItems, project)
   229  			}
   230  		}
   231  		list.Items = newItems
   232  	}
   233  	return list, err
   234  }
   235  
   236  // Get returns a project by name
   237  func (s *Server) Get(ctx context.Context, q *project.ProjectQuery) (*v1alpha1.AppProject, error) {
   238  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionGet, q.Name); err != nil {
   239  		return nil, err
   240  	}
   241  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	proj.NormalizeJWTTokens()
   246  	return proj, err
   247  }
   248  
   249  // GetGlobalProjects returns global projects
   250  func (s *Server) GetGlobalProjects(ctx context.Context, q *project.ProjectQuery) (*project.GlobalProjectsResponse, error) {
   251  	projOrig, err := s.Get(ctx, q)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	globalProjects := argo.GetGlobalProjects(projOrig, listersv1alpha1.NewAppProjectLister(s.projInformer.GetIndexer()), s.settingsMgr)
   257  
   258  	res := &project.GlobalProjectsResponse{}
   259  	res.Items = globalProjects
   260  	return res, nil
   261  }
   262  
   263  // Update updates a project
   264  func (s *Server) Update(ctx context.Context, q *project.ProjectUpdateRequest) (*v1alpha1.AppProject, error) {
   265  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionUpdate, q.Project.Name); err != nil {
   266  		return nil, err
   267  	}
   268  	q.Project.NormalizePolicies()
   269  	q.Project.NormalizeJWTTokens()
   270  	err := validateProject(q.Project)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	s.projectLock.Lock(q.Project.Name)
   275  	defer s.projectLock.Unlock(q.Project.Name)
   276  
   277  	oldProj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project.Name, metav1.GetOptions{})
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	for _, cluster := range difference(q.Project.Spec.DestinationClusters(), oldProj.Spec.DestinationClusters()) {
   283  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, cluster); err != nil {
   284  			return nil, err
   285  		}
   286  	}
   287  
   288  	for _, repoUrl := range difference(q.Project.Spec.SourceRepos, oldProj.Spec.SourceRepos) {
   289  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceRepositories, rbacpolicy.ActionUpdate, repoUrl); err != nil {
   290  			return nil, err
   291  		}
   292  	}
   293  
   294  	clusterResourceWhitelistsEqual := reflect.DeepEqual(q.Project.Spec.ClusterResourceWhitelist, oldProj.Spec.ClusterResourceWhitelist)
   295  	clusterResourceBlacklistsEqual := reflect.DeepEqual(q.Project.Spec.ClusterResourceBlacklist, oldProj.Spec.ClusterResourceBlacklist)
   296  	namespacesResourceBlacklistsEqual := reflect.DeepEqual(q.Project.Spec.NamespaceResourceBlacklist, oldProj.Spec.NamespaceResourceBlacklist)
   297  	namespacesResourceWhitelistsEqual := reflect.DeepEqual(q.Project.Spec.NamespaceResourceWhitelist, oldProj.Spec.NamespaceResourceWhitelist)
   298  	if !clusterResourceWhitelistsEqual || !clusterResourceBlacklistsEqual || !namespacesResourceBlacklistsEqual || !namespacesResourceWhitelistsEqual {
   299  		for _, cluster := range q.Project.Spec.DestinationClusters() {
   300  			if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, cluster); err != nil {
   301  				return nil, err
   302  			}
   303  		}
   304  	}
   305  
   306  	appsList, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).List(ctx, metav1.ListOptions{})
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	var srcValidatedApps []v1alpha1.Application
   312  	var dstValidatedApps []v1alpha1.Application
   313  
   314  	for _, a := range argo.FilterByProjects(appsList.Items, []string{q.Project.Name}) {
   315  		if oldProj.IsSourcePermitted(a.Spec.Source) {
   316  			srcValidatedApps = append(srcValidatedApps, a)
   317  		}
   318  		if oldProj.IsDestinationPermitted(a.Spec.Destination) {
   319  			dstValidatedApps = append(dstValidatedApps, a)
   320  		}
   321  	}
   322  
   323  	invalidSrcCount := 0
   324  	invalidDstCount := 0
   325  
   326  	for _, a := range srcValidatedApps {
   327  		if !q.Project.IsSourcePermitted(a.Spec.Source) {
   328  			invalidSrcCount++
   329  		}
   330  	}
   331  	for _, a := range dstValidatedApps {
   332  		if !q.Project.IsDestinationPermitted(a.Spec.Destination) {
   333  			invalidDstCount++
   334  		}
   335  	}
   336  
   337  	var parts []string
   338  	if invalidSrcCount > 0 {
   339  		parts = append(parts, fmt.Sprintf("%d applications source became invalid", invalidSrcCount))
   340  	}
   341  	if invalidDstCount > 0 {
   342  		parts = append(parts, fmt.Sprintf("%d applications destination became invalid", invalidDstCount))
   343  	}
   344  	if len(parts) > 0 {
   345  		return nil, status.Errorf(codes.InvalidArgument, "as a result of project update %s", strings.Join(parts, " and "))
   346  	}
   347  
   348  	res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, q.Project, metav1.UpdateOptions{})
   349  	if err == nil {
   350  		s.logEvent(res, ctx, argo.EventReasonResourceUpdated, "updated project")
   351  	}
   352  	return res, err
   353  }
   354  
   355  // Delete deletes a project
   356  func (s *Server) Delete(ctx context.Context, q *project.ProjectQuery) (*project.EmptyResponse, error) {
   357  	if q.Name == common.DefaultAppProjectName {
   358  		return nil, status.Errorf(codes.InvalidArgument, "name '%s' is reserved and cannot be deleted", q.Name)
   359  	}
   360  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionDelete, q.Name); err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	s.projectLock.Lock(q.Name)
   365  	defer s.projectLock.Unlock(q.Name)
   366  
   367  	p, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	appsList, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).List(ctx, metav1.ListOptions{})
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	apps := argo.FilterByProjects(appsList.Items, []string{q.Name})
   377  	if len(apps) > 0 {
   378  		return nil, status.Errorf(codes.InvalidArgument, "project is referenced by %d applications", len(apps))
   379  	}
   380  	err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Delete(ctx, q.Name, metav1.DeleteOptions{})
   381  	if err == nil {
   382  		s.logEvent(p, ctx, argo.EventReasonResourceDeleted, "deleted project")
   383  	}
   384  	return &project.EmptyResponse{}, err
   385  }
   386  
   387  func (s *Server) ListEvents(ctx context.Context, q *project.ProjectQuery) (*v1.EventList, error) {
   388  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionGet, q.Name); err != nil {
   389  		return nil, err
   390  	}
   391  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	fieldSelector := fields.SelectorFromSet(map[string]string{
   396  		"involvedObject.name":      proj.Name,
   397  		"involvedObject.uid":       string(proj.UID),
   398  		"involvedObject.namespace": proj.Namespace,
   399  	}).String()
   400  	return s.kubeclientset.CoreV1().Events(s.ns).List(ctx, metav1.ListOptions{FieldSelector: fieldSelector})
   401  }
   402  
   403  func (s *Server) logEvent(a *v1alpha1.AppProject, ctx context.Context, reason string, action string) {
   404  	eventInfo := argo.EventInfo{Type: v1.EventTypeNormal, Reason: reason}
   405  	user := session.Username(ctx)
   406  	if user == "" {
   407  		user = "Unknown user"
   408  	}
   409  	message := fmt.Sprintf("%s %s", user, action)
   410  	s.auditLogger.LogAppProjEvent(a, eventInfo, message)
   411  }
   412  
   413  func (s *Server) GetSyncWindowsState(ctx context.Context, q *project.SyncWindowsQuery) (*project.SyncWindowsResponse, error) {
   414  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceProjects, rbacpolicy.ActionGet, q.Name); err != nil {
   415  		return nil, err
   416  	}
   417  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   418  
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	res := &project.SyncWindowsResponse{}
   424  
   425  	windows := proj.Spec.SyncWindows.Active()
   426  	if windows.HasWindows() {
   427  		res.Windows = *windows
   428  	} else {
   429  		res.Windows = []*v1alpha1.SyncWindow{}
   430  	}
   431  
   432  	return res, nil
   433  }
   434  
   435  func (s *Server) NormalizeProjs() error {
   436  	projList, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).List(context.Background(), metav1.ListOptions{})
   437  	if err != nil {
   438  		return status.Errorf(codes.Internal, "Error retrieving project list: %s", err.Error())
   439  	}
   440  	for _, proj := range projList.Items {
   441  		for i := 0; i < 3; i++ {
   442  			if proj.NormalizeJWTTokens() {
   443  				_, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(context.Background(), &proj, metav1.UpdateOptions{})
   444  				if err == nil {
   445  					log.Info(fmt.Sprintf("Successfully normalized project %s.", proj.Name))
   446  					break
   447  				}
   448  				if !apierr.IsConflict(err) {
   449  					log.Warn(fmt.Sprintf("Failed normalize project %s", proj.Name))
   450  					break
   451  				}
   452  				projGet, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(context.Background(), proj.Name, metav1.GetOptions{})
   453  				if err != nil {
   454  					return status.Errorf(codes.Internal, "Error retrieving project: %s", err.Error())
   455  				}
   456  				proj = *projGet
   457  				if i == 2 {
   458  					return status.Errorf(codes.Internal, "Failed normalize project %s", proj.Name)
   459  				}
   460  			} else {
   461  				break
   462  			}
   463  		}
   464  	}
   465  	return nil
   466  }