@@ -0,0 +1,7 @@
version: 2
formats: all
mkdocs:
fail_on_warning: false
python:
install:
- requirements: docs/requirements.txt
@@ -1 +1 @@
1.8.3
1.8.4
@@ -25,6 +25,7 @@ import (
"github.com/argoproj/argo-cd/util/errors"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
"github.com/argoproj/argo-cd/util/io"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
"github.com/argoproj/argo-cd/util/localconfig"
oidcutil "github.com/argoproj/argo-cd/util/oidc"
"github.com/argoproj/argo-cd/util/rand"
@@ -113,7 +114,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
}

parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
claims := jwt.MapClaims{}
_, _, err := parser.ParseUnverified(tokenString, &claims)
@@ -161,13 +162,13 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
}

func userDisplayName(claims jwt.MapClaims) string {
if email, ok := claims["email"]; ok && email != nil {
return email.(string)
if email := jwtutil.StringField(claims, "email"); email != "" {
return email
}
if name, ok := claims["name"]; ok && name != nil {
return name.(string)
if name := jwtutil.StringField(claims, "name"); name != "" {
return name
}
return claims["sub"].(string)
return jwtutil.StringField(claims, "sub")
}

// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
@@ -0,0 +1,31 @@
package commands

import (
"testing"

"github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
)

//

func Test_userDisplayName_email(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "email": "firstname.lastname@example.com", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "firstname.lastname@example.com"
assert.Equal(t, expectedName, actualName)
}

func Test_userDisplayName_name(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "name": "Firstname Lastname", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "Firstname Lastname"
assert.Equal(t, expectedName, actualName)
}

func Test_userDisplayName_sub(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "foo"
assert.Equal(t, expectedName, actualName)
}
@@ -17,6 +17,7 @@ import (
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/errors"
"github.com/argoproj/argo-cd/util/io"
"github.com/argoproj/argo-cd/util/jwt"
)

const (
@@ -247,13 +248,10 @@ func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *c
}

claims := token.Claims.(jwtgo.MapClaims)
issuedAt := int64(claims["iat"].(float64))
expiresAt := int64(0)
if expires, ok := claims["exp"]; ok {
expiresAt = int64(expires.(float64))
}
id := claims["jti"].(string)
subject := claims["sub"].(string)
issuedAt, _ := jwt.IssuedAt(claims)
expiresAt := int64(jwt.Float64Field(claims, "exp"))
id := jwt.StringField(claims, "jti")
subject := jwt.StringField(claims, "sub")

if !outputTokenOnly {
fmt.Printf("Create token succeeded for %s.\n", subject)
@@ -0,0 +1,172 @@
.md-header-nav__title {
display: flex;
}

.dropdown-caret {
display: inline-block !important;
position: absolute;
right: 4px;
}

.fa .fa-caret-down {
display: none !important;
}

.rst-other-versions {
text-align: right;
}

.rst-other-versions > dl, .rst-other-versions dt, .rst-other-versions small {
display: none;
}

.rst-other-versions > dl:first-child {
display: flex !important;
flex-direction: column;
line-height: 0px !important;
}

.rst-versions.shift-up .rst-other-versions {
display: flex !important;
}

.rst-versions .rst-other-versions {
display: none;
}

/* Version Warning */
div[data-md-component=announce] {
background-color: rgba(255,145,0,.1);
}
div[data-md-component=announce]>div#announce-msg{
color: var(--md-admonition-fg-color);
font-size: .8rem;
text-align: center;
margin: 15px;
}
div[data-md-component=announce]>div#announce-msg>a{
color: var(--md-typeset-a-color);
text-decoration: underline;
}

/* from https://assets.readthedocs.org/static/css/badge_only.css,
most styles have to be overriden here */
.rst-versions{
position: relative !important;
bottom: 0;
left: 0;
width: 100px !important;
background: hsla(173, 100%, 24%, 1) !important;
font-family: inherit !important;
z-index: 0 !important;
}
.rst-versions a{
color:#2980B9;
text-decoration:none
}
.rst-versions .rst-badge-small{
display:none
}
.rst-versions .rst-current-version{
padding:12px;
background: hsla(173, 100%, 24%, 1) !important;
display:block;
text-align:right;
font-size:90%;
cursor:pointer;
color: white !important;
*zoom:1
}
.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{
display:table;content:""
}
.rst-versions .rst-current-version:after{
clear:both
}
.rst-versions .rst-current-version .fa{
color:#fcfcfc
}
.rst-versions .rst-current-version .fa-caret-down{
display: none;
}
.rst-versions.shift-up .rst-other-versions{
display:block
}
.rst-versions .rst-other-versions{
font-size:90%;
padding:12px;
color:gray;
display:none
}
.rst-versions .rst-other-versions hr{
display: none !important;
height: 0px !important;
border: 0px;
margin: 0px !important;
padding: 0px;
border-top: none !important;
}
.rst-versions .rst-other-versions dd{
display:inline-block;
margin:0
}
.rst-versions .rst-other-versions dd a{
display:inline-block;
padding: 1em 0em !important;
color:#fcfcfc;
font-size: .6rem !important;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 80px;
}
.rst-versions .rst-other-versions dd a:hover{
font-size: .7rem !important;
font-weight: bold;
}
.rst-versions.rst-badge{
display: block !important;
width: 100px !important;
bottom: 0px !important;
right: 0px !important;
left:auto;
border:none;
text-align: center !important;
line-height: 0;
}
.rst-versions.rst-badge .icon-book{
display: none;
}
.rst-versions.rst-badge .fa-book{
display: none !important;
}
.rst-versions.rst-badge.shift-up .rst-current-version{
text-align: left !important;
}
.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{
display: none !important;
}
.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{
display: none !important;
}
.rst-versions.rst-badge .rst-current-version{
width: 70px !important;
height: 2.4rem !important;
line-height:2.4rem !important;
padding: 0px 5px !important;
display: inline-block !important;
font-size: .6rem !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
text-align: left !important;
}
@media screen and (max-width: 768px){
.rst-versions{
width:85%;
display:none
}
.rst-versions.shift{
display:block
}
}
@@ -0,0 +1,43 @@
setTimeout(function() {
const callbackName = 'callback_' + new Date().getTime();
window[callbackName] = function (response) {
const div = document.createElement('div');
div.innerHTML = response.html;
document.querySelector(".md-header-nav > .md-header-nav__title").appendChild(div);
const container = div.querySelector('.rst-versions');
var caret = document.createElement('div');
caret.innerHTML = "<i class='fa fa-caret-down dropdown-caret'></i>"
caret.classList.add('dropdown-caret')
div.querySelector('.rst-current-version').appendChild(caret);
div.querySelector('.rst-current-version').addEventListener('click', function() {
const classes = container.className.split(' ');
const index = classes.indexOf('shift-up');
if (index === -1) {
classes.push('shift-up');
} else {
classes.splice(index, 1);
}
container.className = classes.join(' ');
});
}

var CSSLink = document.createElement('link');
CSSLink.rel='stylesheet';
CSSLink.href = '/assets/versions.css';
document.getElementsByTagName('head')[0].appendChild(CSSLink);

var script = document.createElement('script');
script.src = 'https://argo-cd.readthedocs.io/_/api/v2/footer_html/?'+
'callback=' + callbackName + '&project=argo-cd&page=&theme=mkdocs&format=jsonp&docroot=docs&source_suffix=.md&version=' + (window['READTHEDOCS_DATA'] || { version: 'latest' }).version;
document.getElementsByTagName('head')[0].appendChild(script);
}, 0);

// VERSION WARNINGS
window.addEventListener("DOMContentLoaded", function() {
if ((window['READTHEDOCS_DATA']).version === "latest") {
document.querySelector("div[data-md-component=announce]").innerHTML = "<div id='announce-msg'>You are viewing the docs for an unreleased version of Argo CD, <a href='https://argo-cd.readthedocs.io/en/stable/'>click here to go to the latest stable version.</a></div>"
}
else if ((window['READTHEDOCS_DATA']).version !== "stable") {
document.querySelector("div[data-md-component=announce]").innerHTML = "<div id='announce-msg'>You are viewing the docs for a previous version of Argo CD, <a href='https://argo-cd.readthedocs.io/en/stable/'>click here to go to the latest stable version.</a></div>"
}
});
@@ -0,0 +1,3 @@
mkdocs-material
markdown_include
pygments==2.4
@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: v1.8.3
newTag: v1.8.4
resources:
- ./application-controller
- ./dex
@@ -11,7 +11,7 @@ patchesStrategicMerge:
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: v1.8.3
newTag: v1.8.4
resources:
- ../../base/application-controller
- ../../base/dex
@@ -2864,7 +2864,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -3002,7 +3002,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -3084,7 +3084,7 @@ spec:
env:
- name: ARGOCD_API_SERVER_REPLICAS
value: "2"
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3160,7 +3160,7 @@ spec:
- "10"
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2779,7 +2779,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2917,7 +2917,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2999,7 +2999,7 @@ spec:
env:
- name: ARGOCD_API_SERVER_REPLICAS
value: "2"
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3075,7 +3075,7 @@ spec:
- "10"
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2453,7 +2453,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2553,7 +2553,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2631,7 +2631,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2706,7 +2706,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2368,7 +2368,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2468,7 +2468,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2546,7 +2546,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2621,7 +2621,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:v1.8.3
image: argoproj/argocd:v1.8.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -9,6 +9,12 @@ theme:
text: 'Work Sans'
logo: 'assets/logo.png'
favicon: 'assets/favicon.png'
# language: en-custom
custom_dir: overrides
extra_javascript:
- assets/versions.js
extra_css:
- assets/versions.css
google_analytics:
- 'UA-105170809-2'
- 'auto'
@@ -0,0 +1,3 @@
{% macro t(key) %}{{ {
"toc.title": "Table of Contents"
}[key] }}{% endmacro %}
@@ -328,14 +328,14 @@ func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, co
return err
}
parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
var claims jwt.StandardClaims
_, _, err = parser.ParseUnverified(configCtx.User.AuthToken, &claims)
if err != nil {
return err
}
if claims.Valid(jwt.DefaultValidationHelper) == nil {
if claims.Valid(parser.ValidationHelper) == nil {
// token is still valid
return nil
}
@@ -2707,8 +2707,11 @@ func (proj *AppProject) NormalizeJWTTokens() bool {
}

func syncJWTTokenBetweenStatusAndSpec(proj *AppProject) bool {
existingRole := map[string]bool{}
needSync := false
for roleIndex, role := range proj.Spec.Roles {
existingRole[role.Name] = true

tokensInSpec := role.JWTTokens
tokensInStatus := []JWTToken{}
if proj.Status.JWTTokensByRole == nil {
@@ -2731,8 +2734,16 @@ func syncJWTTokenBetweenStatusAndSpec(proj *AppProject) bool {

proj.Spec.Roles[roleIndex].JWTTokens = tokens
proj.Status.JWTTokensByRole[role.Name] = JWTTokens{Items: tokens}

}
if proj.Status.JWTTokensByRole != nil {
for role := range proj.Status.JWTTokensByRole {
if !existingRole[role] {
delete(proj.Status.JWTTokensByRole, role)
needSync = true
}
}
}

return needSync
}

@@ -79,7 +79,9 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach
// CreateGRPC creates new configured grpc server
func (a *ArgoCDRepoServer) CreateGRPC() *grpc.Server {
server := grpc.NewServer(a.opts...)
versionpkg.RegisterVersionServiceServer(server, &version.Server{})
versionpkg.RegisterVersionServiceServer(server, version.NewServer(nil, func() (bool, error) {
return true, nil
}))
manifestService := repository.NewService(a.metricsServer, a.cache, a.initConstants)
apiclient.RegisterRepoServerServiceServer(server, manifestService)

@@ -17,6 +17,7 @@ import (
"github.com/argoproj/argo-cd/pkg/apiclient/account"
sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
"github.com/argoproj/argo-cd/server/session"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/errors"
"github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/rbac"
@@ -63,7 +64,7 @@ func newTestAccountServerExt(ctx context.Context, enforceFn rbac.ClaimsEnforcerF
}
kubeclientset := fake.NewSimpleClientset(cm, secret)
settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace)
sessionMgr := sessionutil.NewSessionManager(settingsMgr, "", sessionutil.NewInMemoryUserStateStorage())
sessionMgr := sessionutil.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", sessionutil.NewInMemoryUserStateStorage())
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
enforcer.SetClaimsEnforcerFunc(enforceFn)

@@ -85,7 +85,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

issuer := jwtutil.GetField(mapClaims, "iss")
issuer := jwtutil.StringField(mapClaims, "iss")

if argoCDSettings.OIDCConfig() == nil || argoCDSettings.OIDCConfig().LogoutURL == "" || issuer == session.SessionManagerClaimsIssuer {
http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
@@ -9,18 +9,17 @@ import (
"regexp"
"testing"

"github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
"k8s.io/client-go/kubernetes/fake"

"github.com/argoproj/argo-cd/common"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/session"
"github.com/argoproj/argo-cd/util/settings"

"github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
"k8s.io/client-go/kubernetes/fake"
)

var (
@@ -177,7 +176,7 @@ func TestHandlerConstructLogoutURL(t *testing.T) {
settingsManagerWithoutOIDCConfig := settings.NewSettingsManager(context.Background(), kubeClientWithoutOIDCConfig, "default")
settingsManagerWithOIDCConfigButNoLogoutURL := settings.NewSettingsManager(context.Background(), kubeClientWithOIDCConfigButNoLogoutURL, "default")

sessionManager := session.NewSessionManager(settingsManagerWithOIDCConfig, "", session.NewInMemoryUserStateStorage())
sessionManager := session.NewSessionManager(settingsManagerWithOIDCConfig, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())

oidcHandler := NewHandler(appclientset.NewSimpleClientset(), settingsManagerWithOIDCConfig, sessionManager, "", "default")
oidcHandler.verifyToken = func(tokenString string) (jwt.Claims, error) {
@@ -108,7 +108,7 @@ func (s *Server) CreateToken(ctx context.Context, q *project.ProjectTokenCreateR
return nil, status.Error(codes.InvalidArgument, err.Error())
}
parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
claims := jwt.StandardClaims{}
_, _, err = parser.ParseUnverified(jwtToken, &claims)
@@ -7,6 +7,7 @@ import (
"testing"

"github.com/argoproj/pkg/sync"
"github.com/dgrijalva/jwt-go/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
@@ -17,14 +18,13 @@ import (
"k8s.io/client-go/kubernetes/fake"
k8scache "k8s.io/client-go/tools/cache"

"github.com/dgrijalva/jwt-go/v4"

"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apiclient/project"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
informer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
"github.com/argoproj/argo-cd/server/rbacpolicy"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/assets"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
"github.com/argoproj/argo-cd/util/rbac"
@@ -82,7 +82,7 @@ func TestProjectServer(t *testing.T) {
}

t.Run("TestNormalizeProj", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
roleName := "roleName"
role1 := v1alpha1.ProjectRole{Name: roleName, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: 1}}}
@@ -319,7 +319,7 @@ func TestProjectServer(t *testing.T) {
id := "testId"

t.Run("TestCreateTokenDenied", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName}}
projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(projectWithRole), enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
@@ -328,7 +328,7 @@ func TestProjectServer(t *testing.T) {
})

t.Run("TestCreateTokenSuccessfullyUsingGroup", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName, Groups: []string{"my-group"}}}
projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(projectWithRole), enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
@@ -339,11 +339,13 @@ func TestProjectServer(t *testing.T) {
_ = enforcer.SetBuiltinPolicy(`p, role:admin, projects, update, *, allow`)

t.Run("TestCreateTokenSuccessfully", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName}}
projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(projectWithRole), enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
tokenResponse, err := projectServer.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projectWithRole.Name, Role: tokenName, ExpiresIn: 1})
clientset := apps.NewSimpleClientset(projectWithRole)

sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjListerFromInterface(clientset.ArgoprojV1alpha1().AppProjects("default")), "", session.NewInMemoryUserStateStorage())
projectServer := NewServer("default", fake.NewSimpleClientset(), clientset, enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
tokenResponse, err := projectServer.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projectWithRole.Name, Role: tokenName, ExpiresIn: 100})
assert.NoError(t, err)
claims, err := sessionMgr.Parse(tokenResponse.Token)
assert.NoError(t, err)
@@ -357,10 +359,12 @@ func TestProjectServer(t *testing.T) {
})

t.Run("TestCreateTokenWithIDSuccessfully", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName}}
projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(projectWithRole), enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
clientset := apps.NewSimpleClientset(projectWithRole)

sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjListerFromInterface(clientset.ArgoprojV1alpha1().AppProjects("default")), "", session.NewInMemoryUserStateStorage())
projectServer := NewServer("default", fake.NewSimpleClientset(), clientset, enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
tokenResponse, err := projectServer.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projectWithRole.Name, Role: tokenName, ExpiresIn: 1, Id: id})
assert.NoError(t, err)
claims, err := sessionMgr.Parse(tokenResponse.Token)
@@ -375,10 +379,12 @@ func TestProjectServer(t *testing.T) {
})

t.Run("TestCreateTokenWithSameIdDeny", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
projectWithRole := existingProj.DeepCopy()
projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName}}
projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(projectWithRole), enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
clientset := apps.NewSimpleClientset(projectWithRole)

sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjListerFromInterface(clientset.ArgoprojV1alpha1().AppProjects("default")), "", session.NewInMemoryUserStateStorage())
projectServer := NewServer("default", fake.NewSimpleClientset(), clientset, enforcer, sync.NewKeyLock(), sessionMgr, policyEnf, projInformer, settingsMgr)
tokenResponse, err := projectServer.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projectWithRole.Name, Role: tokenName, ExpiresIn: 1, Id: id})

assert.NoError(t, err)
@@ -400,7 +406,7 @@ func TestProjectServer(t *testing.T) {
_ = enforcer.SetBuiltinPolicy(`p, *, *, *, *, deny`)

t.Run("TestDeleteTokenDenied", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projWithToken := existingProj.DeepCopy()
issuedAt := int64(1)
secondIssuedAt := issuedAt + 1
@@ -413,7 +419,7 @@ func TestProjectServer(t *testing.T) {
})

t.Run("TestDeleteTokenSuccessfullyWithGroup", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projWithToken := existingProj.DeepCopy()
issuedAt := int64(1)
secondIssuedAt := issuedAt + 1
@@ -429,7 +435,7 @@ func TestProjectServer(t *testing.T) {
p, role:admin, projects, update, *, allow`)

t.Run("TestDeleteTokenSuccessfully", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projWithToken := existingProj.DeepCopy()
issuedAt := int64(1)
secondIssuedAt := issuedAt + 1
@@ -450,7 +456,7 @@ p, role:admin, projects, update, *, allow`)
p, role:admin, projects, update, *, allow`)

t.Run("TestDeleteTokenByIdSuccessfully", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projWithToken := existingProj.DeepCopy()
issuedAt := int64(1)
secondIssuedAt := issuedAt + 1
@@ -473,7 +479,7 @@ p, role:admin, projects, update, *, allow`)
enforcer = newEnforcer(kubeclientset)

t.Run("TestCreateTwoTokensInRoleSuccess", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projWithToken := existingProj.DeepCopy()
tokenName := "testToken"
token := v1alpha1.ProjectRole{Name: tokenName, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: 1}}}
@@ -638,7 +644,7 @@ p, role:admin, projects, update, *, allow`)
})

t.Run("TestSyncWindowsActive", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithSyncWindows := existingProj.DeepCopy()
projectWithSyncWindows.Spec.SyncWindows = v1alpha1.SyncWindows{}
win := &v1alpha1.SyncWindow{Kind: "allow", Schedule: "* * * * *", Duration: "1h"}
@@ -651,7 +657,7 @@ p, role:admin, projects, update, *, allow`)
})

t.Run("TestGetSyncWindowsStateCannotGetProjectDetails", func(t *testing.T) {
sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithSyncWindows := existingProj.DeepCopy()
projectWithSyncWindows.Spec.SyncWindows = v1alpha1.SyncWindows{}
win := &v1alpha1.SyncWindow{Kind: "allow", Schedule: "* * * * *", Duration: "1h"}
@@ -670,7 +676,7 @@ p, role:admin, projects, update, *, allow`)
// nolint:staticcheck
ctx := context.WithValue(context.Background(), "claims", &jwt.MapClaims{"groups": []string{"my-group"}})

sessionMgr := session.NewSessionManager(settingsMgr, "", session.NewInMemoryUserStateStorage())
sessionMgr := session.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", session.NewInMemoryUserStateStorage())
projectWithSyncWindows := existingProj.DeepCopy()
win := &v1alpha1.SyncWindow{Kind: "allow", Schedule: "* * * * *", Duration: "1h"}
projectWithSyncWindows.Spec.SyncWindows = append(projectWithSyncWindows.Spec.SyncWindows, win)
@@ -82,7 +82,16 @@ func (p *RBACPolicyEnforcer) GetScopes() []string {
}

func IsProjectSubject(subject string) bool {
return strings.HasPrefix(subject, "proj:")
_, _, ok := GetProjectRoleFromSubject(subject)
return ok
}

func GetProjectRoleFromSubject(subject string) (string, string, bool) {
parts := strings.Split(subject, ":")
if len(parts) == 3 && parts[0] == "proj" {
return parts[1], parts[2], true
}
return "", "", false
}

// EnforceClaims is an RBAC claims enforcer specific to the Argo CD API server
@@ -92,14 +101,14 @@ func (p *RBACPolicyEnforcer) EnforceClaims(claims jwt.Claims, rvals ...interface
return false
}

subject := jwtutil.GetField(mapClaims, "sub")
subject := jwtutil.StringField(mapClaims, "sub")
// Check if the request is for an application resource. We have special enforcement which takes
// into consideration the project's token and group bindings
var runtimePolicy string
proj := p.getProjectFromRequest(rvals...)
if proj != nil {
if IsProjectSubject(subject) {
return p.enforceProjectToken(subject, mapClaims, proj, rvals...)
return p.enforceProjectToken(subject, proj, rvals...)
}
runtimePolicy = proj.ProjectPoliciesString()
}
@@ -158,31 +167,17 @@ func (p *RBACPolicyEnforcer) getProjectFromRequest(rvals ...interface{}) *v1alph
}

// enforceProjectToken will check to see the valid token has not yet been revoked in the project
func (p *RBACPolicyEnforcer) enforceProjectToken(subject string, claims jwt.MapClaims, proj *v1alpha1.AppProject, rvals ...interface{}) bool {
func (p *RBACPolicyEnforcer) enforceProjectToken(subject string, proj *v1alpha1.AppProject, rvals ...interface{}) bool {
subjectSplit := strings.Split(subject, ":")
if len(subjectSplit) != 3 {
return false
}
projName, roleName := subjectSplit[1], subjectSplit[2]
projName, _ := subjectSplit[1], subjectSplit[2]
if projName != proj.Name {
// this should never happen (we generated a project token for a different project)
return false
}

var iat int64 = -1
jti, err := jwtutil.GetID(claims)
if err != nil || jti == "" {
iat, err = jwtutil.GetIssuedAt(claims)
if err != nil {
return false
}
}

_, _, err = proj.GetJWTToken(roleName, iat, jti)
if err != nil {
// if we get here the token is still valid, but has been revoked (no longer exists in the project)
return false
}
vals := append([]interface{}{subject}, rvals[1:]...)
return p.enf.EnforceRuntimePolicy(proj.ProjectPoliciesString(), vals...)

@@ -67,12 +67,6 @@ func TestEnforceAllPolicies(t *testing.T) {

claims = jwt.MapClaims{"sub": "cathy"}
assert.False(t, enf.Enforce(claims, "applications", "create", "my-proj/my-app"))
claims = jwt.MapClaims{"sub": "proj:my-proj:my-role"}
assert.False(t, enf.Enforce(claims, "applications", "create", "my-proj/my-app"))
claims = jwt.MapClaims{"sub": "proj:my-proj:other-role", "iat": 1234}
assert.False(t, enf.Enforce(claims, "applications", "create", "my-proj/my-app"))
claims = jwt.MapClaims{"groups": []string{"my-org:other-group"}}
assert.False(t, enf.Enforce(claims, "applications", "create", "my-proj/my-app"))

// AWS cognito returns its groups in cognito:groups
rbacEnf.SetScopes([]string{"cognito:groups"})
@@ -209,15 +209,14 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer {
err = initializeDefaultProject(opts)
errors.CheckError(err)

sessionMgr := util_session.NewSessionManager(settingsMgr, opts.DexServerAddr, opts.Cache)

factory := appinformer.NewFilteredSharedInformerFactory(opts.AppClientset, 0, opts.Namespace, func(options *metav1.ListOptions) {})
projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
projLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(opts.Namespace)

appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
appLister := factory.Argoproj().V1alpha1().Applications().Lister().Applications(opts.Namespace)

sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.Cache)
enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil)
enf.EnableEnforce(!opts.DisableAuth)
err = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
@@ -562,7 +561,16 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
certificateService := certificate.NewServer(a.RepoClientset, db, a.enf)
gpgkeyService := gpgkey.NewServer(a.RepoClientset, db, a.enf)
versionpkg.RegisterVersionServiceServer(grpcS, &version.Server{})
versionpkg.RegisterVersionServiceServer(grpcS, version.NewServer(a, func() (bool, error) {
if a.DisableAuth {
return true, nil
}
sett, err := a.settingsMgr.GetSettings()
if err != nil {
return false, err
}
return sett.AnonymousUserEnabled, err
}))
clusterpkg.RegisterClusterServiceServer(grpcS, clusterService)
applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService)
repositorypkg.RegisterRepositoryServiceServer(grpcS, repoService)
@@ -833,6 +841,7 @@ func (server *ArgoCDServer) newStaticAssetsHandler(dir string, baseHRef string)
if server.XFrameOptions != "" {
w.Header().Set("X-Frame-Options", server.XFrameOptions)
}
w.Header().Set("X-XSS-Protection", "1")

// serve index.html for non file requests to support HTML5 History API
if acceptHTML && !fileRequest && (r.Method == "GET" || r.Method == "HEAD") {
@@ -362,13 +362,6 @@ func TestRevokedToken(t *testing.T) {
claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name))
assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
// Now revoke the token by deleting the token
existingProj.Spec.Roles[0].JWTTokens = nil
existingProj.Status.JWTTokensByRole = nil
_, _ = s.AppClientset.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Update(context.Background(), &existingProj, metav1.UpdateOptions{})
time.Sleep(200 * time.Millisecond) // this lets the informer get synced
assert.False(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name))
assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
}

func TestCertsAreNotGeneratedInInsecureMode(t *testing.T) {
@@ -15,18 +15,26 @@ import (

"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apiclient/version"
"github.com/argoproj/argo-cd/server/settings"
"github.com/argoproj/argo-cd/util/helm"
ksutil "github.com/argoproj/argo-cd/util/ksonnet"
"github.com/argoproj/argo-cd/util/kustomize"
"github.com/argoproj/argo-cd/util/log"
sessionmgr "github.com/argoproj/argo-cd/util/session"
)

type Server struct {
type server struct {
ksonnetVersion string
kustomizeVersion string
helmVersion string
kubectlVersion string
jsonnetVersion string
authenticator settings.Authenticator
disableAuth func() (bool, error)
}

func NewServer(authenticator settings.Authenticator, disableAuth func() (bool, error)) *server {
return &server{authenticator: authenticator, disableAuth: disableAuth}
}

func getVersion() (string, error) {
@@ -51,8 +59,17 @@ func getVersion() (string, error) {
}

// Version returns the version of the API server
func (s *Server) Version(context.Context, *empty.Empty) (*version.VersionMessage, error) {
func (s *server) Version(ctx context.Context, _ *empty.Empty) (*version.VersionMessage, error) {
vers := common.GetVersion()
disableAuth, err := s.disableAuth()
if err != nil {
return nil, err
}

if !sessionmgr.LoggedIn(ctx) && !disableAuth {
return &version.VersionMessage{Version: vers.Version}, nil
}

if s.ksonnetVersion == "" {
ksonnetVersion, err := ksutil.Version()
if err == nil {
@@ -104,6 +121,10 @@ func (s *Server) Version(context.Context, *empty.Empty) (*version.VersionMessage
}

// AuthFuncOverride allows the version to be returned without auth
func (s *Server) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
func (s *server) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
if s.authenticator != nil {
// this authenticates the user, but ignores any error, so that we have claims populated
ctx, _ = s.authenticator.Authenticate(ctx)
}
return ctx, nil
}
@@ -1,14 +1,19 @@
package test

import (
"context"

"github.com/argoproj/gitops-engine/pkg/utils/testing"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"

"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
applister "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
)
@@ -114,6 +119,30 @@ func NewFakeSecret(policy ...string) *apiv1.Secret {
return &secret
}

type interfaceLister struct {
appProjects appclient.AppProjectInterface
}

func (l interfaceLister) List(selector labels.Selector) ([]*v1alpha1.AppProject, error) {
res, err := l.appProjects.List(context.Background(), metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, err
}
items := make([]*v1alpha1.AppProject, len(res.Items))
for i := range res.Items {
items[i] = &res.Items[i]
}
return items, nil
}

func (l interfaceLister) Get(name string) (*v1alpha1.AppProject, error) {
return l.appProjects.Get(context.Background(), name, metav1.GetOptions{})
}

func NewFakeProjListerFromInterface(appProjects appclient.AppProjectInterface) applister.AppProjectNamespaceLister {
return &interfaceLister{appProjects: appProjects}
}

func NewFakeProjLister(objects ...runtime.Object) applister.AppProjectNamespaceLister {
fakeAppClientset := apps.NewSimpleClientset(objects...)
factory := appinformer.NewFilteredSharedInformerFactory(fakeAppClientset, 0, "", func(options *metav1.ListOptions) {})
@@ -303,7 +303,7 @@ func (sac *ServiceAccountClaims) Valid(helper *jwt.ValidationHelper) error {
// ParseServiceAccountToken parses a Kubernetes service account token
func ParseServiceAccountToken(token string) (*ServiceAccountClaims, error) {
parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
var claims ServiceAccountClaims
_, _, err := parser.ParseUnverified(token, &claims)
@@ -2,7 +2,9 @@ package jwt

import (
"encoding/json"
"errors"
"fmt"
"time"

jwtgo "github.com/dgrijalva/jwt-go/v4"
)
@@ -21,8 +23,8 @@ func MapClaims(claims jwtgo.Claims) (jwtgo.MapClaims, error) {
return mapClaims, nil
}

// GetField extracts a field from the claims as a string
func GetField(claims jwtgo.MapClaims, fieldName string) string {
// StringField extracts a field from the claims as a string
func StringField(claims jwtgo.MapClaims, fieldName string) string {
if fieldIf, ok := claims[fieldName]; ok {
if field, ok := fieldIf.(string); ok {
return field
@@ -31,6 +33,16 @@ func GetField(claims jwtgo.MapClaims, fieldName string) string {
return ""
}

// Float64Field extracts a field from the claims as a float64
func Float64Field(claims jwtgo.MapClaims, fieldName string) float64 {
if fieldIf, ok := claims[fieldName]; ok {
if field, ok := fieldIf.(float64); ok {
return field
}
}
return 0
}

// GetScopeValues extracts the values of specified scopes from the claims
func GetScopeValues(claims jwtgo.MapClaims, scopes []string) []string {
groups := make([]string, 0)
@@ -67,9 +79,13 @@ func GetID(m jwtgo.MapClaims) (string, error) {
return "", fmt.Errorf("jti '%v' is not a string", m["jti"])
}

// GetIssuedAt returns the issued at as an int64
func GetIssuedAt(m jwtgo.MapClaims) (int64, error) {
switch iat := m["iat"].(type) {
// IssuedAt returns the issued at as an int64
func IssuedAt(m jwtgo.MapClaims) (int64, error) {
iatField, ok := m["iat"]
if !ok {
return 0, errors.New("token does not have iat claim")
}
switch iat := iatField.(type) {
case float64:
return int64(iat), nil
case json.Number:
@@ -81,6 +97,12 @@ func GetIssuedAt(m jwtgo.MapClaims) (int64, error) {
}
}

// IssuedAtTime returns the issued at as a time.Time
func IssuedAtTime(m jwtgo.MapClaims) (time.Time, error) {
iat, err := IssuedAt(m)
return time.Unix(iat, 0), err
}

func Claims(in interface{}) jwtgo.Claims {
claims, ok := in.(jwtgo.Claims)
if ok {
@@ -1,7 +1,9 @@
package jwt

import (
"fmt"
"testing"
"time"

jwt "github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
@@ -36,3 +38,25 @@ func TestGetGroups(t *testing.T) {
assert.Empty(t, GetGroups(jwt.MapClaims{}, []string{"groups"}))
assert.Equal(t, []string{"foo"}, GetGroups(jwt.MapClaims{"groups": []string{"foo"}}, []string{"groups"}))
}

func TestIssuedAtTime_Int64(t *testing.T) {
// Tuesday, 1 December 2020 14:00:00
claims := jwt.MapClaims{"iat": int64(1606831200)}
issuedAt, err := IssuedAtTime(claims)
assert.Nil(t, err)
str := fmt.Sprint(issuedAt.UTC().Format("Mon Jan _2 15:04:05 2006"))
assert.Equal(t, "Tue Dec 1 14:00:00 2020", str)
}

func TestIssuedAtTime_Error_NoInt(t *testing.T) {
claims := jwt.MapClaims{"iat": 1606831200}
_, err := IssuedAtTime(claims)
assert.NotNil(t, err)
}

func TestIssuedAtTime_Error_Missing(t *testing.T) {
claims := jwt.MapClaims{}
iat, err := IssuedAtTime(claims)
assert.NotNil(t, err)
assert.Equal(t, time.Unix(0, 0), iat)
}
@@ -64,7 +64,7 @@ type User struct {
// Claims returns the standard claims from the JWT claims
func (u *User) Claims() (*jwt.StandardClaims, error) {
parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
claims := jwt.StandardClaims{}
_, _, err := parser.ParseUnverified(u.AuthToken, &claims)
@@ -132,19 +132,12 @@ func (e *Enforcer) EnforceErr(rvals ...interface{}) error {
if err != nil {
break
}
sub := jwtutil.GetField(claims, "sub")
if sub != "" {
if sub := jwtutil.StringField(claims, "sub"); sub != "" {
rvalsStrs = append(rvalsStrs, fmt.Sprintf("sub: %s", sub))
}
iatField, ok := claims["iat"]
if !ok {
break
}
iat, ok := iatField.(float64)
if !ok {
break
if issuedAtTime, err := jwtutil.IssuedAtTime(claims); err == nil {
rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", issuedAtTime.Format(time.RFC3339)))
}
rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", time.Unix(int64(iat), 0).Format(time.RFC3339)))
}
errMsg = fmt.Sprintf("%s: %s", errMsg, strings.Join(rvalsStrs, ", "))
}
@@ -19,6 +19,7 @@ import (
"google.golang.org/grpc/status"

"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/server/rbacpolicy"
"github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/dex"
@@ -33,6 +34,7 @@ import (
// SessionManager generates and validates JWT tokens for login sessions.
type SessionManager struct {
settingsMgr *settings.SettingsManager
projectsLister v1alpha1.AppProjectNamespaceLister
client *http.Client
prov oidcutil.Provider
storage UserStateStorage
@@ -129,11 +131,12 @@ func getLoginFailureWindow() time.Duration {
}

// NewSessionManager creates a new session manager from Argo CD settings
func NewSessionManager(settingsMgr *settings.SettingsManager, dexServerAddr string, storage UserStateStorage) *SessionManager {
func NewSessionManager(settingsMgr *settings.SettingsManager, projectsLister v1alpha1.AppProjectNamespaceLister, dexServerAddr string, storage UserStateStorage) *SessionManager {
s := SessionManager{
settingsMgr: settingsMgr,
storage: storage,
sleep: time.Sleep,
projectsLister: projectsLister,
verificationDelayNoiseEnabled: true,
}
settings, err := settingsMgr.GetSettings()
@@ -239,7 +242,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
// head of the token to identify which key to use, but the parsed token (head and claims) is provided
// to the callback, providing flexibility.
var claims jwt.MapClaims
settings, err := mgr.settingsMgr.GetSettings()
argoCDSettings, err := mgr.settingsMgr.GetSettings()
if err != nil {
return nil, err
}
@@ -248,14 +251,30 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return settings.ServerSignature, nil
return argoCDSettings.ServerSignature, nil
})
if err != nil {
return nil, err
}

subject := jwtutil.GetField(claims, "sub")
if rbacpolicy.IsProjectSubject(subject) {
issuedAt, err := jwtutil.IssuedAtTime(claims)
if err != nil {
return nil, err
}

subject := jwtutil.StringField(claims, "sub")
id := jwtutil.StringField(claims, "jti")

if projName, role, ok := rbacpolicy.GetProjectRoleFromSubject(subject); ok {
proj, err := mgr.projectsLister.Get(projName)
if err != nil {
return nil, err
}
_, _, err = proj.GetJWTToken(role, issuedAt.Unix(), id)
if err != nil {
return nil, err
}

return token.Claims, nil
}

@@ -264,11 +283,24 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
return nil, err
}

if id := jwtutil.GetField(claims, "jti"); id != "" && account.TokenIndex(id) == -1 {
if !account.Enabled {
return nil, fmt.Errorf("account %s is disabled", subject)
}

var capability settings.AccountCapability
if id != "" {
capability = settings.AccountCapabilityApiKey
} else {
capability = settings.AccountCapabilityLogin
}
if !account.HasCapability(capability) {
return nil, fmt.Errorf("account %s does not have '%s' capability", subject, capability)
}

if id != "" && account.TokenIndex(id) == -1 {
return nil, fmt.Errorf("account %s does not have token with id %s", subject, id)
}

issuedAt := time.Unix(int64(claims["iat"].(float64)), 0)
if account.PasswordMtime != nil && issuedAt.Before(*account.PasswordMtime) {
return nil, fmt.Errorf("Account password has changed since token issued")
}
@@ -458,7 +490,7 @@ func (mgr *SessionManager) VerifyUsernamePassword(username string, password stri
// We choose how to verify based on the issuer.
func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, error) {
parser := &jwt.Parser{
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation()),
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
}
var claims jwt.StandardClaims
_, _, err := parser.ParseUnverified(tokenString, &claims)
@@ -519,11 +551,11 @@ func Username(ctx context.Context) string {
if !ok {
return ""
}
switch jwtutil.GetField(mapClaims, "iss") {
switch jwtutil.StringField(mapClaims, "iss") {
case SessionManagerClaimsIssuer:
return jwtutil.GetField(mapClaims, "sub")
return jwtutil.StringField(mapClaims, "sub")
default:
return jwtutil.GetField(mapClaims, "email")
return jwtutil.StringField(mapClaims, "email")
}
}

@@ -532,32 +564,23 @@ func Iss(ctx context.Context) string {
if !ok {
return ""
}
return jwtutil.GetField(mapClaims, "iss")
return jwtutil.StringField(mapClaims, "iss")
}

func Iat(ctx context.Context) (time.Time, error) {
mapClaims, ok := mapClaims(ctx)
if !ok {
return time.Time{}, errors.New("unable to extract token claims")
}
iatField, ok := mapClaims["iat"]
if !ok {
return time.Time{}, errors.New("token does not have iat claim")
}

if iat, ok := iatField.(float64); !ok {
return time.Time{}, errors.New("iat token field has unexpected type")
} else {
return time.Unix(int64(iat), 0), nil
}
return jwtutil.IssuedAtTime(mapClaims)
}

func Sub(ctx context.Context) string {
mapClaims, ok := mapClaims(ctx)
if !ok {
return ""
}
return jwtutil.GetField(mapClaims, "sub")
return jwtutil.StringField(mapClaims, "sub")
}

func Groups(ctx context.Context, scopes []string) []string {
@@ -22,7 +22,7 @@ func TestRandomPasswordVerificationDelay(t *testing.T) {

var sleptFor time.Duration
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
mgr.verificationDelayNoiseEnabled = true
mgr.sleep = func(d time.Duration) {
sleptFor = d
@@ -6,28 +6,46 @@ import (
"math"
"os"
"strconv"
"strings"
"testing"
"time"

"github.com/dgrijalva/jwt-go/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"

"github.com/argoproj/argo-cd/common"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/errors"
"github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/settings"
)

func getKubeClient(pass string, enabled bool) *fake.Clientset {
func getProjLister(objects ...runtime.Object) v1alpha1.AppProjectNamespaceLister {
return test.NewFakeProjListerFromInterface(apps.NewSimpleClientset(objects...).ArgoprojV1alpha1().AppProjects("argocd"))
}

func getKubeClient(pass string, enabled bool, capabilities ...settings.AccountCapability) *fake.Clientset {
const defaultSecretKey = "Hello, world!"

bcrypt, err := password.HashPassword(pass)
errors.CheckError(err)
if len(capabilities) == 0 {
capabilities = []settings.AccountCapability{settings.AccountCapabilityLogin, settings.AccountCapabilityApiKey}
}
var capabilitiesStr []string
for i := range capabilities {
capabilitiesStr = append(capabilitiesStr, string(capabilities[i]))
}

return fake.NewSimpleClientset(&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
@@ -38,6 +56,7 @@ func getKubeClient(pass string, enabled bool) *fake.Clientset {
},
},
Data: map[string]string{
"admin": strings.Join(capabilitiesStr, ","),
"admin.enabled": strconv.FormatBool(enabled),
},
}, &corev1.Secret{
@@ -52,18 +71,18 @@ func getKubeClient(pass string, enabled bool) *fake.Clientset {
})
}

func newSessionManager(settingsMgr *settings.SettingsManager, storage UserStateStorage) *SessionManager {
mgr := NewSessionManager(settingsMgr, "", storage)
func newSessionManager(settingsMgr *settings.SettingsManager, projectLister v1alpha1.AppProjectNamespaceLister, storage UserStateStorage) *SessionManager {
mgr := NewSessionManager(settingsMgr, projectLister, "", storage)
mgr.verificationDelayNoiseEnabled = false
return mgr
}

func TestSessionManager(t *testing.T) {
func TestSessionManager_AdminToken(t *testing.T) {
const (
defaultSubject = "admin"
)
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true), "argocd")
mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

token, err := mgr.Create(defaultSubject, 0, "")
if err != nil {
@@ -82,6 +101,80 @@ func TestSessionManager(t *testing.T) {
}
}

func TestSessionManager_AdminToken_Deactivated(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", false), "argocd")
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

token, err := mgr.Create("admin", 0, "")
if err != nil {
t.Errorf("Could not create token: %v", err)
}

_, err = mgr.Parse(token)
require.Error(t, err)
assert.Contains(t, err.Error(), "account admin is disabled")
}

func TestSessionManager_AdminToken_LoginCapabilityDisabled(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true, settings.AccountCapabilityLogin), "argocd")
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

token, err := mgr.Create("admin", 0, "abc")
if err != nil {
t.Errorf("Could not create token: %v", err)
}

_, err = mgr.Parse(token)
require.Error(t, err)
assert.Contains(t, err.Error(), "account admin does not have 'apiKey' capability")
}

func TestSessionManager_ProjectToken(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true), "argocd")

t.Run("Valid Token", func(t *testing.T) {
proj := appv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "argocd",
},
Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
Status: appv1.AppProjectStatus{JWTTokensByRole: map[string]appv1.JWTTokens{
"test": {
Items: []appv1.JWTToken{{ID: "abc", IssuedAt: time.Now().Unix(), ExpiresAt: 0}},
},
}},
}
mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewInMemoryUserStateStorage())

jwtToken, err := mgr.Create("proj:default:test", 100, "abc")
require.NoError(t, err)

_, err = mgr.Parse(jwtToken)
assert.NoError(t, err)
})

t.Run("Token Revoked", func(t *testing.T) {
proj := appv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "argocd",
},
Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
}

mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewInMemoryUserStateStorage())

jwtToken, err := mgr.Create("proj:default:test", 10, "")
require.NoError(t, err)

_, err = mgr.Parse(jwtToken)
require.Error(t, err)

assert.Contains(t, err.Error(), "does not exist in project 'default'")
})
}

var loggedOutContext = context.Background()

// nolint:staticcheck
@@ -153,7 +246,7 @@ func TestVerifyUsernamePassword(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient(password, !tc.disabled), "argocd")

mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

err := mgr.VerifyUsernamePassword(tc.userName, tc.password)

@@ -237,7 +330,7 @@ func TestLoginRateLimiter(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
storage := NewInMemoryUserStateStorage()

mgr := newSessionManager(settingsMgr, storage)
mgr := newSessionManager(settingsMgr, getProjLister(), storage)

t.Run("Test login delay valid user", func(t *testing.T) {
for i := 0; i < getMaxLoginFailures(); i++ {
@@ -276,15 +369,15 @@ func TestMaxUsernameLength(t *testing.T) {
username += "a"
}
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
err := mgr.VerifyUsernamePassword(username, "password")
assert.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf(usernameTooLongError, maxUsernameLength))
}

func TestMaxCacheSize(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}
// Temporarily decrease max cache size
@@ -300,7 +393,7 @@ func TestMaxCacheSize(t *testing.T) {

func TestFailedAttemptsExpiry(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
mgr := newSessionManager(settingsMgr, NewInMemoryUserStateStorage())
mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())

invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}