Compare commits

...

35 Commits

Author SHA1 Message Date
Alexander Matyushentsev
ebbc1d02f5 chore: pin mkdocs version to fix docs build (#6421)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-07-11 13:20:59 +02:00
Saumeya Katyal
9b0631b6c6 fix: Version warning banner in docs (#6682)
Signed-off-by: saumeya <saumeyakatyal@gmail.com>

add side-bar media queries

removed extra comments

Signed-off-by: saumeya <saumeyakatyal@gmail.com>
2021-07-11 13:12:28 +02:00
argo-bot
eb3d1fb84b Bump version to 1.8.7 2021-03-03 07:02:37 +00:00
argo-bot
e97b643526 Bump version to 1.8.7 2021-03-03 07:02:20 +00:00
kshamajain99
b51ba85aae fix: don't log certain fields (#5662)
* fix: support longer cookie

Signed-off-by: kshamajain99 <kshamajain99@gmail.com>

* merge conflicts

Signed-off-by: kshamajain99 <kshamajain99@gmail.com>

* fix: don't log certain fields

Signed-off-by: kshamajain99 <kshamajain99@gmail.com>
2021-03-02 21:32:29 -08:00
Regina Scott
be72e7cc42 fix: docs version selector not rendering (#5649)
Signed-off-by: Regina Scott <rescott@redhat.com>
2021-03-01 22:00:14 -08:00
Jan Gräfen
f6e9e41d7b fix: Empty resource whitelist allowed all resources (#5540) (#5551)
* fix: Empty resource whitelist allowed all resources

This requires setting the default in quite a few
places around the code base as well as adapting
a couple of tests

Signed-off-by: Jan Graefen <223234+jangraefen@users.noreply.github.com>

* Improve default behavior and not require explicitly set whitelist

Signed-off-by: Jan Graefen <223234+jangraefen@users.noreply.github.com>
2021-03-01 10:22:08 -08:00
jannfis
087b8b2fd5 docs: Move security policy to SECURITY.md for integration with GitHub (#5627)
* docs: Move security policy to SECURITY.md for integration with GitHub

Signed-off-by: jannfis <jann@mistrust.net>

* Change wording a bit.

Signed-off-by: jannfis <jann@mistrust.net>

* Change order of e-mail addresses

Signed-off-by: jannfis <jann@mistrust.net>
2021-02-27 15:17:53 +01:00
argo-bot
6dbbb18aa9 Bump version to 1.8.6 2021-02-26 21:12:06 +00:00
argo-bot
62b9b3aeb5 Bump version to 1.8.6 2021-02-26 21:11:50 +00:00
jannfis
31110cde4d fix: Properly escape HTML for error message from CLI SSO (#5563)
Signed-off-by: jannfis <jann@mistrust.net>
2021-02-26 10:29:50 +01:00
Alexander Matyushentsev
d6c5c72eb4 fix: API server should not print resource body when resource update fails (#5617)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-25 19:25:22 -08:00
kshamajain99
0b3333ef4b fix: fix memory leak in application controller (#5604)
fix: fix memory leak in application controller
2021-02-25 19:25:18 -08:00
argo-bot
d0f8edfec8 Bump version to 1.8.5 2021-02-20 05:29:23 +00:00
argo-bot
b1ff29fdf9 Bump version to 1.8.5 2021-02-20 05:29:05 +00:00
Alexander Matyushentsev
785bb9ecce fix: 'argocd app wait --suspended' stuck if operation is in progress (#5511)
* fix: 'argocd app wait --suspended' stuck if operation is in progress

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-19 14:41:58 -08:00
Alexander Matyushentsev
b57579c4ae fix: Presync hooks stop working after namespace resource is added in a Helm chart #5522 2021-02-19 09:50:56 -08:00
Ajay Kemparaj
6b53ac785e docs: add the missing rbac resources to the documentation (#5476)
* Adds resources accounts and gpgkeys

Signed-off-by: ajayk <ajaykemparaj@gmail.com>
2021-02-13 09:05:07 +01:00
Alexander Matyushentsev
e38920f570 refactor: optimize argocd-application-controller redis usage (#5345)
* refactor: controller uses two level caching to reduce number of redis calls

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 10:24:03 -08:00
argo-bot
28aea3dfde Bump version to 1.8.4 2021-02-05 17:46:03 +00:00
argo-bot
fe59190a96 Bump version to 1.8.4 2021-02-05 17:45:46 +00:00
Alexander Matyushentsev
0a04a491d9 fix: version info should be avaialble if anonymous access is enabled (#5422)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:17:43 -08:00
kshamajain99
701ce05393 fix: disable jwt claim audience validation #5381 (#5413)
* fix: disable audience validation

Signed-off-by: kshamajain99 <kshamajain99@gmail.com>

* update other places

Signed-off-by: kshamajain99 <kshamajain99@gmail.com>
2021-02-05 09:17:39 -08:00
Alexander Matyushentsev
965825f752 fix: /api/version should not return tools version for unauthenticated requests (#5415)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:17:31 -08:00
Alexander Matyushentsev
bd73326b8a fix: account tokens should be rejected if required capability is disabled (#5414)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:16:56 -08:00
Alexander Matyushentsev
502b8944c4 feat: set X-XSS-Protection while serving static content (#5412)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:16:52 -08:00
Alexander Matyushentsev
f5b0db240b fix: tokens keep working after account is deactivated (#5402)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:16:48 -08:00
Alexander Matyushentsev
ce43b7a438 fix: a request which was using a revoked project token, would still be allowed to perform requests allowed by default policy (#5378)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-02-05 09:16:44 -08:00
Liviu Costea
ebcfea64ff refactor(jwt): use typed access to claims (#5075)
Signed-off-by: Liviu Costea <email.lcostea@gmail.com>
2021-02-05 09:16:31 -08:00
Regina Scott
14c3dd2c59 fix: overriding version logic in warning banner (#5410)
Signed-off-by: Regina Scott <rescott@redhat.com>
2021-02-04 12:53:36 -08:00
Regina Scott
4359d345a0 feat: add versioning to argocd docs (#5099)
* feat: add versioning to argocd docs

Signed-off-by: Regina Scott <rescott@redhat.com>

* make default branch stable, provide warning for latest

Signed-off-by: Regina Scott <rescott@redhat.com>
2021-02-04 12:24:34 -08:00
Regina Scott
7081068a2d fix: Capitalization in toc (#5024)
Signed-off-by: Regina Scott <rescott@redhat.com>
2021-02-04 12:24:24 -08:00
argo-bot
0f9c684278 Bump version to 1.8.3 2021-01-21 22:09:58 +00:00
argo-bot
3ea3c13665 Bump version to 1.8.3 2021-01-21 22:09:44 +00:00
Alexander Matyushentsev
13fed83ec6 fix: make sure JWT token time fields contain only integer values (#5228)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2021-01-11 13:59:58 -08:00
60 changed files with 995 additions and 215 deletions

7
.readthedocs.yml Normal file
View File

@@ -0,0 +1,7 @@
version: 2
formats: all
mkdocs:
fail_on_warning: false
python:
install:
- requirements: docs/requirements.txt

47
SECURITY.md Normal file
View File

@@ -0,0 +1,47 @@
# Security Policy for Argo CD
Version: **v1.0 (2020-02-26)**
## Preface
As a deployment tool, Argo CD needs to have production access which makes
security a very important topic. The Argoproj team takes security very
seriously and is continuously working on improving it.
## Supported Versions
We currently support the most recent release (`N`, e.g. `1.8`) and the release
previous to the most recent one (`N-1`, e.g. `1.7`). With the release of
`N+1`, `N-1` drops out of support and `N` becomes `N-1`.
We regularly perform patch releases (e.g. `1.8.5` and `1.7.12`) for the
supported versions, which will contain fixes for security vulnerabilities and
important bugs. Prior releases might receive critical security fixes on a best
effort basis, however, it cannot be guaranteed that security fixes get
back-ported to these unsupported versions.
In rare cases, where a security fix needs complex re-design of a feature or is
otherwise very intrusive, and there's a workaround available, we may decide to
provide a forward-fix only, e.g. to be released the next minor release, instead
of releasing it within a patch branch for the currently supported releases.
## Reporting a Vulnerability
If you find a security related bug in ArgoCD, we kindly ask you for responsible
disclosure and for giving us appropriate time to react, analyze and develop a
fix to mitigate the found security vulnerability.
We will do our best to react quickly on your inquiry, and to coordinate a fix
and disclosure with you. Sometimes, it might take a little longer for us to
react (e.g. out of office conditions), so please bear with us in these cases.
We will publish security advisiories using the Git Hub SA feature to keep our
community well informed, and will credit you for your findings (unless you
prefer to stay anonymous, of course).
Please report vulnerabilities by e-mail to all of the following people:
* jfischer@redhat.com
* Jesse_Suen@intuit.com
* Alexander_Matyushentsev@intuit.com
* Edward_Lee@intuit.com

View File

@@ -1 +1 @@
1.8.2
1.8.7

View File

@@ -78,6 +78,7 @@ func NewCommand() *cobra.Command {
cache, err := cacheSrc()
errors.CheckError(err)
cache.Cache.SetClient(cacheutil.NewTwoLevelClient(cache.Cache.GetClient(), 10*time.Minute))
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
kubectl := kubeutil.NewKubectl()

View File

@@ -1901,7 +1901,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
selectedResourcesAreReady = checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation)
}
if selectedResourcesAreReady && !operationInProgress {
if selectedResourcesAreReady && (!operationInProgress || !watchOperation) {
app = printFinalStatus(app)
return app, nil
}

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"html"
"net/http"
"os"
"strconv"
@@ -25,6 +26,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 +115,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 +163,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
@@ -190,7 +192,7 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
var refreshToken string
handleErr := func(w http.ResponseWriter, errMsg string) {
http.Error(w, errMsg, http.StatusBadRequest)
http.Error(w, html.EscapeString(errMsg), http.StatusBadRequest)
completionChan <- errMsg
}
@@ -205,7 +207,7 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
log.Debugf("Callback: %s", r.URL)
if formErr := r.FormValue("error"); formErr != "" {
handleErr(w, formErr+": "+r.FormValue("error_description"))
handleErr(w, fmt.Sprintf("%s: %s", formErr, r.FormValue("error_description")))
return
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -294,7 +294,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
})
c.clusters[cluster.Server] = clusterCache
c.clusters[server] = clusterCache
return clusterCache, nil
}

175
docs/assets/versions.css Normal file
View File

@@ -0,0 +1,175 @@
.md-header__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: rgb(248, 243, 236);
position: sticky;
top: 0;
z-index: 2;
}
div[data-md-component=announce]>div#announce-msg{
color: var(--md-code-hl-number-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
}
}

58
docs/assets/versions.js Normal file
View File

@@ -0,0 +1,58 @@
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__inner > .md-header__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() {
var rtdData = window['READTHEDOCS_DATA'] || { version: 'latest' };
var margin = 30;
var headerHeight = document.getElementsByClassName("md-header")[0].offsetHeight;
if (rtdData.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>"
var bannerHeight = document.getElementById('announce-msg').offsetHeight + margin
document.querySelector("header.md-header").style.top = bannerHeight +"px";
document.querySelector('style').textContent +=
"@media screen and (min-width: 76.25em){ .md-sidebar { height: 0; top:"+ (bannerHeight+headerHeight)+"px !important; }}"
document.querySelector('style').textContent +=
"@media screen and (min-width: 60em){ .md-sidebar--secondary { height: 0; top:"+ (bannerHeight+headerHeight)+"px !important; }}"
}
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>"
var bannerHeight = document.getElementById('announce-msg').offsetHeight + margin
document.querySelector("header.md-header").style.top = bannerHeight +"px";
document.querySelector('style').textContent +=
"@media screen and (min-width: 76.25em){ .md-sidebar { height: 0; top:"+ (bannerHeight+headerHeight)+"px !important; }}"
document.querySelector('style').textContent +=
"@media screen and (min-width: 60em){ .md-sidebar--secondary { height: 0; top:"+ (bannerHeight+headerHeight)+"px !important; }}"
}
});

View File

@@ -28,7 +28,7 @@ Breaking down the permissions definition differs slightly between applications a
### RBAC Resources and Actions
Resources: `clusters`, `projects`, `applications`, `repositories`, `certificates`
Resources: `clusters`, `projects`, `applications`, `repositories`, `certificates`, `accounts`, `gpgkeys`
Actions: `get`, `create`, `update`, `delete`, `sync`, `override`, `action`

4
docs/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
mkdocs==1.1.2
mkdocs-material==7.1.7
markdown_include==0.6.0
pygments==2.7.4

View File

@@ -1,5 +1,11 @@
# Security Considerations
!!!warning "Deprecation notice"
This page is now deprecated and serves as an archive only. For up-to-date
information, please have a look at our
[security policy](https://github.com/argoproj/argo-cd/security/policy) and
[published security advisories](https://github.com/argoproj/argo-cd/security/advisories).
As a deployment tool, Argo CD needs to have production access which makes security a very important topic.
The Argoproj team takes security very seriously and continuously working on improving it. Learn more about security
related features in [Security](./operator-manual/security.md) section.
@@ -171,12 +177,6 @@ Upgrade to ArgoCD v1.5.0 or higher. No workaround available
## Reporting Vulnerabilities
If you find a security related bug in ArgoCD, we kindly ask you for responsible
disclosure and for giving us appropriate time to react, analyze and develop a
fix to mitigate the found security vulnerability.
Please report security vulnerabilities by e-mailing:
* [Jesse_Suen@intuit.com](mailto:Jesse_Suen@intuit.com)
* [Alexander_Matyushentsev@intuit.com](mailto:Alexander_Matyushentsev@intuit.com)
* [Edward_Lee@intuit.com](mailto:Edward_Lee@intuit.com)
Please have a look at our
[security policy](https://github.com/argoproj/argo-cd/security/policy)
for more details on how to report security vulnerabilities for Argo CD.

2
go.mod
View File

@@ -7,7 +7,7 @@ require (
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/argoproj/gitops-engine v0.2.1
github.com/argoproj/gitops-engine v0.2.2
github.com/argoproj/pkg v0.2.0
github.com/bombsimon/logrusr v1.0.0
github.com/casbin/casbin v1.9.1

4
go.sum
View File

@@ -79,8 +79,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
github.com/argoproj/gitops-engine v0.2.1 h1:iXmTCCM0m2u/YVLMhJatU21awuAscGiirscn13rYQtE=
github.com/argoproj/gitops-engine v0.2.1/go.mod h1:OxXp8YaT73rw9gEBnGBWg55af80nkV/uIjWCbJu1Nw0=
github.com/argoproj/gitops-engine v0.2.2 h1:islNwFTaHVVgx2pMvgXqINV93WPCVInWe64DlasFSx4=
github.com/argoproj/gitops-engine v0.2.2/go.mod h1:OxXp8YaT73rw9gEBnGBWg55af80nkV/uIjWCbJu1Nw0=
github.com/argoproj/pkg v0.2.0 h1:ETgC600kr8WcAi3MEVY5sA1H7H/u1/IysYOobwsZ8No=
github.com/argoproj/pkg v0.2.0/go.mod h1:F4TZgInLUEjzsWFB/BTJBsewoEy0ucnKSq6vmQiD/yc=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: v1.8.2
newTag: v1.8.7
resources:
- ./application-controller
- ./dex

View File

@@ -11,7 +11,7 @@ patchesStrategicMerge:
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: v1.8.2
newTag: v1.8.7
resources:
- ../../base/application-controller
- ../../base/dex

View File

@@ -2864,7 +2864,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -3002,7 +3002,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -3084,7 +3084,7 @@ spec:
env:
- name: ARGOCD_API_SERVER_REPLICAS
value: "2"
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3160,7 +3160,7 @@ spec:
- "10"
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -2779,7 +2779,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2917,7 +2917,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2999,7 +2999,7 @@ spec:
env:
- name: ARGOCD_API_SERVER_REPLICAS
value: "2"
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3075,7 +3075,7 @@ spec:
- "10"
- --redis
- argocd-redis-ha-haproxy:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -2453,7 +2453,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2553,7 +2553,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2631,7 +2631,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2706,7 +2706,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -2368,7 +2368,7 @@ spec:
- -n
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2468,7 +2468,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2546,7 +2546,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2621,7 +2621,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:v1.8.2
image: argoproj/argocd:v1.8.7
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -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'

View File

@@ -0,0 +1,3 @@
{% macro t(key) %}{{ {
"toc.title": "Table of Contents"
}[key] }}{% endmacro %}

View File

@@ -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
}

View File

@@ -943,6 +943,17 @@ type ApplicationTree struct {
OrphanedNodes []ResourceNode `json:"orphanedNodes,omitempty" protobuf:"bytes,2,rep,name=orphanedNodes"`
}
// Normalize sorts application tree nodes and hosts. The persistent order allows to
// effectively compare previously cached app tree and allows to unnecessary Redis requests.
func (t *ApplicationTree) Normalize() {
sort.Slice(t.Nodes, func(i, j int) bool {
return t.Nodes[i].FullName() < t.Nodes[j].FullName()
})
sort.Slice(t.OrphanedNodes, func(i, j int) bool {
return t.OrphanedNodes[i].FullName() < t.OrphanedNodes[j].FullName()
})
}
type ApplicationSummary struct {
// ExternalURLs holds all external URLs of application child resources.
ExternalURLs []string `json:"externalURLs,omitempty" protobuf:"bytes,1,opt,name=externalURLs"`
@@ -1011,6 +1022,11 @@ type ResourceNode struct {
CreatedAt *metav1.Time `json:"createdAt,omitempty" protobuf:"bytes,8,opt,name=createdAt"`
}
// FullName returns node full name
func (n *ResourceNode) FullName() string {
return fmt.Sprintf("%s/%s/%s/%s", n.Group, n.Kind, n.Namespace, n.Name)
}
func (n *ResourceNode) GroupKindVersion() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: n.Group,
@@ -1056,6 +1072,11 @@ type ResourceDiff struct {
PredictedLiveState string `json:"predictedLiveState,omitempty" protobuf:"bytes,10,opt,name=predictedLiveState"`
}
// FullName returns full name of a node that was used for diffing
func (r *ResourceDiff) FullName() string {
return fmt.Sprintf("%s/%s/%s/%s", r.Group, r.Kind, r.Namespace, r.Name)
}
// ConnectionStatus represents connection status
type ConnectionStatus = string
@@ -2443,13 +2464,19 @@ func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool
res := metav1.GroupKind{Group: gk.Group, Kind: gk.Kind}
if namespaced {
isWhiteListed = len(proj.Spec.NamespaceResourceWhitelist) == 0 || isResourceInList(res, proj.Spec.NamespaceResourceWhitelist)
isBlackListed = isResourceInList(res, proj.Spec.NamespaceResourceBlacklist)
namespaceWhitelist := proj.Spec.NamespaceResourceWhitelist
namespaceBlacklist := proj.Spec.NamespaceResourceBlacklist
isWhiteListed = namespaceWhitelist == nil || len(namespaceWhitelist) != 0 && isResourceInList(res, namespaceWhitelist)
isBlackListed = len(namespaceBlacklist) != 0 && isResourceInList(res, namespaceBlacklist)
return isWhiteListed && !isBlackListed
}
isWhiteListed = len(proj.Spec.ClusterResourceWhitelist) == 0 || isResourceInList(res, proj.Spec.ClusterResourceWhitelist)
isBlackListed = isResourceInList(res, proj.Spec.ClusterResourceBlacklist)
clusterWhitelist := proj.Spec.ClusterResourceWhitelist
clusterBlacklist := proj.Spec.ClusterResourceBlacklist
isWhiteListed = len(clusterWhitelist) != 0 && isResourceInList(res, clusterWhitelist)
isBlackListed = len(clusterBlacklist) != 0 && isResourceInList(res, clusterBlacklist)
return isWhiteListed && !isBlackListed
}
@@ -2707,8 +2734,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 +2761,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
}

View File

@@ -134,7 +134,7 @@ func TestAppProject_IsGroupKindPermitted(t *testing.T) {
NamespaceResourceBlacklist: []metav1.GroupKind{{Group: "apps", Kind: "Deployment"}},
},
}
assert.True(t, proj.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, true))
assert.False(t, proj.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, true))
assert.False(t, proj.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Deployment"}, true))
proj2 := AppProject{
@@ -161,6 +161,21 @@ func TestAppProject_IsGroupKindPermitted(t *testing.T) {
}
assert.False(t, proj4.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj4.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
proj5 := AppProject{
Spec: AppProjectSpec{
ClusterResourceWhitelist: []metav1.GroupKind{},
NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
},
}
assert.False(t, proj5.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj5.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
proj6 := AppProject{
Spec: AppProjectSpec{},
}
assert.False(t, proj6.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj6.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
}
func TestAppProject_GetRoleByName(t *testing.T) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -899,6 +899,10 @@ func (s *Server) PatchResource(ctx context.Context, q *application.ApplicationRe
manifest, err := s.kubectl.PatchResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace, types.PatchType(q.PatchType), []byte(q.Patch))
if err != nil {
// don't expose real error for secrets since it might contain secret data
if res.Kind == kube.SecretKind && res.Group == "" {
return nil, fmt.Errorf("failed to patch Secret %s/%s", res.Namespace, res.Name)
}
return nil, err
}
manifest, err = replaceSecretValues(manifest)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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...)

View File

@@ -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"})

View File

@@ -209,8 +209,6 @@ 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)
@@ -218,6 +216,7 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer {
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") {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/argoproj/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
networkingv1beta "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -425,6 +426,15 @@ func TestAppWithSecrets(t *testing.T) {
diffOutput := FailOnErr(RunCli("app", "diff", app.Name)).(string)
assert.Empty(t, diffOutput)
// make sure resource update error does not print secret details
_, err = RunCli("app", "patch-resource", "test-app-with-secrets", "--resource-name", "test-secret",
"--kind", "Secret", "--patch", `{"op": "add", "path": "/data", "value": "hello"}'`,
"--patch-type", "application/json-patch+json")
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("failed to patch Secret %s/test-secret", DeploymentNamespace()))
assert.NotContains(t, err.Error(), "username")
assert.NotContains(t, err.Error(), "password")
// patch secret and make sure app is out of sync and diff detects the change
FailOnErr(KubeClientset.CoreV1().Secrets(DeploymentNamespace()).Patch(context.Background(),
"test-secret", types.JSONPatchType, []byte(`[
@@ -1514,3 +1524,30 @@ definitions:
assert.Equal(t, "update-status", text)
})
}
func TestAppWaitOperationInProgress(t *testing.T) {
Given(t).
And(func() {
SetResourceOverrides(map[string]ResourceOverride{
"batch/Job": {
HealthLua: `return { status = 'Running' }`,
},
"apps/Deployment": {
HealthLua: `return { status = 'Suspended' }`,
},
})
}).
Async(true).
Path("hook-and-deployment").
When().
Create().
Sync().
Then().
// stuck in running state
Expect(OperationPhaseIs(OperationRunning)).
When().
And(func() {
_, err := RunCli("app", "wait", Name(), "--suspended")
errors.CheckError(err)
})
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/test/e2e/fixture"
. "github.com/argoproj/argo-cd/test/e2e/fixture/app"
"github.com/argoproj/gitops-engine/pkg/health"
@@ -16,23 +17,30 @@ func TestFixingDegradedApp(t *testing.T) {
When().
IgnoreErrors().
Create().
PatchFile("pod-1.yaml", `[{"op": "replace", "path": "/spec/containers/0/image", "value": "rubbish"}]`).
And(func() {
SetResourceOverrides(map[string]ResourceOverride{
"ConfigMap": {
HealthLua: `return { status = obj.metadata.annotations and obj.metadata.annotations['health'] or 'Degraded' }`,
},
})
}).
Sync().
Then().
Expect(OperationPhaseIs(OperationFailed)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(ResourceResultNumbering(1)).
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("Pod", "pod-1", health.HealthStatusDegraded)).
Expect(ResourceSyncStatusIs("Pod", "pod-2", SyncStatusCodeOutOfSync)).
Expect(ResourceHealthIs("Pod", "pod-2", health.HealthStatusMissing)).
Expect(ResourceSyncStatusIs("ConfigMap", "cm-1", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("ConfigMap", "cm-1", health.HealthStatusDegraded)).
Expect(ResourceSyncStatusIs("ConfigMap", "cm-2", SyncStatusCodeOutOfSync)).
Expect(ResourceHealthIs("ConfigMap", "cm-2", health.HealthStatusMissing)).
When().
PatchFile("pod-1.yaml", `[{"op": "replace", "path": "/spec/containers/0/image", "value": "nginx:1.17.4-alpine"}]`).
PatchFile("cm-1.yaml", `[{"op": "replace", "path": "/metadata/annotations/health", "value": "Healthy"}]`).
PatchFile("cm-2.yaml", `[{"op": "replace", "path": "/metadata/annotations/health", "value": "Healthy"}]`).
// need to force a refresh here
Refresh(RefreshTypeNormal).
Then().
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync)).
Expect(ResourceSyncStatusIs("ConfigMap", "cm-1", SyncStatusCodeOutOfSync)).
When().
Sync().
Then().
@@ -40,10 +48,10 @@ func TestFixingDegradedApp(t *testing.T) {
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(ResourceResultNumbering(2)).
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("Pod", "pod-1", health.HealthStatusHealthy)).
Expect(ResourceSyncStatusIs("Pod", "pod-2", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("Pod", "pod-2", health.HealthStatusHealthy))
Expect(ResourceSyncStatusIs("ConfigMap", "cm-1", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("ConfigMap", "cm-1", health.HealthStatusHealthy)).
Expect(ResourceSyncStatusIs("ConfigMap", "cm-2", SyncStatusCodeSynced)).
Expect(ResourceHealthIs("ConfigMap", "cm-2", health.HealthStatusHealthy))
}
func TestOneProgressingDeploymentIsSucceededAndSynced(t *testing.T) {

View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-1
annotations:
argocd.argoproj.io/sync-wave: "1"

View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-2
annotations:
argocd.argoproj.io/sync-wave: "2"

View File

@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-1
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
containers:
- name: main
image: nginx:1.17.4-alpine
imagePullPolicy: IfNotPresent

View File

@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-2
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
containers:
- name: main
image: nginx:1.17.4-alpine
imagePullPolicy: IfNotPresent

View File

@@ -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) {})

View File

@@ -3,6 +3,7 @@ package appstate
import (
"context"
"fmt"
"sort"
"time"
"github.com/go-redis/redis/v8"
@@ -61,6 +62,9 @@ func (c *Cache) GetAppManagedResources(appName string, res *[]*appv1.ResourceDif
}
func (c *Cache) SetAppManagedResources(appName string, managedResources []*appv1.ResourceDiff) error {
sort.Slice(managedResources, func(i, j int) bool {
return managedResources[i].FullName() < managedResources[j].FullName()
})
return c.SetItem(appManagedResourcesKey(appName), managedResources, c.appStateCacheExpiration, managedResources == nil)
}
@@ -82,6 +86,9 @@ func (c *Cache) OnAppResourcesTreeChanged(ctx context.Context, appName string, c
}
func (c *Cache) SetAppResourcesTree(appName string, resourcesTree *appv1.ApplicationTree) error {
if resourcesTree != nil {
resourcesTree.Normalize()
}
err := c.SetItem(appResourcesTreeKey(appName), resourcesTree, c.appStateCacheExpiration, resourcesTree == nil)
if err != nil {
return err

8
util/cache/cache.go vendored
View File

@@ -78,6 +78,14 @@ type Cache struct {
client CacheClient
}
func (c *Cache) GetClient() CacheClient {
return c.client
}
func (c *Cache) SetClient(client CacheClient) {
c.client = client
}
func (c *Cache) SetItem(key string, item interface{}, expiration time.Duration, delete bool) error {
key = fmt.Sprintf("%s|%s", key, common.CacheVersion)
if delete {

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/gob"
"fmt"
"time"
gocache "github.com/patrickmn/go-cache"
@@ -29,6 +30,25 @@ func (i *InMemoryCache) Set(item *Item) error {
return nil
}
// HasSame returns true if key with the same value already present in cache
func (i *InMemoryCache) HasSame(key string, obj interface{}) (bool, error) {
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(obj)
if err != nil {
return false, err
}
bufIf, found := i.memCache.Get(key)
if !found {
return false, nil
}
existingBuf, ok := bufIf.(bytes.Buffer)
if !ok {
panic(fmt.Errorf("InMemoryCache has unexpected entry: %v", existingBuf))
}
return bytes.Equal(buf.Bytes(), existingBuf.Bytes()), nil
}
func (i *InMemoryCache) Get(key string, obj interface{}) error {
bufIf, found := i.memCache.Get(key)
if !found {

68
util/cache/twolevelclient.go vendored Normal file
View File

@@ -0,0 +1,68 @@
package cache
import (
"context"
"time"
log "github.com/sirupsen/logrus"
)
// NewTwoLevelClient creates cache client that proxies requests to given external cache and tries to minimize
// number of requests to external client by storing cache entries in local in-memory cache.
func NewTwoLevelClient(client CacheClient, inMemoryExpiration time.Duration) *twoLevelClient {
return &twoLevelClient{inMemoryCache: NewInMemoryCache(inMemoryExpiration), externalCache: client}
}
type twoLevelClient struct {
inMemoryCache *InMemoryCache
externalCache CacheClient
}
// Set stores the given value in both in-memory and external cache.
// Skip storing the value in external cache if the same value already exists in memory to avoid requesting external cache.
func (c *twoLevelClient) Set(item *Item) error {
has, err := c.inMemoryCache.HasSame(item.Key, item.Object)
if has {
return nil
}
if err != nil {
log.Warnf("Failed to check key '%s' in in-memory cache: %v", item.Key, err)
}
err = c.inMemoryCache.Set(item)
if err != nil {
log.Warnf("Failed to save key '%s' in in-memory cache: %v", item.Key, err)
}
return c.externalCache.Set(item)
}
// Get returns cache value from in-memory cache if it present. Otherwise loads it from external cache and persists
// in memory to avoid future requests to external cache.
func (c *twoLevelClient) Get(key string, obj interface{}) error {
err := c.inMemoryCache.Get(key, obj)
if err == nil {
return nil
}
err = c.externalCache.Get(key, obj)
if err == nil {
_ = c.inMemoryCache.Set(&Item{Key: key, Object: obj})
}
return err
}
// Delete deletes cache for given key in both in-memory and external cache.
func (c *twoLevelClient) Delete(key string) error {
err := c.inMemoryCache.Delete(key)
if err != nil {
return err
}
return c.externalCache.Delete(key)
}
func (c *twoLevelClient) OnUpdated(ctx context.Context, key string, callback func() error) error {
return c.externalCache.OnUpdated(ctx, key, callback)
}
func (c *twoLevelClient) NotifyUpdated(key string) error {
return c.externalCache.NotifyUpdated(key)
}

View File

@@ -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)

View File

@@ -34,13 +34,15 @@ func Run(cmd *exec.Cmd) (string, error) {
}
func RunWithRedactor(cmd *exec.Cmd, redactor func(text string) string) (string, error) {
opts := argoexec.CmdOpts{Timeout: timeout}
span := tracing.NewLoggingTracer(log.NewLogrusLogger(logrus.New())).StartSpan(fmt.Sprintf("exec %v", cmd.Args[0]))
span.SetBaggageItem("dir", fmt.Sprintf("%v", cmd.Dir))
span.SetBaggageItem("args", fmt.Sprintf("%v", cmd.Args))
defer span.Finish()
opts := argoexec.CmdOpts{Timeout: timeout}
if redactor != nil {
span.SetBaggageItem("args", redactor(fmt.Sprintf("%v", cmd.Args)))
opts.Redactor = redactor
} else {
span.SetBaggageItem("args", fmt.Sprintf("%v", cmd.Args))
}
defer span.Finish()
return argoexec.RunCommandExt(cmd, opts)
}

View File

@@ -3,6 +3,7 @@ package exec
import (
"os"
"os/exec"
"regexp"
"testing"
"time"
@@ -27,3 +28,14 @@ func TestRun(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, out)
}
func TestHideUsernamePassword(t *testing.T) {
_, err := RunWithRedactor(exec.Command("helm registry login https://charts.bitnami.com/bitnami", "--username", "foo", "--password", "bar"), nil)
assert.NotEmpty(t, err)
var redactor = func(text string) string {
return regexp.MustCompile("(--username|--password) [^ ]*").ReplaceAllString(text, "$1 ******")
}
_, err = RunWithRedactor(exec.Command("helm registry login https://charts.bitnami.com/bitnami", "--username", "foo", "--password", "bar"), redactor)
assert.NotEmpty(t, err)
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
if issuedAtTime, err := jwtutil.IssuedAtTime(claims); err == nil {
rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", issuedAtTime.Format(time.RFC3339)))
}
iat, ok := iatField.(float64)
if !ok {
break
}
rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", time.Unix(int64(iat), 0).Format(time.RFC3339)))
}
errMsg = fmt.Sprintf("%s: %s", errMsg, strings.Join(rvalsStrs, ", "))
}

View File

@@ -2,6 +2,7 @@ package session
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
@@ -18,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"
@@ -32,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
@@ -128,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()
@@ -187,14 +191,48 @@ func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id
return mgr.signClaims(claims)
}
type standardClaims struct {
Audience jwt.ClaimStrings `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
ID string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
func unixTimeOrZero(t *jwt.Time) int64 {
if t == nil {
return 0
}
return t.Unix()
}
func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) {
log.Infof("Issuing claims: %v", claims)
// log.Infof("Issuing claims: %v", claims)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
settings, err := mgr.settingsMgr.GetSettings()
if err != nil {
return "", err
}
return token.SignedString(settings.ServerSignature)
// workaround for https://github.com/argoproj/argo-cd/issues/5217
// According to https://tools.ietf.org/html/rfc7519#section-4.1.6 "iat" and other time fields must contain
// number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time.
// The https://github.com/dgrijalva/jwt-go marshals time as non integer.
return token.SignedString(settings.ServerSignature, jwt.WithMarshaller(func(ctx jwt.CodingContext, v interface{}) ([]byte, error) {
if std, ok := v.(jwt.StandardClaims); ok {
return json.Marshal(standardClaims{
Audience: std.Audience,
ExpiresAt: unixTimeOrZero(std.ExpiresAt),
ID: std.ID,
IssuedAt: unixTimeOrZero(std.IssuedAt),
Issuer: std.Issuer,
NotBefore: unixTimeOrZero(std.NotBefore),
Subject: std.Subject,
})
}
return json.Marshal(v)
}))
}
// Parse tries to parse the provided string and returns the token claims for local login.
@@ -204,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
}
@@ -213,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
}
@@ -229,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")
}
@@ -423,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)
@@ -484,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")
}
}
@@ -497,7 +564,7 @@ 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) {
@@ -505,16 +572,7 @@ func Iat(ctx context.Context) (time.Time, error) {
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 {
@@ -522,7 +580,7 @@ func Sub(ctx context.Context) string {
if !ok {
return ""
}
return jwtutil.GetField(mapClaims, "sub")
return jwtutil.StringField(mapClaims, "sub")
}
func Groups(ctx context.Context, scopes []string) []string {

View File

@@ -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

View File

@@ -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,7 +369,7 @@ 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))
@@ -284,7 +377,7 @@ func TestMaxUsernameLength(t *testing.T) {
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"}