github.com/argoproj/argo-cd/v3@v3.2.1/server/project/project.go (about)

     1  package project
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    10  	"github.com/argoproj/pkg/v2/sync"
    11  	"github.com/golang-jwt/jwt/v5"
    12  	"github.com/google/uuid"
    13  	log "github.com/sirupsen/logrus"
    14  	"google.golang.org/grpc/codes"
    15  	"google.golang.org/grpc/status"
    16  	corev1 "k8s.io/api/core/v1"
    17  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/fields"
    20  	"k8s.io/client-go/kubernetes"
    21  	"k8s.io/client-go/tools/cache"
    22  	"k8s.io/client-go/util/retry"
    23  
    24  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    25  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
    26  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    27  	appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
    28  	listersv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    29  	"github.com/argoproj/argo-cd/v3/server/deeplinks"
    30  	"github.com/argoproj/argo-cd/v3/server/rbacpolicy"
    31  	"github.com/argoproj/argo-cd/v3/util/argo"
    32  	"github.com/argoproj/argo-cd/v3/util/db"
    33  	jwtutil "github.com/argoproj/argo-cd/v3/util/jwt"
    34  	"github.com/argoproj/argo-cd/v3/util/rbac"
    35  	"github.com/argoproj/argo-cd/v3/util/session"
    36  	"github.com/argoproj/argo-cd/v3/util/settings"
    37  )
    38  
    39  const (
    40  	// JWTTokenSubFormat format of the JWT token subject that Argo CD vends out.
    41  	JWTTokenSubFormat = "proj:%s:%s"
    42  )
    43  
    44  // Server provides a Project service
    45  type Server struct {
    46  	ns            string
    47  	enf           *rbac.Enforcer
    48  	policyEnf     *rbacpolicy.RBACPolicyEnforcer
    49  	appclientset  appclientset.Interface
    50  	kubeclientset kubernetes.Interface
    51  	auditLogger   *argo.AuditLogger
    52  	projectLock   sync.KeyLock
    53  	sessionMgr    *session.SessionManager
    54  	projInformer  cache.SharedIndexInformer
    55  	settingsMgr   *settings.SettingsManager
    56  	db            db.ArgoDB
    57  }
    58  
    59  // NewServer returns a new instance of the Project service
    60  func NewServer(ns string, kubeclientset kubernetes.Interface, appclientset appclientset.Interface, enf *rbac.Enforcer, projectLock sync.KeyLock, sessionMgr *session.SessionManager, policyEnf *rbacpolicy.RBACPolicyEnforcer,
    61  	projInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, db db.ArgoDB, enableK8sEvent []string,
    62  ) *Server {
    63  	auditLogger := argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent)
    64  	return &Server{
    65  		enf: enf, policyEnf: policyEnf, appclientset: appclientset, kubeclientset: kubeclientset, ns: ns, projectLock: projectLock, auditLogger: auditLogger, sessionMgr: sessionMgr,
    66  		projInformer: projInformer, settingsMgr: settingsMgr, db: db,
    67  	}
    68  }
    69  
    70  func validateProject(proj *v1alpha1.AppProject) error {
    71  	err := proj.ValidateProject()
    72  	if err != nil {
    73  		return err
    74  	}
    75  	err = rbac.ValidatePolicy(proj.ProjectPoliciesString())
    76  	if err != nil {
    77  		return status.Errorf(codes.InvalidArgument, "policy syntax error: %s", err.Error())
    78  	}
    79  	return nil
    80  }
    81  
    82  // CreateToken creates a new token to access a project
    83  func (s *Server) CreateToken(ctx context.Context, q *project.ProjectTokenCreateRequest) (*project.ProjectTokenResponse, error) {
    84  	var resp *project.ProjectTokenResponse
    85  	err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
    86  		var createErr error
    87  		resp, createErr = s.createToken(ctx, q)
    88  		return createErr
    89  	})
    90  	return resp, err
    91  }
    92  
    93  func (s *Server) createToken(ctx context.Context, q *project.ProjectTokenCreateRequest) (*project.ProjectTokenResponse, error) {
    94  	prj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project, metav1.GetOptions{})
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	err = validateProject(prj)
    99  	if err != nil {
   100  		return nil, fmt.Errorf("error validating project: %w", err)
   101  	}
   102  
   103  	s.projectLock.Lock(q.Project)
   104  	defer s.projectLock.Unlock(q.Project)
   105  
   106  	role, _, err := prj.GetRoleByName(q.Role)
   107  	if err != nil {
   108  		return nil, status.Errorf(codes.NotFound, "project '%s' does not have role '%s'", q.Project, q.Role)
   109  	}
   110  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionUpdate, q.Project); err != nil {
   111  		if !jwtutil.IsMember(jwtutil.Claims(ctx.Value("claims")), role.Groups, s.policyEnf.GetScopes()) {
   112  			return nil, err
   113  		}
   114  	}
   115  	id := q.Id
   116  	if err := prj.ValidateJWTTokenID(q.Role, q.Id); err != nil {
   117  		return nil, status.Error(codes.InvalidArgument, err.Error())
   118  	}
   119  	if id == "" {
   120  		uniqueId, _ := uuid.NewRandom()
   121  		id = uniqueId.String()
   122  	}
   123  	subject := fmt.Sprintf(JWTTokenSubFormat, q.Project, q.Role)
   124  	jwtToken, err := s.sessionMgr.Create(subject, q.ExpiresIn, id)
   125  	if err != nil {
   126  		return nil, status.Error(codes.InvalidArgument, err.Error())
   127  	}
   128  	parser := jwt.NewParser(jwt.WithoutClaimsValidation())
   129  	claims := jwt.RegisteredClaims{}
   130  	_, _, err = parser.ParseUnverified(jwtToken, &claims)
   131  	if err != nil {
   132  		return nil, status.Error(codes.InvalidArgument, err.Error())
   133  	}
   134  	var issuedAt, expiresAt int64
   135  	if claims.IssuedAt != nil {
   136  		issuedAt = claims.IssuedAt.Unix()
   137  	}
   138  	if claims.ExpiresAt != nil {
   139  		expiresAt = claims.ExpiresAt.Unix()
   140  	}
   141  	id = claims.ID
   142  
   143  	prj.NormalizeJWTTokens()
   144  
   145  	items := append(prj.Status.JWTTokensByRole[q.Role].Items, v1alpha1.JWTToken{IssuedAt: issuedAt, ExpiresAt: expiresAt, ID: id})
   146  	if _, found := prj.Status.JWTTokensByRole[q.Role]; found {
   147  		prj.Status.JWTTokensByRole[q.Role] = v1alpha1.JWTTokens{Items: items}
   148  	} else {
   149  		tokensMap := make(map[string]v1alpha1.JWTTokens)
   150  		tokensMap[q.Role] = v1alpha1.JWTTokens{Items: items}
   151  		prj.Status.JWTTokensByRole = tokensMap
   152  	}
   153  
   154  	prj.NormalizeJWTTokens()
   155  
   156  	_, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, prj, metav1.UpdateOptions{})
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	s.logEvent(ctx, prj, argo.EventReasonResourceCreated, "created token")
   161  	return &project.ProjectTokenResponse{Token: jwtToken}, nil
   162  }
   163  
   164  func (s *Server) ListLinks(ctx context.Context, q *project.ListProjectLinksRequest) (*application.LinksResponse, error) {
   165  	projName := q.GetName()
   166  
   167  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, projName); err != nil {
   168  		log.WithFields(map[string]any{
   169  			"project": projName,
   170  		}).Warnf("unauthorized access to project, error=%v", err.Error())
   171  		return nil, fmt.Errorf("unauthorized access to project %v", projName)
   172  	}
   173  
   174  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, projName, metav1.GetOptions{})
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// sanitize project jwt tokens
   180  	proj.Status = v1alpha1.AppProjectStatus{}
   181  
   182  	obj, err := kube.ToUnstructured(proj)
   183  	if err != nil {
   184  		return nil, fmt.Errorf("error getting application: %w", err)
   185  	}
   186  
   187  	deepLinks, err := s.settingsMgr.GetDeepLinks(settings.ProjectDeepLinks)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("failed to read application deep links from configmap: %w", err)
   190  	}
   191  
   192  	deeplinksObj := deeplinks.CreateDeepLinksObject(nil, nil, nil, obj)
   193  	finalList, errorList := deeplinks.EvaluateDeepLinksResponse(deeplinksObj, obj.GetName(), deepLinks)
   194  	if len(errorList) > 0 {
   195  		log.Errorf("errorList while evaluating project deep links, %v", strings.Join(errorList, ", "))
   196  	}
   197  
   198  	return finalList, nil
   199  }
   200  
   201  // DeleteToken deletes a token in a project
   202  func (s *Server) DeleteToken(ctx context.Context, q *project.ProjectTokenDeleteRequest) (*project.EmptyResponse, error) {
   203  	var resp *project.EmptyResponse
   204  	err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   205  		var deleteErr error
   206  		resp, deleteErr = s.deleteToken(ctx, q)
   207  		return deleteErr
   208  	})
   209  	return resp, err
   210  }
   211  
   212  func (s *Server) deleteToken(ctx context.Context, q *project.ProjectTokenDeleteRequest) (*project.EmptyResponse, error) {
   213  	prj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project, metav1.GetOptions{})
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	err = validateProject(prj)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("error validating project: %w", err)
   220  	}
   221  
   222  	s.projectLock.Lock(q.Project)
   223  	defer s.projectLock.Unlock(q.Project)
   224  
   225  	role, roleIndex, err := prj.GetRoleByName(q.Role)
   226  	if err != nil {
   227  		return &project.EmptyResponse{}, nil
   228  	}
   229  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionUpdate, q.Project); err != nil {
   230  		if !jwtutil.IsMember(jwtutil.Claims(ctx.Value("claims")), role.Groups, s.policyEnf.GetScopes()) {
   231  			return nil, err
   232  		}
   233  	}
   234  
   235  	err = prj.RemoveJWTToken(roleIndex, q.Iat, q.Id)
   236  	if err != nil {
   237  		return &project.EmptyResponse{}, nil
   238  	}
   239  
   240  	_, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, prj, metav1.UpdateOptions{})
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	s.logEvent(ctx, prj, argo.EventReasonResourceDeleted, "deleted token")
   245  
   246  	return &project.EmptyResponse{}, nil
   247  }
   248  
   249  // Create a new project
   250  func (s *Server) Create(ctx context.Context, q *project.ProjectCreateRequest) (*v1alpha1.AppProject, error) {
   251  	if q.Project == nil {
   252  		return nil, status.Errorf(codes.InvalidArgument, "missing payload 'project' in request")
   253  	}
   254  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionCreate, q.Project.Name); err != nil {
   255  		return nil, err
   256  	}
   257  	q.Project.NormalizePolicies()
   258  	err := validateProject(q.Project)
   259  	if err != nil {
   260  		return nil, fmt.Errorf("error validating project: %w", err)
   261  	}
   262  	res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Create(ctx, q.Project, metav1.CreateOptions{})
   263  	if apierrors.IsAlreadyExists(err) {
   264  		existing, getErr := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project.Name, metav1.GetOptions{})
   265  		if getErr != nil {
   266  			return nil, status.Errorf(codes.Internal, "unable to check existing project details: %v", getErr)
   267  		}
   268  		if !q.GetUpsert() {
   269  			if !reflect.DeepEqual(existing.Spec, q.GetProject().Spec) {
   270  				return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("project", existing.Spec, q.GetProject().Spec))
   271  			}
   272  			return existing, nil
   273  		}
   274  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionUpdate, q.GetProject().Name); err != nil {
   275  			return nil, err
   276  		}
   277  		existing.Spec = q.GetProject().Spec
   278  		res, err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, existing, metav1.UpdateOptions{})
   279  	}
   280  	if err == nil {
   281  		s.logEvent(ctx, res, argo.EventReasonResourceCreated, "created project")
   282  	}
   283  	return res, err
   284  }
   285  
   286  // List returns list of projects
   287  func (s *Server) List(ctx context.Context, _ *project.ProjectQuery) (*v1alpha1.AppProjectList, error) {
   288  	list, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).List(ctx, metav1.ListOptions{})
   289  	if list != nil {
   290  		newItems := make([]v1alpha1.AppProject, 0)
   291  		for i := range list.Items {
   292  			project := list.Items[i]
   293  			if s.enf.Enforce(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, project.Name) {
   294  				newItems = append(newItems, project)
   295  			}
   296  		}
   297  		list.Items = newItems
   298  	}
   299  	return list, err
   300  }
   301  
   302  // GetDetailedProject returns a project with scoped resources
   303  func (s *Server) GetDetailedProject(ctx context.Context, q *project.ProjectQuery) (*project.DetailedProjectsResponse, error) {
   304  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, q.Name); err != nil {
   305  		return nil, err
   306  	}
   307  	proj, repositories, clusters, err := argo.GetAppProjectWithScopedResources(ctx, q.Name, listersv1alpha1.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	proj.NormalizeJWTTokens()
   312  	globalProjects := argo.GetGlobalProjects(proj, listersv1alpha1.NewAppProjectLister(s.projInformer.GetIndexer()), s.settingsMgr)
   313  	var apiRepos []*v1alpha1.Repository
   314  	for _, repo := range repositories {
   315  		apiRepos = append(apiRepos, repo.Normalize().Sanitized())
   316  	}
   317  	var apiClusters []*v1alpha1.Cluster
   318  	for _, cluster := range clusters {
   319  		apiClusters = append(apiClusters, cluster.Sanitized())
   320  	}
   321  
   322  	return &project.DetailedProjectsResponse{
   323  		GlobalProjects: globalProjects,
   324  		Project:        proj,
   325  		Repositories:   apiRepos,
   326  		Clusters:       apiClusters,
   327  	}, err
   328  }
   329  
   330  // Get returns a project by name
   331  func (s *Server) Get(ctx context.Context, q *project.ProjectQuery) (*v1alpha1.AppProject, error) {
   332  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, q.Name); err != nil {
   333  		return nil, err
   334  	}
   335  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	proj.NormalizeJWTTokens()
   340  	return proj, err
   341  }
   342  
   343  // GetGlobalProjects returns global projects
   344  func (s *Server) GetGlobalProjects(ctx context.Context, q *project.ProjectQuery) (*project.GlobalProjectsResponse, error) {
   345  	projOrig, err := s.Get(ctx, q)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	globalProjects := argo.GetGlobalProjects(projOrig, listersv1alpha1.NewAppProjectLister(s.projInformer.GetIndexer()), s.settingsMgr)
   351  
   352  	res := &project.GlobalProjectsResponse{}
   353  	res.Items = globalProjects
   354  	return res, nil
   355  }
   356  
   357  // Update updates a project
   358  func (s *Server) Update(ctx context.Context, q *project.ProjectUpdateRequest) (*v1alpha1.AppProject, error) {
   359  	if q.Project == nil {
   360  		return nil, status.Errorf(codes.InvalidArgument, "missing payload 'project' in request")
   361  	}
   362  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionUpdate, q.Project.Name); err != nil {
   363  		return nil, err
   364  	}
   365  	q.Project.NormalizePolicies()
   366  	q.Project.NormalizeJWTTokens()
   367  	err := validateProject(q.Project)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	s.projectLock.Lock(q.Project.Name)
   372  	defer s.projectLock.Unlock(q.Project.Name)
   373  
   374  	oldProj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Project.Name, metav1.GetOptions{})
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  
   379  	for _, cluster := range difference(q.Project.Spec.DestinationClusters(), oldProj.Spec.DestinationClusters()) {
   380  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionUpdate, cluster); err != nil {
   381  			return nil, err
   382  		}
   383  	}
   384  
   385  	for _, repoURL := range difference(q.Project.Spec.SourceRepos, oldProj.Spec.SourceRepos) {
   386  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionUpdate, repoURL); err != nil {
   387  			return nil, err
   388  		}
   389  	}
   390  
   391  	clusterResourceWhitelistsEqual := reflect.DeepEqual(q.Project.Spec.ClusterResourceWhitelist, oldProj.Spec.ClusterResourceWhitelist)
   392  	clusterResourceBlacklistsEqual := reflect.DeepEqual(q.Project.Spec.ClusterResourceBlacklist, oldProj.Spec.ClusterResourceBlacklist)
   393  	namespacesResourceBlacklistsEqual := reflect.DeepEqual(q.Project.Spec.NamespaceResourceBlacklist, oldProj.Spec.NamespaceResourceBlacklist)
   394  	namespacesResourceWhitelistsEqual := reflect.DeepEqual(q.Project.Spec.NamespaceResourceWhitelist, oldProj.Spec.NamespaceResourceWhitelist)
   395  	if !clusterResourceWhitelistsEqual || !clusterResourceBlacklistsEqual || !namespacesResourceBlacklistsEqual || !namespacesResourceWhitelistsEqual {
   396  		for _, cluster := range q.Project.Spec.DestinationClusters() {
   397  			if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceClusters, rbac.ActionUpdate, cluster); err != nil {
   398  				return nil, err
   399  			}
   400  		}
   401  	}
   402  
   403  	appsList, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).List(ctx, metav1.ListOptions{})
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  
   408  	getProjectClusters := func(project string) ([]*v1alpha1.Cluster, error) {
   409  		return s.db.GetProjectClusters(ctx, project)
   410  	}
   411  
   412  	invalidSrcCount := 0
   413  	invalidDstCount := 0
   414  
   415  	for _, a := range argo.FilterByProjects(appsList.Items, []string{q.Project.Name}) {
   416  		if oldProj.IsSourcePermitted(a.Spec.GetSource()) && !q.Project.IsSourcePermitted(a.Spec.GetSource()) {
   417  			invalidSrcCount++
   418  		}
   419  
   420  		destCluster, err := argo.GetDestinationCluster(ctx, a.Spec.Destination, s.db)
   421  		if err != nil {
   422  			if err.Error() != argo.ErrDestinationMissing {
   423  				// If cluster is not found, we should discard this app, as it's most likely already in error
   424  				continue
   425  			}
   426  			invalidDstCount++
   427  		}
   428  		dstPermitted, err := oldProj.IsDestinationPermitted(destCluster, a.Spec.Destination.Namespace, getProjectClusters)
   429  		if err != nil {
   430  			return nil, err
   431  		}
   432  
   433  		if dstPermitted {
   434  			dstPermitted, err = q.Project.IsDestinationPermitted(destCluster, a.Spec.Destination.Namespace, getProjectClusters)
   435  			if err != nil {
   436  				return nil, err
   437  			}
   438  			if !dstPermitted {
   439  				invalidDstCount++
   440  			}
   441  		}
   442  	}
   443  
   444  	var parts []string
   445  	if invalidSrcCount > 0 {
   446  		parts = append(parts, fmt.Sprintf("%d applications source became invalid", invalidSrcCount))
   447  	}
   448  	if invalidDstCount > 0 {
   449  		parts = append(parts, fmt.Sprintf("%d applications destination became invalid", invalidDstCount))
   450  	}
   451  	if len(parts) > 0 {
   452  		return nil, status.Errorf(codes.InvalidArgument, "as a result of project update %s", strings.Join(parts, " and "))
   453  	}
   454  
   455  	res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(ctx, q.Project, metav1.UpdateOptions{})
   456  	if err == nil {
   457  		s.logEvent(ctx, res, argo.EventReasonResourceUpdated, "updated project")
   458  	}
   459  	return res, err
   460  }
   461  
   462  // Delete deletes a project
   463  func (s *Server) Delete(ctx context.Context, q *project.ProjectQuery) (*project.EmptyResponse, error) {
   464  	if q.Name == v1alpha1.DefaultAppProjectName {
   465  		return nil, status.Errorf(codes.InvalidArgument, "name '%s' is reserved and cannot be deleted", q.Name)
   466  	}
   467  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionDelete, q.Name); err != nil {
   468  		return nil, err
   469  	}
   470  
   471  	s.projectLock.Lock(q.Name)
   472  	defer s.projectLock.Unlock(q.Name)
   473  
   474  	p, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  
   479  	appsList, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).List(ctx, metav1.ListOptions{})
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  	apps := argo.FilterByProjects(appsList.Items, []string{q.Name})
   484  	if len(apps) > 0 {
   485  		return nil, status.Errorf(codes.InvalidArgument, "project is referenced by %d applications", len(apps))
   486  	}
   487  	err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Delete(ctx, q.Name, metav1.DeleteOptions{})
   488  	if err == nil {
   489  		s.logEvent(ctx, p, argo.EventReasonResourceDeleted, "deleted project")
   490  	}
   491  	return &project.EmptyResponse{}, err
   492  }
   493  
   494  func (s *Server) ListEvents(ctx context.Context, q *project.ProjectQuery) (*corev1.EventList, error) {
   495  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, q.Name); err != nil {
   496  		return nil, err
   497  	}
   498  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  	fieldSelector := fields.SelectorFromSet(map[string]string{
   503  		"involvedObject.name":      proj.Name,
   504  		"involvedObject.uid":       string(proj.UID),
   505  		"involvedObject.namespace": proj.Namespace,
   506  	}).String()
   507  	return s.kubeclientset.CoreV1().Events(s.ns).List(ctx, metav1.ListOptions{FieldSelector: fieldSelector})
   508  }
   509  
   510  func (s *Server) logEvent(ctx context.Context, a *v1alpha1.AppProject, reason string, action string) {
   511  	eventInfo := argo.EventInfo{Type: corev1.EventTypeNormal, Reason: reason}
   512  	user := session.Username(ctx)
   513  	if user == "" {
   514  		user = "Unknown user"
   515  	}
   516  	message := fmt.Sprintf("%s %s", user, action)
   517  	s.auditLogger.LogAppProjEvent(a, eventInfo, message, user)
   518  }
   519  
   520  func (s *Server) GetSyncWindowsState(ctx context.Context, q *project.SyncWindowsQuery) (*project.SyncWindowsResponse, error) {
   521  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceProjects, rbac.ActionGet, q.Name); err != nil {
   522  		return nil, err
   523  	}
   524  	proj, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, q.Name, metav1.GetOptions{})
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  
   529  	res := &project.SyncWindowsResponse{}
   530  
   531  	windows, err := proj.Spec.SyncWindows.Active()
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  	if windows.HasWindows() {
   536  		res.Windows = *windows
   537  	} else {
   538  		res.Windows = []*v1alpha1.SyncWindow{}
   539  	}
   540  
   541  	return res, nil
   542  }
   543  
   544  func (s *Server) NormalizeProjs() error {
   545  	projList, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).List(context.Background(), metav1.ListOptions{})
   546  	if err != nil {
   547  		return status.Errorf(codes.Internal, "Error retrieving project list: %s", err.Error())
   548  	}
   549  	for _, proj := range projList.Items {
   550  		for i := 0; i < 3; i++ {
   551  			if !proj.NormalizeJWTTokens() {
   552  				break
   553  			}
   554  			_, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(context.Background(), &proj, metav1.UpdateOptions{})
   555  			if err == nil {
   556  				log.Infof("Successfully normalized project %s.", proj.Name)
   557  				break
   558  			}
   559  			if !apierrors.IsConflict(err) {
   560  				log.Warnf("Failed normalize project %s", proj.Name)
   561  				break
   562  			}
   563  			projGet, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(context.Background(), proj.Name, metav1.GetOptions{})
   564  			if err != nil {
   565  				return status.Errorf(codes.Internal, "Error retrieving project: %s", err.Error())
   566  			}
   567  			proj = *projGet
   568  			if i == 2 {
   569  				return status.Errorf(codes.Internal, "Failed normalize project %s", proj.Name)
   570  			}
   571  		}
   572  	}
   573  	return nil
   574  }