mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-27 13:08:46 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8b7df946 | ||
|
|
5d515b8423 | ||
|
|
69dcee049e | ||
|
|
d36d95dc9f | ||
|
|
df79d7db1d | ||
|
|
7165431a84 | ||
|
|
13ec3f43d8 | ||
|
|
eea93c5103 | ||
|
|
2cc81959b4 | ||
|
|
07ac038a8f | ||
|
|
3828da7f8c | ||
|
|
3bd9121545 | ||
|
|
df6e7c169e | ||
|
|
25b01666f0 | ||
|
|
9d6e6d84de | ||
|
|
7f9ff6e8c3 |
2
.github/workflows/ci-build.yaml
vendored
2
.github/workflows/ci-build.yaml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
env:
|
||||
# Golang version to use across CI steps
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
GOLANG_VERSION: '1.17'
|
||||
|
||||
jobs:
|
||||
check-go:
|
||||
|
||||
2
.github/workflows/image.yaml
vendored
2
.github/workflows/image.yaml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
types: [ labeled, unlabeled, opened, synchronize, reopened ]
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
GOLANG_VERSION: '1.17'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '!release-v0*'
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
GOLANG_VERSION: '1.17'
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
|
||||
@@ -4,7 +4,7 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:21.10
|
||||
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
|
||||
# Also used as the image in CI jobs so needs all dependencies
|
||||
####################################################################################################
|
||||
FROM docker.io/library/golang:1.17.6 as builder
|
||||
FROM docker.io/library/golang:1.17 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
@@ -102,7 +102,7 @@ RUN HOST_ARCH='amd64' NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTION
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM docker.io/library/golang:1.17.6 as argocd-build
|
||||
FROM docker.io/library/golang:1.17 as argocd-build
|
||||
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==7.1.7
|
||||
markdown_include==0.6.0
|
||||
pygments==2.7.4
|
||||
pygments==2.7.4
|
||||
jinja2===3.0.3
|
||||
113
docs/roadmap.md
113
docs/roadmap.md
@@ -1,26 +1,29 @@
|
||||
# Roadmap
|
||||
|
||||
- [Roadmap](#roadmap)
|
||||
- [v2.3](#v23)
|
||||
- [Merge Argo CD Notifications into Argo CD](#merge-argo-cd-notifications-into-argo-cd)
|
||||
- [Merge ApplicationSet controller into Argo CD](#merge-applicationset-controller-into-argo-cd)
|
||||
- [Compact resources tree](#compact-resources-tree)
|
||||
- [Maintain difference in cluster and git values for specific fields](#maintain-difference-in-cluster-and-git-values-for-specific-fields)
|
||||
- [ARM images and CLI binary](#arm-images-and-cli-binary)
|
||||
- [v2.4](#v24)
|
||||
- [Server side apply](#server-side-apply)
|
||||
- [v2.4 and beyond](#v24-and-beyond)
|
||||
- [First class support for ApplicationSet resources](#first-class-support-for-applicationset-resources)
|
||||
- [Input Forms UI Refresh](#input-forms-ui-refresh)
|
||||
- [Merge Argo CD Image Updater into Argo CD](#merge-argo-cd-image-updater-into-argo-cd)
|
||||
- [Web Shell](#web-shell)
|
||||
- [Helm values from external repo](#helm-values-from-external-repo)
|
||||
- [Support multiple sources for an Application](#support-multiple-sources-for-an-application)
|
||||
- [Config Management Tools Enhancements: Parametrization & Security Improvements](#config-management-tools-enhancements-parametrization--security-improvements)
|
||||
- [v2.5 and beyond](#v25-and-beyond)
|
||||
- [Config Management Tools Enhancements: UI/CLI](#config-management-tools-enhancements-uicli)
|
||||
- [First class support for ApplicationSet resources](#first-class-support-for-applicationset-resources)
|
||||
- [Merge Argo CD Image Updater into Argo CD](#merge-argo-cd-image-updater-into-argo-cd)
|
||||
- [Sharding application controller](#sharding-application-controller)
|
||||
- [Add support for secrets in Application parameters](#add-support-for-secrets-in-application-parameters)
|
||||
- [Config Management Tools Integrations UI/CLI](#config-management-tools-integrations-uicli)
|
||||
- [Allow specifying parent/child relationships in config](#allow-specifying-parentchild-relationships-in-config)
|
||||
- [Dependencies between applications](#dependencies-between-applications)
|
||||
- [Multi-tenancy improvements](#multi-tenancy-improvements)
|
||||
- [GitOps Engine Enhancements](#gitops-engine-enhancements)
|
||||
- [Completed](#completed)
|
||||
- [✅ Merge Argo CD Notifications into Argo CD](#-merge-argo-cd-notifications-into-argo-cd)
|
||||
- [✅ Merge ApplicationSet controller into Argo CD](#-merge-applicationset-controller-into-argo-cd)
|
||||
- [✅ Compact resources tree](#-compact-resources-tree)
|
||||
- [✅ Maintain difference in cluster and git values for specific fields](#-maintain-difference-in-cluster-and-git-values-for-specific-fields)
|
||||
- [✅ ARM images and CLI binary](#-arm-images-and-cli-binary)
|
||||
- [✅ Config Management Tools Integrations (proposal)](#-config-management-tools-integrations-proposal)
|
||||
- [✅ Argo CD Extensions (proposal)](#-argo-cd-extensions-proposal)
|
||||
- [✅ Project scoped repository and clusters (proposal)](#-project-scoped-repository-and-clusters-proposal)
|
||||
@@ -34,49 +37,19 @@
|
||||
- [✅ Automated Registry Monitoring](#-automated-registry-monitoring)
|
||||
- [✅ Projects Enhancements](#-projects-enhancements)
|
||||
|
||||
## v2.3
|
||||
## v2.4
|
||||
|
||||
> ETA: Feb 2021
|
||||
|
||||
### Merge Argo CD Notifications into Argo CD
|
||||
|
||||
The [Argo CD Notifications](https://github.com/argoproj-labs/argocd-notifications) should be merged into Argo CD and available out-of-the-box: [#7350](https://github.com/argoproj/argo-cd/issues/7350)
|
||||
|
||||
### Merge ApplicationSet controller into Argo CD
|
||||
|
||||
The ApplicationSet functionality is available in Argo CD out-of-the-box ([#7351](https://github.com/argoproj/argo-cd/issues/7351)).
|
||||
|
||||
### Compact resources tree
|
||||
|
||||
An ability to collaps leaf resources tree to improve visualization of very large applications: [#7349](https://github.com/argoproj/argo-cd/issues/7349)
|
||||
|
||||
### Maintain difference in cluster and git values for specific fields
|
||||
|
||||
The feature allows to avoid updating fields excluded from diffing ([#2913](https://github.com/argoproj/argo-cd/issues/2913)).
|
||||
|
||||
### ARM images and CLI binary
|
||||
|
||||
The release workflow should build and publish ARM images and CLI binaries: ([#4211](https://github.com/argoproj/argo-cd/issues/4211))
|
||||
> ETA: May 2022
|
||||
|
||||
### Server side apply
|
||||
|
||||
Support using [server side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) during application syncing
|
||||
[#2267](https://github.com/argoproj/argo-cd/issues/2267)
|
||||
|
||||
## v2.4 and beyond
|
||||
|
||||
### First class support for ApplicationSet resources
|
||||
|
||||
The Argo CD UI/CLI/API allows to manage ApplicationSet resources same as Argo CD Applications ([#7352](https://github.com/argoproj/argo-cd/issues/7352)).
|
||||
|
||||
### Input Forms UI Refresh
|
||||
|
||||
Improved design of the input forms in Argo CD Web UI: https://www.figma.com/file/IIlsFqqmM5UhqMVul9fQNq/Argo-CD?node-id=0%3A1
|
||||
|
||||
### Merge Argo CD Image Updater into Argo CD
|
||||
|
||||
The [Argo CD Image Updater](https://github.com/argoproj-labs/argocd-image-updater) should be merged into Argo CD and available out-of-the-box: [#7385](https://github.com/argoproj/argo-cd/issues/7385)
|
||||
|
||||
### Web Shell
|
||||
|
||||
Exec into the Kubernetes Pod right from Argo CD Web UI! [#4351](https://github.com/argoproj/argo-cd/issues/4351)
|
||||
@@ -85,15 +58,40 @@ Exec into the Kubernetes Pod right from Argo CD Web UI! [#4351](https://github.c
|
||||
|
||||
The feature allows combining of-the-shelf Helm chart and value file in Git repository ([#2789](https://github.com/argoproj/argo-cd/issues/2789))
|
||||
|
||||
### Support multiple sources for an Application
|
||||
|
||||
Support more than one source for creating an Application [#8322](https://github.com/argoproj/argo-cd/pull/8322).
|
||||
|
||||
### Config Management Tools Enhancements: Parametrization & Security Improvements
|
||||
|
||||
The continuation of the Config Management Tools of [proposal](https://github.com/argoproj/argo-cd/blob/master/docs/proposals/parameterized-config-management-plugins.md).
|
||||
The Argo config management plugin configuration allows users to specify the accepted parameters, default values to eventually power UI and CLI.
|
||||
Additionally, plugins implementation should provide better Argo CD tenant isolation and security.
|
||||
|
||||
## v2.5 and beyond
|
||||
|
||||
### Config Management Tools Enhancements: UI/CLI
|
||||
|
||||
The Argo CD should provide a first-class experience for configured third-party config management tools. User should be able to view supported parameters,
|
||||
observe default parameter values and override them.
|
||||
|
||||
### First class support for ApplicationSet resources
|
||||
|
||||
The Argo CD UI/CLI/API allows to manage ApplicationSet resources same as Argo CD Applications ([#7352](https://github.com/argoproj/argo-cd/issues/7352)).
|
||||
|
||||
### Merge Argo CD Image Updater into Argo CD
|
||||
|
||||
The [Argo CD Image Updater](https://github.com/argoproj-labs/argocd-image-updater) should be merged into Argo CD and available out-of-the-box: [#7385](https://github.com/argoproj/argo-cd/issues/7385)
|
||||
|
||||
|
||||
### Sharding application controller
|
||||
|
||||
Application controller to scale automatically to provide high availability[#8340](https://github.com/argoproj/argo-cd/issues/8340).
|
||||
|
||||
### Add support for secrets in Application parameters
|
||||
|
||||
The feature allows referencing secrets in Application parameters. [#1786](https://github.com/argoproj/argo-cd/issues/1786).
|
||||
|
||||
### Config Management Tools Integrations UI/CLI
|
||||
|
||||
The continuation of the Config Management Tools of [proposal](https://github.com/argoproj/argo-cd/pull/5927). The Argo CD UI/CLI
|
||||
should provide first class experience for configured third-party config management tools: [#5734](https://github.com/argoproj/argo-cd/issues/5734).
|
||||
|
||||
### Allow specifying parent/child relationships in config
|
||||
|
||||
The feature [#5082](https://github.com/argoproj/argo-cd/issues/5082) allows configuring parent/child relationships between resources. This allows to correctly
|
||||
@@ -123,13 +121,32 @@ A lot of Argo CD features are still not available in GitOps engine. The followin
|
||||
|
||||
## Completed
|
||||
|
||||
### ✅ Merge Argo CD Notifications into Argo CD
|
||||
|
||||
The [Argo CD Notifications](https://github.com/argoproj-labs/argocd-notifications) should be merged into Argo CD and available out-of-the-box: [#7350](https://github.com/argoproj/argo-cd/issues/7350)
|
||||
|
||||
### ✅ Merge ApplicationSet controller into Argo CD
|
||||
|
||||
The ApplicationSet functionality is available in Argo CD out-of-the-box ([#7351](https://github.com/argoproj/argo-cd/issues/7351)).
|
||||
|
||||
### ✅ Compact resources tree
|
||||
|
||||
An ability to collaps leaf resources tree to improve visualization of very large applications: [#7349](https://github.com/argoproj/argo-cd/issues/7349)
|
||||
|
||||
### ✅ Maintain difference in cluster and git values for specific fields
|
||||
|
||||
The feature allows to avoid updating fields excluded from diffing ([#2913](https://github.com/argoproj/argo-cd/issues/2913)).
|
||||
|
||||
### ✅ ARM images and CLI binary
|
||||
|
||||
The release workflow should build and publish ARM images and CLI binaries: ([#4211](https://github.com/argoproj/argo-cd/issues/4211))
|
||||
|
||||
### ✅ Config Management Tools Integrations ([proposal](https://github.com/argoproj/argo-cd/pull/5927))
|
||||
|
||||
The community likes the first class support of Helm, Kustomize and keeps requesting support for more tools.
|
||||
Argo CD provides a mechanism to integrate with any config management tool. We need to investigate why
|
||||
it is not enough and implement missing features.
|
||||
|
||||
|
||||
### ✅ Argo CD Extensions ([proposal](https://github.com/argoproj/argo-cd/pull/6240))
|
||||
|
||||
Argo CD supports customizing handling of Kubernetes resources via diffing customizations,
|
||||
|
||||
2
go.mod
2
go.mod
@@ -8,7 +8,7 @@ require (
|
||||
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible
|
||||
github.com/alicebob/miniredis/v2 v2.14.2
|
||||
github.com/argoproj/gitops-engine v0.6.1
|
||||
github.com/argoproj/gitops-engine v0.6.2
|
||||
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998
|
||||
github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0
|
||||
github.com/bombsimon/logrusr/v2 v2.0.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -125,8 +125,8 @@ github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/appscode/go v0.0.0-20190808133642-1d4ef1f1c1e0/go.mod h1:iy07dV61Z7QQdCKJCIvUoDL21u6AIceRhZzyleh2ymc=
|
||||
github.com/argoproj/gitops-engine v0.6.1 h1:fiaMQ+0OfBHQWInekgkO645i4dolvl3KA//0F7n4PK8=
|
||||
github.com/argoproj/gitops-engine v0.6.1/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
|
||||
github.com/argoproj/gitops-engine v0.6.2 h1:hM+pQeplCeIPAvfAmr1f91+ykxqaU0GAzuxVujqlKHM=
|
||||
github.com/argoproj/gitops-engine v0.6.2/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
|
||||
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998 h1:V9RDg+IZeebnm3XjkfkbN07VM21Fu1Cy/RJNoHO++VM=
|
||||
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998/go.mod h1:5mKv7zEgI3NO0L+fsuRSwBSY9EIXSuyIsDND8O8TTIw=
|
||||
github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 h1:Cfp7rO/HpVxnwlRqJe0jHiBbZ77ZgXhB6HWlYD02Xdc=
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.3.2
|
||||
newTag: v2.3.4
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
@@ -9692,7 +9692,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -9741,7 +9741,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
@@ -9906,7 +9906,7 @@ spec:
|
||||
key: controller.default.cache.expiration
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -11,4 +11,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.3.2
|
||||
newTag: v2.3.4
|
||||
|
||||
@@ -11,7 +11,7 @@ patchesStrategicMerge:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.3.2
|
||||
newTag: v2.3.4
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/dex
|
||||
|
||||
@@ -10516,7 +10516,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
@@ -10549,7 +10549,7 @@ spec:
|
||||
containers:
|
||||
- command:
|
||||
- argocd-notifications
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -10782,7 +10782,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -10831,7 +10831,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
@@ -11058,7 +11058,7 @@ spec:
|
||||
key: server.http.cookie.maxnumber
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -11254,7 +11254,7 @@ spec:
|
||||
key: controller.default.cache.expiration
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -7812,7 +7812,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
@@ -7845,7 +7845,7 @@ spec:
|
||||
containers:
|
||||
- command:
|
||||
- argocd-notifications
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -8078,7 +8078,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -8127,7 +8127,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
@@ -8354,7 +8354,7 @@ spec:
|
||||
key: server.http.cookie.maxnumber
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -8550,7 +8550,7 @@ spec:
|
||||
key: controller.default.cache.expiration
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -9886,7 +9886,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
@@ -9919,7 +9919,7 @@ spec:
|
||||
containers:
|
||||
- command:
|
||||
- argocd-notifications
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -10116,7 +10116,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -10165,7 +10165,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
@@ -10388,7 +10388,7 @@ spec:
|
||||
key: server.http.cookie.maxnumber
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -10578,7 +10578,7 @@ spec:
|
||||
key: controller.default.cache.expiration
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -7182,7 +7182,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
@@ -7215,7 +7215,7 @@ spec:
|
||||
containers:
|
||||
- command:
|
||||
- argocd-notifications
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -7412,7 +7412,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -7461,7 +7461,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
name: copyutil
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
@@ -7684,7 +7684,7 @@ spec:
|
||||
key: server.http.cookie.maxnumber
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -7874,7 +7874,7 @@ spec:
|
||||
key: controller.default.cache.expiration
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.3.2
|
||||
image: quay.io/argoproj/argocd:v2.3.4
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
||||
@@ -48,17 +48,17 @@ var (
|
||||
K8sMaxIdleConnections = env.ParseNumFromEnv(EnvK8sClientMaxIdleConnections, 500, 0, math.MaxInt32)
|
||||
|
||||
// K8sTLSHandshakeTimeout defines the maximum duration to wait for a TLS handshake to complete
|
||||
K8sTLSHandshakeTimeout = env.ParseDurationFromEnv(EnvK8sTLSHandshakeTimeout, 10*time.Second, 0, math.MaxInt32)
|
||||
K8sTLSHandshakeTimeout = env.ParseDurationFromEnv(EnvK8sTLSHandshakeTimeout, 10*time.Second, 0, math.MaxInt32*time.Second)
|
||||
|
||||
// K8sTCPTimeout defines the TCP timeout to use when performing K8s API requests
|
||||
K8sTCPTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 30*time.Second, 0, math.MaxInt32)
|
||||
K8sTCPTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 30*time.Second, 0, math.MaxInt32*time.Second)
|
||||
|
||||
// K8sTCPKeepAlive defines the interval for sending TCP keep alive to K8s API server
|
||||
K8sTCPKeepAlive = env.ParseDurationFromEnv(EnvK8sTCPKeepAlive, 30*time.Second, 0, math.MaxInt32)
|
||||
K8sTCPKeepAlive = env.ParseDurationFromEnv(EnvK8sTCPKeepAlive, 30*time.Second, 0, math.MaxInt32*time.Second)
|
||||
|
||||
// K8sTCPIdleConnTimeout defines the duration for keeping idle TCP connections to the K8s API server
|
||||
K8sTCPIdleConnTimeout = env.ParseDurationFromEnv(EnvK8sTCPIdleConnTimeout, 5*time.Minute, 0, math.MaxInt32)
|
||||
K8sTCPIdleConnTimeout = env.ParseDurationFromEnv(EnvK8sTCPIdleConnTimeout, 5*time.Minute, 0, math.MaxInt32*time.Second)
|
||||
|
||||
// K8sServerSideTimeout defines which server side timeout to send with each API request
|
||||
K8sServerSideTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 32*time.Second, 0, math.MaxInt32)
|
||||
K8sServerSideTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 0, 0, math.MaxInt32*time.Second)
|
||||
)
|
||||
|
||||
@@ -2477,6 +2477,8 @@ func (c *Cluster) RawRestConfig() *rest.Config {
|
||||
panic(fmt.Sprintf("Unable to create K8s REST config: %v", err))
|
||||
}
|
||||
config.Timeout = K8sServerSideTimeout
|
||||
config.QPS = K8sClientConfigQPS
|
||||
config.Burst = K8sClientConfigBurst
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/io/files"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/TomOnTime/utfutil"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
@@ -810,7 +812,8 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string,
|
||||
if directory = q.ApplicationSource.Directory; directory == nil {
|
||||
directory = &v1alpha1.ApplicationSourceDirectory{}
|
||||
}
|
||||
targetObjs, err = findManifests(appPath, repoRoot, env, *directory, q.EnabledSourceTypes)
|
||||
logCtx := log.WithField("application", q.AppName)
|
||||
targetObjs, err = findManifests(logCtx, appPath, repoRoot, env, *directory, q.EnabledSourceTypes)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1012,12 +1015,32 @@ func ksShow(appLabelKey, appPath string, ksonnetOpts *v1alpha1.ApplicationSource
|
||||
var manifestFile = regexp.MustCompile(`^.*\.(yaml|yml|json|jsonnet)$`)
|
||||
|
||||
// findManifests looks at all yaml files in a directory and unmarshals them into a list of unstructured objects
|
||||
func findManifests(appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory, enabledManifestGeneration map[string]bool) ([]*unstructured.Unstructured, error) {
|
||||
func findManifests(logCtx *log.Entry, appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory, enabledManifestGeneration map[string]bool) ([]*unstructured.Unstructured, error) {
|
||||
var objs []*unstructured.Unstructured
|
||||
err := filepath.Walk(appPath, func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(appPath, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path of symlink: %w", err)
|
||||
}
|
||||
if files.IsSymlink(f) {
|
||||
realPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
logCtx.Debugf("error checking symlink realpath: %s", err)
|
||||
if os.IsNotExist(err) {
|
||||
log.Warnf("ignoring out-of-bounds symlink at %q: %s", relPath, err)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("failed to evaluate symlink at %q: %w", relPath, err)
|
||||
}
|
||||
}
|
||||
if !files.Inbound(realPath, appPath) {
|
||||
logCtx.Warnf("illegal filepath in symlink: %s", realPath)
|
||||
return fmt.Errorf("illegal filepath in symlink at %q", relPath)
|
||||
}
|
||||
}
|
||||
if f.IsDir() {
|
||||
if path != appPath && !directory.Recurse {
|
||||
return filepath.SkipDir
|
||||
@@ -1030,10 +1053,6 @@ func findManifests(appPath string, repoRoot string, env *v1alpha1.Env, directory
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(appPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if directory.Exclude != "" && glob.Match(directory.Exclude, relPath) {
|
||||
return nil
|
||||
}
|
||||
@@ -1660,33 +1679,40 @@ func directoryPermissionInitializer(rootPath string) goio.Closer {
|
||||
// nolint:unparam
|
||||
func (s *Service) checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) (goio.Closer, error) {
|
||||
closer := s.gitRepoInitializer(gitClient.Root())
|
||||
return closer, checkoutRevision(gitClient, revision, submoduleEnabled)
|
||||
}
|
||||
|
||||
func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) error {
|
||||
err := gitClient.Init()
|
||||
if err != nil {
|
||||
return closer, status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
|
||||
return status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
|
||||
}
|
||||
|
||||
err = gitClient.Fetch(revision)
|
||||
|
||||
// Fetching with no revision first. Fetching with an explicit version can cause repo bloat. https://github.com/argoproj/argo-cd/issues/8845
|
||||
err = gitClient.Fetch("")
|
||||
if err != nil {
|
||||
log.Infof("Failed to fetch revision %s: %v", revision, err)
|
||||
log.Infof("Fallback to fetch default")
|
||||
err = gitClient.Fetch("")
|
||||
if err != nil {
|
||||
return closer, status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
|
||||
}
|
||||
err = gitClient.Checkout(revision, submoduleEnabled)
|
||||
if err != nil {
|
||||
return closer, status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
|
||||
}
|
||||
return closer, err
|
||||
return status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
|
||||
}
|
||||
|
||||
err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
|
||||
err = gitClient.Checkout(revision, submoduleEnabled)
|
||||
if err != nil {
|
||||
return closer, status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
|
||||
// When fetching with no revision, only refs/heads/* and refs/remotes/origin/* are fetched. If checkout fails
|
||||
// for the given revision, try explicitly fetching it.
|
||||
log.Infof("Failed to checkout revision %s: %v", revision, err)
|
||||
log.Infof("Fallback to fetching specific revision %s. ref might not have been in the default refspec fetched.", revision)
|
||||
|
||||
err = gitClient.Fetch(revision)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
|
||||
}
|
||||
|
||||
err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return closer, err
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -149,6 +151,76 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
|
||||
assert.Equal(t, 3, len(res2.Manifests))
|
||||
}
|
||||
|
||||
func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
outOfBoundsFilename string
|
||||
outOfBoundsFileContents string
|
||||
mustNotContain string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents.
|
||||
}{
|
||||
{
|
||||
name: "out of bounds JSON file should not appear in error output",
|
||||
outOfBoundsFilename: "test.json",
|
||||
outOfBoundsFileContents: `{"some": "json"}`,
|
||||
},
|
||||
{
|
||||
name: "malformed JSON file contents should not appear in error output",
|
||||
outOfBoundsFilename: "test.json",
|
||||
outOfBoundsFileContents: "$",
|
||||
},
|
||||
{
|
||||
name: "out of bounds JSON manifest should not appear in manifest output",
|
||||
outOfBoundsFilename: "test.json",
|
||||
// JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests.
|
||||
outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
|
||||
},
|
||||
{
|
||||
name: "out of bounds YAML manifest should not appear in manifest output",
|
||||
outOfBoundsFilename: "test.yaml",
|
||||
outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n name: test\n namespace: default\ntype: Opaque",
|
||||
mustNotContain: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCaseCopy := testCase
|
||||
t.Run(testCaseCopy.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
outOfBoundsDir := t.TempDir()
|
||||
outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename)
|
||||
err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0444))
|
||||
require.NoError(t, err)
|
||||
|
||||
repoDir := t.TempDir()
|
||||
err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename))
|
||||
require.NoError(t, err)
|
||||
|
||||
var mustNotContain = testCaseCopy.outOfBoundsFileContents
|
||||
if testCaseCopy.mustNotContain != "" {
|
||||
mustNotContain = testCaseCopy.mustNotContain
|
||||
}
|
||||
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}}
|
||||
res, err := GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{})
|
||||
require.Error(t, err)
|
||||
assert.NotContains(t, err.Error(), mustNotContain)
|
||||
assert.Contains(t, err.Error(), "illegal filepath")
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
|
||||
repoDir := t.TempDir()
|
||||
err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml"))
|
||||
require.NoError(t, err)
|
||||
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}}
|
||||
_, err = GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
||||
service := newService("../..")
|
||||
|
||||
@@ -1641,7 +1713,7 @@ func TestFindResources(t *testing.T) {
|
||||
for i := range testCases {
|
||||
tc := testCases[i]
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
Recurse: true,
|
||||
Include: tc.include,
|
||||
Exclude: tc.exclude,
|
||||
@@ -1659,7 +1731,7 @@ func TestFindResources(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindManifests_Exclude(t *testing.T) {
|
||||
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
Recurse: true,
|
||||
Exclude: "subdir/deploymentSub.yaml",
|
||||
}, map[string]bool{})
|
||||
@@ -1672,7 +1744,7 @@ func TestFindManifests_Exclude(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindManifests_Exclude_NothingMatches(t *testing.T) {
|
||||
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
|
||||
Recurse: true,
|
||||
Exclude: "nothing.yaml",
|
||||
}, map[string]bool{})
|
||||
@@ -1816,3 +1888,51 @@ func TestInit(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
|
||||
}
|
||||
|
||||
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
|
||||
// other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935
|
||||
func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
sourceRepoPath, err := ioutil.TempDir(rootPath, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
|
||||
// example, a GitHub ref for a pull into one repo from a fork of that repo.
|
||||
runGit(t, sourceRepoPath, "init")
|
||||
runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
|
||||
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
||||
runGit(t, sourceRepoPath, "checkout", "-b", "branch")
|
||||
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
|
||||
sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
|
||||
runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
|
||||
runGit(t, sourceRepoPath, "checkout", "main")
|
||||
runGit(t, sourceRepoPath, "branch", "-D", "branch")
|
||||
|
||||
destRepoPath, err := ioutil.TempDir(rootPath, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
pullSha, err := gitClient.LsRemote("refs/pull/123/head")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = checkoutRevision(gitClient, "does-not-exist", false)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = checkoutRevision(gitClient, pullSha, false)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard
|
||||
// and error output. If it fails, it stops the test with a failure message.
|
||||
func runGit(t *testing.T, workDir string, args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = workDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
stringOut := string(out)
|
||||
require.NoError(t, err, stringOut)
|
||||
return stringOut
|
||||
}
|
||||
|
||||
@@ -951,6 +951,8 @@ func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error
|
||||
}
|
||||
if !argoCDSettings.AnonymousUserEnabled {
|
||||
return ctx, claimsErr
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, "claims", "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
@@ -432,6 +435,386 @@ func TestAuthenticate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.RequestURI {
|
||||
case "/api/dex/.well-known/openid-configuration":
|
||||
_, err := io.WriteString(w, fmt.Sprintf(`
|
||||
{
|
||||
"issuer": "%[1]s/api/dex",
|
||||
"authorization_endpoint": "%[1]s/api/dex/auth",
|
||||
"token_endpoint": "%[1]s/api/dex/token",
|
||||
"jwks_uri": "%[1]s/api/dex/keys",
|
||||
"userinfo_endpoint": "%[1]s/api/dex/userinfo",
|
||||
"device_authorization_endpoint": "%[1]s/api/dex/device/code",
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"urn:ietf:params:oauth:grant-type:device_code"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256", "HS256"
|
||||
],
|
||||
"code_challenge_methods_supported": [
|
||||
"S256",
|
||||
"plain"
|
||||
],
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"email",
|
||||
"groups",
|
||||
"profile",
|
||||
"offline_access"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post"
|
||||
],
|
||||
"claims_supported": [
|
||||
"iss",
|
||||
"sub",
|
||||
"aud",
|
||||
"iat",
|
||||
"exp",
|
||||
"email",
|
||||
"email_verified",
|
||||
"locale",
|
||||
"name",
|
||||
"preferred_username",
|
||||
"at_hash"
|
||||
]
|
||||
}`, url))
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool) (argocd *ArgoCDServer, dexURL string) {
|
||||
cm := test.NewFakeConfigMap()
|
||||
if anonymousEnabled {
|
||||
cm.Data["users.anonymous.enabled"] = "true"
|
||||
}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
return // Start with a placeholder. We need the server URL before setting up the real handler.
|
||||
}))
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
dexMockHandler(t, ts.URL)(w, r)
|
||||
})
|
||||
if withFakeSSO {
|
||||
cm.Data["url"] = ts.URL
|
||||
cm.Data["dex.config"] = `
|
||||
connectors:
|
||||
# OIDC
|
||||
- type: OIDC
|
||||
id: oidc
|
||||
name: OIDC
|
||||
config:
|
||||
issuer: https://auth.example.gom
|
||||
clientID: test-client
|
||||
clientSecret: $dex.oidc.clientSecret`
|
||||
}
|
||||
secret := test.NewFakeSecret()
|
||||
kubeclientset := fake.NewSimpleClientset(cm, secret)
|
||||
appClientSet := apps.NewSimpleClientset()
|
||||
argoCDOpts := ArgoCDServerOpts{
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appClientSet,
|
||||
}
|
||||
if withFakeSSO {
|
||||
argoCDOpts.DexServerAddr = ts.URL
|
||||
}
|
||||
argocd = NewServer(context.Background(), argoCDOpts)
|
||||
return argocd, ts.URL
|
||||
}
|
||||
|
||||
func TestAuthenticate_3rd_party_JWTs(t *testing.T) {
|
||||
type testData struct {
|
||||
test string
|
||||
anonymousEnabled bool
|
||||
claims jwt.RegisteredClaims
|
||||
expectedErrorContains string
|
||||
expectedClaims interface{}
|
||||
}
|
||||
var tests = []testData{
|
||||
{
|
||||
test: "anonymous disabled, no audience",
|
||||
anonymousEnabled: false,
|
||||
claims: jwt.RegisteredClaims{},
|
||||
expectedErrorContains: "no audience found in the token",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, no audience",
|
||||
anonymousEnabled: true,
|
||||
claims: jwt.RegisteredClaims{},
|
||||
expectedErrorContains: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, unexpired token, admin claim",
|
||||
anonymousEnabled: false,
|
||||
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
|
||||
expectedErrorContains: "id token signed with unsupported algorithm",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, unexpired token, admin claim",
|
||||
anonymousEnabled: true,
|
||||
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
|
||||
expectedErrorContains: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, expired token, admin claim",
|
||||
anonymousEnabled: false,
|
||||
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
|
||||
expectedErrorContains: "token is expired",
|
||||
expectedClaims: jwt.RegisteredClaims{Issuer:"sso"},
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, expired token, admin claim",
|
||||
anonymousEnabled: true,
|
||||
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
|
||||
expectedErrorContains: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testData := range tests {
|
||||
testDataCopy := testData
|
||||
|
||||
t.Run(testDataCopy.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, true)
|
||||
ctx := context.Background()
|
||||
testDataCopy.claims.Issuer = fmt.Sprintf("%s/api/dex", dexURL)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, testDataCopy.claims)
|
||||
tokenString, err := token.SignedString([]byte("key"))
|
||||
require.NoError(t, err)
|
||||
ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))
|
||||
|
||||
ctx, err = argocd.Authenticate(ctx)
|
||||
claims := ctx.Value("claims")
|
||||
if testDataCopy.expectedClaims == nil {
|
||||
assert.Nil(t, claims)
|
||||
} else {
|
||||
assert.Equal(t, testDataCopy.expectedClaims, claims)
|
||||
}
|
||||
if testDataCopy.expectedErrorContains != "" {
|
||||
assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_no_request_metadata(t *testing.T) {
|
||||
type testData struct {
|
||||
test string
|
||||
anonymousEnabled bool
|
||||
expectedErrorContains string
|
||||
expectedClaims interface{}
|
||||
}
|
||||
var tests = []testData{
|
||||
{
|
||||
test: "anonymous disabled",
|
||||
anonymousEnabled: false,
|
||||
expectedErrorContains: "no session information",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled",
|
||||
anonymousEnabled: true,
|
||||
expectedErrorContains: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testData := range tests {
|
||||
testDataCopy := testData
|
||||
|
||||
t.Run(testDataCopy.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true)
|
||||
ctx := context.Background()
|
||||
|
||||
ctx, err := argocd.Authenticate(ctx)
|
||||
claims := ctx.Value("claims")
|
||||
assert.Equal(t, testDataCopy.expectedClaims, claims)
|
||||
if testDataCopy.expectedErrorContains != "" {
|
||||
assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_no_SSO(t *testing.T) {
|
||||
type testData struct {
|
||||
test string
|
||||
anonymousEnabled bool
|
||||
expectedErrorMessage string
|
||||
expectedClaims interface{}
|
||||
}
|
||||
var tests = []testData{
|
||||
{
|
||||
test: "anonymous disabled",
|
||||
anonymousEnabled: false,
|
||||
expectedErrorMessage: "SSO is not configured",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled",
|
||||
anonymousEnabled: true,
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testData := range tests {
|
||||
testDataCopy := testData
|
||||
|
||||
t.Run(testDataCopy.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false)
|
||||
ctx := context.Background()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{Issuer: fmt.Sprintf("%s/api/dex", dexURL)})
|
||||
tokenString, err := token.SignedString([]byte("key"))
|
||||
require.NoError(t, err)
|
||||
ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))
|
||||
|
||||
ctx, err = argocd.Authenticate(ctx)
|
||||
claims := ctx.Value("claims")
|
||||
assert.Equal(t, testDataCopy.expectedClaims, claims)
|
||||
if testDataCopy.expectedErrorMessage != "" {
|
||||
assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_bad_request_metadata(t *testing.T) {
|
||||
type testData struct {
|
||||
test string
|
||||
anonymousEnabled bool
|
||||
metadata metadata.MD
|
||||
expectedErrorMessage string
|
||||
expectedClaims interface{}
|
||||
}
|
||||
var tests = []testData{
|
||||
{
|
||||
test: "anonymous disabled, empty metadata",
|
||||
anonymousEnabled: false,
|
||||
metadata: metadata.MD{},
|
||||
expectedErrorMessage: "no session information",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, empty metadata",
|
||||
anonymousEnabled: true,
|
||||
metadata: metadata.MD{},
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, empty tokens",
|
||||
anonymousEnabled: false,
|
||||
metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}},
|
||||
expectedErrorMessage: "no session information",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, empty tokens",
|
||||
anonymousEnabled: true,
|
||||
metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}},
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, bad tokens",
|
||||
anonymousEnabled: false,
|
||||
metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
|
||||
expectedErrorMessage: "token contains an invalid number of segments",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, bad tokens",
|
||||
anonymousEnabled: true,
|
||||
metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, bad auth header",
|
||||
anonymousEnabled: false,
|
||||
metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
|
||||
expectedErrorMessage: "no audience found in the token",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, bad auth header",
|
||||
anonymousEnabled: true,
|
||||
metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
{
|
||||
test: "anonymous disabled, bad auth cookie",
|
||||
anonymousEnabled: false,
|
||||
metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
|
||||
expectedErrorMessage: "no audience found in the token",
|
||||
expectedClaims: nil,
|
||||
},
|
||||
{
|
||||
test: "anonymous enabled, bad auth cookie",
|
||||
anonymousEnabled: true,
|
||||
metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
|
||||
expectedErrorMessage: "",
|
||||
expectedClaims: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testData := range tests {
|
||||
testDataCopy := testData
|
||||
|
||||
t.Run(testDataCopy.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true)
|
||||
ctx := context.Background()
|
||||
ctx = metadata.NewIncomingContext(context.Background(), testDataCopy.metadata)
|
||||
|
||||
ctx, err := argocd.Authenticate(ctx)
|
||||
claims := ctx.Value("claims")
|
||||
assert.Equal(t, testDataCopy.expectedClaims, claims)
|
||||
if testDataCopy.expectedErrorMessage != "" {
|
||||
assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getToken(t *testing.T) {
|
||||
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM redis:6.2.6 as redis
|
||||
|
||||
FROM node:12.18.4 as node
|
||||
|
||||
FROM golang:1.17.6 as golang
|
||||
FROM golang:1.17 as golang
|
||||
|
||||
FROM registry:2.7.1 as registry
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17.6 AS go
|
||||
FROM golang:1.17 AS go
|
||||
|
||||
RUN go install github.com/mattn/goreman@latest && \
|
||||
go install github.com/kisielk/godepgraph@latest
|
||||
|
||||
@@ -20,7 +20,7 @@ interface State {
|
||||
loginError: string;
|
||||
loginInProgress: boolean;
|
||||
returnUrl: string;
|
||||
ssoLoginError: string;
|
||||
hasSsoLoginError: boolean;
|
||||
}
|
||||
|
||||
export class Login extends React.Component<RouteComponentProps<{}>, State> {
|
||||
@@ -31,13 +31,13 @@ export class Login extends React.Component<RouteComponentProps<{}>, State> {
|
||||
public static getDerivedStateFromProps(props: RouteComponentProps<{}>): Partial<State> {
|
||||
const search = new URLSearchParams(props.history.location.search);
|
||||
const returnUrl = search.get('return_url') || '';
|
||||
const ssoLoginError = search.get('sso_error') || '';
|
||||
return {ssoLoginError, returnUrl};
|
||||
const hasSsoLoginError = search.get('has_sso_error') === 'true';
|
||||
return {hasSsoLoginError, returnUrl};
|
||||
}
|
||||
|
||||
constructor(props: RouteComponentProps<{}>) {
|
||||
super(props);
|
||||
this.state = {authSettings: null, loginError: null, returnUrl: null, ssoLoginError: null, loginInProgress: false};
|
||||
this.state = {authSettings: null, loginError: null, returnUrl: null, hasSsoLoginError: false, loginInProgress: false};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
@@ -69,7 +69,7 @@ export class Login extends React.Component<RouteComponentProps<{}>, State> {
|
||||
)}
|
||||
</button>
|
||||
</a>
|
||||
{this.state.ssoLoginError && <div className='argo-form-row__error-msg'>{this.state.ssoLoginError}</div>}
|
||||
{this.state.hasSsoLoginError && <div className='argo-form-row__error-msg'>Login failed.</div>}
|
||||
{authSettings && !authSettings.userLoginsDisabled && (
|
||||
<div className='login__saml-separator'>
|
||||
<span>or</span>
|
||||
|
||||
@@ -88,6 +88,10 @@ const config = {
|
||||
{
|
||||
from: 'node_modules/redoc/bundles/redoc.standalone.js',
|
||||
to: 'assets/scripts/redoc.standalone.js'
|
||||
},
|
||||
{
|
||||
from: 'node_modules/monaco-editor/min/vs/base/browser/ui/codicons/codicon',
|
||||
to: 'assets/fonts'
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("./fonts/codicon.ttf") format("truetype");
|
||||
}
|
||||
|
||||
/* === Heebo - 300 */
|
||||
@font-face {
|
||||
font-family: 'Heebo';
|
||||
|
||||
@@ -3,20 +3,18 @@ package dex
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
)
|
||||
|
||||
var messageRe = regexp.MustCompile(`<p>(.*)([\s\S]*?)<\/p>`)
|
||||
|
||||
func decorateDirector(director func(req *http.Request), target *url.URL) func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
director(req)
|
||||
@@ -44,16 +42,10 @@ func NewDexHTTPReverseProxy(serverAddr string, baseHRef string) func(writer http
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var message string
|
||||
matches := messageRe.FindSubmatch(b)
|
||||
if len(matches) > 1 {
|
||||
message = html.UnescapeString(string(matches[1]))
|
||||
} else {
|
||||
message = "Unknown error"
|
||||
}
|
||||
log.Errorf("received error from dex: %s", string(b))
|
||||
resp.ContentLength = 0
|
||||
resp.Header.Set("Content-Length", strconv.Itoa(0))
|
||||
resp.Header.Set("Location", fmt.Sprintf("%s?sso_error=%s", path.Join(baseHRef, "login"), url.QueryEscape(message)))
|
||||
resp.Header.Set("Location", fmt.Sprintf("%s?has_sso_error=true", path.Join(baseHRef, "login")))
|
||||
resp.StatusCode = http.StatusSeeOther
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(make([]byte, 0)))
|
||||
return nil
|
||||
|
||||
@@ -408,7 +408,7 @@ func Test_DexReverseProxy(t *testing.T) {
|
||||
assert.Equal(t, http.StatusSeeOther, resp.StatusCode)
|
||||
location, _ := resp.Location()
|
||||
fmt.Printf("%s %s\n", resp.Status, location.RequestURI())
|
||||
assert.True(t, strings.HasPrefix(location.RequestURI(), "/login?sso_error"))
|
||||
assert.True(t, strings.HasPrefix(location.RequestURI(), "/login?has_sso_error=true"))
|
||||
})
|
||||
|
||||
t.Run("Invalid URL for Dex reverse proxy", func(t *testing.T) {
|
||||
|
||||
35
util/io/files/util.go
Normal file
35
util/io/files/util.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Inbound will validate if the given candidate path is inside the
|
||||
// baseDir. This is useful to make sure that malicious candidates
|
||||
// are not targeting a file outside of baseDir boundaries.
|
||||
// Considerations:
|
||||
// - baseDir must be absolute path. Will return false otherwise
|
||||
// - candidate can be absolute or relative path
|
||||
// - candidate should not be symlink as only syntatic validation is
|
||||
// applied by this function
|
||||
func Inbound(candidate, baseDir string) bool {
|
||||
if !filepath.IsAbs(baseDir) {
|
||||
return false
|
||||
}
|
||||
var target string
|
||||
if filepath.IsAbs(candidate) {
|
||||
target = filepath.Clean(candidate)
|
||||
} else {
|
||||
target = filepath.Join(baseDir, candidate)
|
||||
}
|
||||
return strings.HasPrefix(target, filepath.Clean(baseDir)+string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// IsSymlink return true if the given FileInfo relates to a
|
||||
// symlink file. Returns false otherwise.
|
||||
func IsSymlink(fi os.FileInfo) bool {
|
||||
return fi.Mode()&fs.ModeSymlink == fs.ModeSymlink
|
||||
}
|
||||
63
util/io/files/util_test.go
Normal file
63
util/io/files/util_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package files_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/io/files"
|
||||
)
|
||||
|
||||
func TestInbound(t *testing.T) {
|
||||
type testcase struct {
|
||||
name string
|
||||
candidate string
|
||||
basedir string
|
||||
expected bool
|
||||
}
|
||||
cases := []testcase{
|
||||
{
|
||||
name: "will return true if candidate is inbound",
|
||||
candidate: "/home/test/app/readme.md",
|
||||
basedir: "/home/test",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "will return false if candidate is not inbound",
|
||||
candidate: "/home/test/../readme.md",
|
||||
basedir: "/home/test",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "will return true if candidate is relative inbound",
|
||||
candidate: "./readme.md",
|
||||
basedir: "/home/test",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "will return false if candidate is relative outbound",
|
||||
candidate: "../readme.md",
|
||||
basedir: "/home/test",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "will return false if basedir is relative",
|
||||
candidate: "/home/test/app/readme.md",
|
||||
basedir: "./test",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
// given
|
||||
t.Parallel()
|
||||
|
||||
// when
|
||||
inbound := files.Inbound(c.candidate, c.basedir)
|
||||
|
||||
// then
|
||||
assert.Equal(t, c.expected, inbound)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
|
||||
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
// Don't forget to validate the alg is what you expect:
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return argoCDSettings.ServerSignature, nil
|
||||
})
|
||||
@@ -262,7 +262,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
|
||||
}
|
||||
|
||||
if account.PasswordMtime != nil && issuedAt.Before(*account.PasswordMtime) {
|
||||
return nil, "", fmt.Errorf("Account password has changed since token issued")
|
||||
return nil, "", fmt.Errorf("account password has changed since token issued")
|
||||
}
|
||||
|
||||
newToken := ""
|
||||
@@ -477,7 +477,7 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, string,
|
||||
// IDP signed token
|
||||
prov, err := mgr.provider()
|
||||
if err != nil {
|
||||
return claims, "", err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Token must be verified for at least one audience
|
||||
@@ -489,16 +489,30 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, string,
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// The token verification has failed. If the token has expired, we will
|
||||
// return a dummy claims only containing a value for the issuer, so the
|
||||
// UI can handle expired tokens appropriately.
|
||||
if err != nil {
|
||||
return claims, "", err
|
||||
if strings.HasPrefix(err.Error(), "oidc: token is expired") {
|
||||
claims = jwt.RegisteredClaims{
|
||||
Issuer: "sso",
|
||||
}
|
||||
return claims, "", err
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if idToken == nil {
|
||||
return claims, "", fmt.Errorf("No audience found in the token")
|
||||
return nil, "", fmt.Errorf("no audience found in the token")
|
||||
}
|
||||
|
||||
var claims jwt.MapClaims
|
||||
err = idToken.Claims(&claims)
|
||||
return claims, "", err
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return claims, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user