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

     1  package rbacpolicy
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/golang-jwt/jwt/v5"
     7  	log "github.com/sirupsen/logrus"
     8  
     9  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    10  	applister "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    11  	jwtutil "github.com/argoproj/argo-cd/v3/util/jwt"
    12  	"github.com/argoproj/argo-cd/v3/util/rbac"
    13  )
    14  
    15  // RBACPolicyEnforcer provides an RBAC Claims Enforcer which additionally consults AppProject
    16  // roles, jwt tokens, and groups. It is backed by a AppProject informer/lister cache and does not
    17  // make any API calls during enforcement.
    18  type RBACPolicyEnforcer struct {
    19  	enf        *rbac.Enforcer
    20  	projLister applister.AppProjectNamespaceLister
    21  	scopes     []string
    22  }
    23  
    24  // NewRBACPolicyEnforcer returns a new RBAC Enforcer for the Argo CD API Server
    25  func NewRBACPolicyEnforcer(enf *rbac.Enforcer, projLister applister.AppProjectNamespaceLister) *RBACPolicyEnforcer {
    26  	return &RBACPolicyEnforcer{
    27  		enf:        enf,
    28  		projLister: projLister,
    29  		scopes:     nil,
    30  	}
    31  }
    32  
    33  func (p *RBACPolicyEnforcer) SetScopes(scopes []string) {
    34  	p.scopes = scopes
    35  }
    36  
    37  func (p *RBACPolicyEnforcer) GetScopes() []string {
    38  	scopes := p.scopes
    39  	if scopes == nil {
    40  		scopes = rbac.DefaultScopes
    41  	}
    42  	return scopes
    43  }
    44  
    45  func IsProjectSubject(subject string) bool {
    46  	_, _, ok := GetProjectRoleFromSubject(subject)
    47  	return ok
    48  }
    49  
    50  func GetProjectRoleFromSubject(subject string) (string, string, bool) {
    51  	parts := strings.Split(subject, ":")
    52  	if len(parts) == 3 && parts[0] == "proj" {
    53  		return parts[1], parts[2], true
    54  	}
    55  	return "", "", false
    56  }
    57  
    58  // EnforceClaims is an RBAC claims enforcer specific to the Argo CD API server
    59  func (p *RBACPolicyEnforcer) EnforceClaims(claims jwt.Claims, rvals ...any) bool {
    60  	mapClaims, err := jwtutil.MapClaims(claims)
    61  	if err != nil {
    62  		return false
    63  	}
    64  
    65  	subject := jwtutil.GetUserIdentifier(mapClaims)
    66  	// Check if the request is for an application resource. We have special enforcement which takes
    67  	// into consideration the project's token and group bindings
    68  	var runtimePolicy string
    69  	var projName string
    70  	proj := p.getProjectFromRequest(rvals...)
    71  	if proj != nil {
    72  		if IsProjectSubject(subject) {
    73  			return p.enforceProjectToken(subject, proj, rvals...)
    74  		}
    75  		runtimePolicy = proj.ProjectPoliciesString()
    76  		projName = proj.Name
    77  	}
    78  
    79  	// NOTE: This calls prevent multiple creation of the wrapped enforcer
    80  	enforcer := p.enf.CreateEnforcerWithRuntimePolicy(projName, runtimePolicy)
    81  
    82  	// Check the subject. This is typically the 'admin' case.
    83  	// NOTE: the call to EnforceWithCustomEnforcer will also consider the default role
    84  	vals := append([]any{subject}, rvals[1:]...)
    85  	if p.enf.EnforceWithCustomEnforcer(enforcer, vals...) {
    86  		return true
    87  	}
    88  
    89  	scopes := p.scopes
    90  	if scopes == nil {
    91  		scopes = rbac.DefaultScopes
    92  	}
    93  	// Finally check if any of the user's groups grant them permissions
    94  	groups := jwtutil.GetScopeValues(mapClaims, scopes)
    95  
    96  	// Get groups to reduce the amount to checking groups
    97  	groupingPolicies, err := enforcer.GetGroupingPolicy()
    98  	if err != nil {
    99  		log.WithError(err).Error("failed to get grouping policy")
   100  		return false
   101  	}
   102  	for gidx := range groups {
   103  		for gpidx := range groupingPolicies {
   104  			// Prefilter user groups by groups defined in the model
   105  			if groupingPolicies[gpidx][0] == groups[gidx] {
   106  				vals := append([]any{groups[gidx]}, rvals[1:]...)
   107  				if p.enf.EnforceWithCustomEnforcer(enforcer, vals...) {
   108  					return true
   109  				}
   110  				break
   111  			}
   112  		}
   113  	}
   114  	logCtx := log.WithFields(log.Fields{"claims": claims, "rval": rvals, "subject": subject, "groups": groups, "project": projName, "scopes": scopes})
   115  	logCtx.Debug("enforce failed")
   116  	return false
   117  }
   118  
   119  // getProjectFromRequest parses the project name from the RBAC request and returns the associated
   120  // project (if it exists)
   121  func (p *RBACPolicyEnforcer) getProjectFromRequest(rvals ...any) *v1alpha1.AppProject {
   122  	if len(rvals) != 4 {
   123  		return nil
   124  	}
   125  	getProjectByName := func(projName string) *v1alpha1.AppProject {
   126  		proj, err := p.projLister.Get(projName)
   127  		if err != nil {
   128  			return nil
   129  		}
   130  		return proj
   131  	}
   132  	if res, ok := rvals[1].(string); ok {
   133  		if obj, ok := rvals[3].(string); ok {
   134  			switch res {
   135  			case rbac.ResourceApplicationSets, rbac.ResourceApplications, rbac.ResourceRepositories, rbac.ResourceClusters, rbac.ResourceLogs, rbac.ResourceExec:
   136  				if objSplit := strings.Split(obj, "/"); len(objSplit) >= 2 {
   137  					return getProjectByName(objSplit[0])
   138  				}
   139  			case rbac.ResourceProjects:
   140  				// we also automatically give project tokens and groups 'get' access to the project
   141  				return getProjectByName(obj)
   142  			}
   143  		}
   144  	}
   145  	return nil
   146  }
   147  
   148  // enforceProjectToken will check to see the valid token has not yet been revoked in the project
   149  func (p *RBACPolicyEnforcer) enforceProjectToken(subject string, proj *v1alpha1.AppProject, rvals ...any) bool {
   150  	subjectSplit := strings.Split(subject, ":")
   151  	if len(subjectSplit) != 3 {
   152  		return false
   153  	}
   154  	projName, _ := subjectSplit[1], subjectSplit[2]
   155  	if projName != proj.Name {
   156  		// this should never happen (we generated a project token for a different project)
   157  		return false
   158  	}
   159  
   160  	vals := append([]any{subject}, rvals[1:]...)
   161  	return p.enf.EnforceRuntimePolicy(proj.Name, proj.ProjectPoliciesString(), vals...)
   162  }