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 }