mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
Support for external OIDC providers and implicit login flows (#727)
This commit is contained in:
10
Gopkg.lock
generated
10
Gopkg.lock
generated
@@ -88,14 +88,6 @@
|
||||
revision = "d71629e497929858300c38cd442098c178121c30"
|
||||
version = "v1.5.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:65bad35bfcdd839cb26bb4ff31de49be39dd6bd2ade0c7c57d010f7d0412a4a5"
|
||||
name = "github.com/coreos/dex"
|
||||
packages = ["api"]
|
||||
pruneopts = ""
|
||||
revision = "218d671a96865df2a4cf7f310efb99b8bfc5a5e2"
|
||||
version = "v2.10.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
digest = "1:d8ee1b165eb7f4fd9ada718e1e7eeb0bc1fd462592d0bd823df694443f448681"
|
||||
@@ -1439,7 +1431,6 @@
|
||||
"github.com/argoproj/pkg/time",
|
||||
"github.com/casbin/casbin",
|
||||
"github.com/casbin/casbin/model",
|
||||
"github.com/coreos/dex/api",
|
||||
"github.com/coreos/go-oidc",
|
||||
"github.com/dgrijalva/jwt-go",
|
||||
"github.com/dustin/go-humanize",
|
||||
@@ -1453,7 +1444,6 @@
|
||||
"github.com/gogo/protobuf/proto",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"github.com/golang/glog",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/golang/protobuf/ptypes/empty",
|
||||
|
||||
4
Procfile
4
Procfile
@@ -1,4 +1,4 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go
|
||||
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth
|
||||
api-server: go run ./cmd/argocd-server/main.go --insecure --dex-server http://localhost:5556 --repo-server localhost:8081
|
||||
repo-server: go run ./cmd/argocd-repo-server/main.go --loglevel debug
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -p 5557:5557 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/coreos/dex:v2.10.0 serve /dex.yaml"
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.12.0 serve /dex.yaml"
|
||||
|
||||
@@ -2,10 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -48,14 +46,8 @@ func newCommand() *cobra.Command {
|
||||
Use: cliName,
|
||||
Short: "application-controller is a controller to operate on applications CRD",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
|
||||
// Set the glog level for the k8s go-client
|
||||
_ = flag.CommandLine.Parse([]string{})
|
||||
_ = flag.Lookup("logtostderr").Value.Set("true")
|
||||
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/ksonnet"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
@@ -35,9 +36,7 @@ func newCommand() *cobra.Command {
|
||||
Use: cliName,
|
||||
Short: "Run argocd-repo-server",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
cli.SetLogLevel(logLevel)
|
||||
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -2,11 +2,8 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@@ -20,6 +17,14 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultDexServerAddr is the HTTP address of the Dex OIDC server, which we run a reverse proxy against
|
||||
DefaultDexServerAddr = "http://dex-server:5556"
|
||||
|
||||
// DefaultRepoServerAddr is the gRPC address of the ArgoCD repo server
|
||||
DefaultRepoServerAddr = "argocd-repo-server:8081"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -29,6 +34,7 @@ func NewCommand() *cobra.Command {
|
||||
clientConfig clientcmd.ClientConfig
|
||||
staticAssetsDir string
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
@@ -37,14 +43,8 @@ func NewCommand() *cobra.Command {
|
||||
Short: "Run the argocd API server",
|
||||
Long: "Run the argocd API server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
|
||||
// Set the glog level for the k8s go-client
|
||||
_ = flag.CommandLine.Parse([]string{})
|
||||
_ = flag.Lookup("logtostderr").Value.Set("true")
|
||||
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
@@ -66,6 +66,7 @@ func NewCommand() *cobra.Command {
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
DexServerAddr: dexServerAddress,
|
||||
DisableAuth: disableAuth,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
}
|
||||
@@ -89,7 +90,8 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", "localhost:8081", "Repo server address.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", DefaultRepoServerAddr, "Repo server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", DefaultDexServerAddr, "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/account"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@@ -51,7 +51,7 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
}
|
||||
if newPassword == "" {
|
||||
var err error
|
||||
newPassword, err = settings.ReadAndConfirmPassword()
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,8 @@ package commands
|
||||
|
||||
const (
|
||||
cliName = "argocd"
|
||||
|
||||
// DefaultSSOLocalPort is the localhost port to listen on for the temporary web server performing
|
||||
// the OAuth2 login flow.
|
||||
DefaultSSOLocalPort = 8085
|
||||
)
|
||||
|
||||
@@ -2,15 +2,19 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/session"
|
||||
@@ -19,11 +23,8 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
oidcutil "github.com/argoproj/argo-cd/util/oidc"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
)
|
||||
|
||||
// NewLoginCommand returns a new instance of `argocd login` command
|
||||
@@ -33,6 +34,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
username string
|
||||
password string
|
||||
sso bool
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "login SERVER",
|
||||
@@ -81,12 +83,15 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
if !sso {
|
||||
tokenString = passwordLogin(acdClient, username, password)
|
||||
} else {
|
||||
acdSet, err := setIf.Get(context.Background(), &settings.SettingsQuery{})
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
if !ssoConfigured(acdSet) {
|
||||
log.Fatalf("ArgoCD instance is not configured with SSO")
|
||||
}
|
||||
tokenString, refreshToken = oauth2Login(server, clientOpts.PlainText)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
@@ -130,7 +135,8 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
command.Flags().StringVar(&ctxName, "name", "", "name to use for the context")
|
||||
command.Flags().StringVar(&username, "username", "", "the username of an account to authenticate")
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "Perform SSO login")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "perform SSO login")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -144,97 +150,107 @@ func userDisplayName(claims jwt.MapClaims) string {
|
||||
return claims["sub"].(string)
|
||||
}
|
||||
|
||||
func ssoConfigured(set *settings.Settings) bool {
|
||||
return set.DexConfig != nil && len(set.DexConfig.Connectors) > 0
|
||||
}
|
||||
|
||||
// getFreePort asks the kernel for a free open port that is ready to use.
|
||||
func getFreePort() (int, error) {
|
||||
ln, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ln.Addr().(*net.TCPAddr).Port, ln.Close()
|
||||
}
|
||||
|
||||
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
|
||||
// returns the JWT token and a refresh token (if supported)
|
||||
func oauth2Login(host string, plaintext bool) (string, string) {
|
||||
ctx := context.Background()
|
||||
port, err := getFreePort()
|
||||
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
var scheme = "https"
|
||||
if plaintext {
|
||||
scheme = "http"
|
||||
}
|
||||
conf := &oauth2.Config{
|
||||
ClientID: common.ArgoCDCLIClientAppID,
|
||||
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, host, common.DexAPIEndpoint),
|
||||
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, host, common.DexAPIEndpoint),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port),
|
||||
}
|
||||
srv := &http.Server{Addr: ":" + strconv.Itoa(port)}
|
||||
log.Debug("OIDC Configuration:")
|
||||
log.Debugf(" supported_scopes: %v", oidcConf.ScopesSupported)
|
||||
log.Debugf(" response_types_supported: %v", oidcConf.ResponseTypesSupported)
|
||||
|
||||
// handledRequests ensures we do not handle more requests than necessary
|
||||
handledRequests := 0
|
||||
// completionChan is to signal flow completed. Non-empty string indicates error
|
||||
completionChan := make(chan string)
|
||||
// stateNonce is an OAuth2 state nonce
|
||||
stateNonce := rand.RandString(10)
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
loginCompleted := make(chan struct{})
|
||||
|
||||
handleErr := func(w http.ResponseWriter, errMsg string) {
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
completionChan <- errMsg
|
||||
}
|
||||
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
// Handles both implicit and authorization code flow
|
||||
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
loginCompleted <- struct{}{}
|
||||
}()
|
||||
log.Debugf("Callback: %s", r.URL)
|
||||
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
if errMsg := r.FormValue("error"); errMsg != "" {
|
||||
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
|
||||
log.Fatal(errMsg)
|
||||
if formErr := r.FormValue("error"); formErr != "" {
|
||||
handleErr(w, formErr+": "+r.FormValue("error_description"))
|
||||
return
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
errMsg := fmt.Sprintf("no code in request: %q", r.Form)
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
log.Fatal(errMsg)
|
||||
return
|
||||
}
|
||||
tok, err := conf.Exchange(ctx, code)
|
||||
errors.CheckError(err)
|
||||
log.Info("Authentication successful")
|
||||
|
||||
var ok bool
|
||||
tokenString, ok = tok.Extra("id_token").(string)
|
||||
if !ok {
|
||||
errMsg := "no id_token in token response"
|
||||
http.Error(w, errMsg, http.StatusInternalServerError)
|
||||
log.Fatal(errMsg)
|
||||
handledRequests++
|
||||
if handledRequests > 2 {
|
||||
// Since implicit flow will redirect back to ourselves, this counter ensures we do not
|
||||
// fallinto a redirect loop (e.g. user visits the page by hand)
|
||||
handleErr(w, "Unable to complete login flow: too many redirects")
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", tokenString)
|
||||
|
||||
if len(r.Form) == 0 {
|
||||
// If we get here, no form data was set. We presume to be performing an implicit login
|
||||
// flow where the id_token is contained in a URL fragment, making it inaccessible to be
|
||||
// read from the request. This javascript will redirect the browser to send the
|
||||
// fragments as query parameters so our callback handler can read and return token.
|
||||
fmt.Fprintf(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
|
||||
return
|
||||
}
|
||||
|
||||
if state := r.FormValue("state"); state != stateNonce {
|
||||
handleErr(w, "Unknown state nonce")
|
||||
return
|
||||
}
|
||||
|
||||
tokenString = r.FormValue("id_token")
|
||||
if tokenString == "" {
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
|
||||
return
|
||||
}
|
||||
tok, err := oauth2conf.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
handleErr(w, err.Error())
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
tokenString, ok = tok.Extra("id_token").(string)
|
||||
if !ok {
|
||||
handleErr(w, "no id_token in token response")
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
}
|
||||
successPage := `
|
||||
<div style="height:100px; width:100%!; display:flex; flex-direction: column; justify-content: center; align-items:center; background-color:#2ecc71; color:white; font-size:22"><div>Authentication successful!</div></div>
|
||||
<p style="margin-top:20px; font-size:18; text-align:center">Authentication was successful, you can now return to CLI. This page will close automatically</p>
|
||||
<script>window.onload=function(){setTimeout(this.close, 4000)}</script>
|
||||
`
|
||||
fmt.Fprintf(w, successPage)
|
||||
completionChan <- ""
|
||||
}
|
||||
srv := &http.Server{Addr: ":" + strconv.Itoa(port)}
|
||||
http.HandleFunc("/auth/callback", callbackHandler)
|
||||
|
||||
// add transport for self-signed certificate to context
|
||||
sslcli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
|
||||
|
||||
// Redirect user to login & consent page to ask for permission for the scopes specified above.
|
||||
log.Info("Opening browser for authentication")
|
||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||
log.Infof("Authentication URL: %s", url)
|
||||
|
||||
var url string
|
||||
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
|
||||
default:
|
||||
log.Fatalf("Unsupported grant type: %v", grantType)
|
||||
}
|
||||
log.Infof("Performing %s flow login: %s", grantType, url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
errors.CheckError(err)
|
||||
@@ -243,8 +259,16 @@ func oauth2Login(host string, plaintext bool) (string, string) {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
<-loginCompleted
|
||||
errMsg := <-completionChan
|
||||
if errMsg != "" {
|
||||
log.Fatal(errMsg)
|
||||
}
|
||||
log.Info("Authentication successful")
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", refreshToken)
|
||||
return tokenString, refreshToken
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
@@ -18,6 +22,7 @@ import (
|
||||
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
password string
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "relogin",
|
||||
@@ -45,19 +50,29 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
if claims.Issuer == session.SessionManagerClaimsIssuer {
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
fmt.Printf("Relogging in as '%s'\n", claims.Subject)
|
||||
tokenString = passwordLogin(acdClient, claims.Subject, password)
|
||||
} else {
|
||||
fmt.Println("Reinitiating SSO login")
|
||||
tokenString, refreshToken = oauth2Login(configCtx.Server.Server, configCtx.Server.PlainText)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
@@ -71,5 +86,6 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
var logLevel string
|
||||
|
||||
func initConfig() {
|
||||
cli.SetLogLevel(logLevel)
|
||||
}
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -41,6 +53,6 @@ func NewCommand() *cobra.Command {
|
||||
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", false, "Skip server certificate and domain verification")
|
||||
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", "", "Server certificate file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", "", "Authentication token")
|
||||
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
name: static-files
|
||||
containers:
|
||||
- name: dex
|
||||
image: quay.io/dexidp/dex:v2.11.0
|
||||
image: quay.io/dexidp/dex:v2.12.0
|
||||
command: [/shared/argocd-util, rundex]
|
||||
ports:
|
||||
- containerPort: 5556
|
||||
|
||||
@@ -416,7 +416,7 @@ spec:
|
||||
- command:
|
||||
- /shared/argocd-util
|
||||
- rundex
|
||||
image: quay.io/dexidp/dex:v2.11.0
|
||||
image: quay.io/dexidp/dex:v2.12.0
|
||||
name: dex
|
||||
ports:
|
||||
- containerPort: 5556
|
||||
|
||||
@@ -356,7 +356,7 @@ spec:
|
||||
- command:
|
||||
- /shared/argocd-util
|
||||
- rundex
|
||||
image: quay.io/dexidp/dex:v2.11.0
|
||||
image: quay.io/dexidp/dex:v2.12.0
|
||||
name: dex
|
||||
ports:
|
||||
- containerPort: 5556
|
||||
|
||||
@@ -44,10 +44,16 @@ const (
|
||||
MaxGRPCMessageSize = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
clientScopes = []string{"openid", "profile", "email", "groups", "offline_access"}
|
||||
)
|
||||
|
||||
// Client defines an interface for interaction with an Argo CD server.
|
||||
type Client interface {
|
||||
ClientOptions() ClientOptions
|
||||
NewConn() (*grpc.ClientConn, error)
|
||||
HTTPClient() (*http.Client, error)
|
||||
OIDCConfig(context.Context, *settings.Settings) (*oauth2.Config, *oidc.Provider, error)
|
||||
NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error)
|
||||
NewRepoClientOrDie() (*grpc.ClientConn, repository.RepositoryServiceClient)
|
||||
NewClusterClient() (*grpc.ClientConn, cluster.ClusterServiceClient, error)
|
||||
@@ -160,16 +166,62 @@ func NewClient(opts *ClientOptions) (Client, error) {
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// OIDCConfig returns OAuth2 client config and a OpenID Provider based on ArgoCD settings
|
||||
// ctx can hold an appropriate http.Client to use for the exchange
|
||||
func (c *client) OIDCConfig(ctx context.Context, set *settings.Settings) (*oauth2.Config, *oidc.Provider, error) {
|
||||
var clientID string
|
||||
var issuerURL string
|
||||
if set.DexConfig != nil && len(set.DexConfig.Connectors) > 0 {
|
||||
clientID = common.ArgoCDCLIClientAppID
|
||||
issuerURL = fmt.Sprintf("%s%s", set.URL, common.DexAPIEndpoint)
|
||||
} else if set.OIDCConfig != nil && set.OIDCConfig.Issuer != "" {
|
||||
clientID = set.OIDCConfig.ClientID
|
||||
issuerURL = set.OIDCConfig.Issuer
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("%s is not configured with SSO", c.ServerAddr)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
|
||||
}
|
||||
oauth2conf := oauth2.Config{
|
||||
ClientID: clientID,
|
||||
Scopes: clientScopes,
|
||||
Endpoint: provider.Endpoint(),
|
||||
}
|
||||
return &oauth2conf, provider, nil
|
||||
}
|
||||
|
||||
// HTTPClient returns a HTTPClient appropriate for performing OAuth, based on TLS settings
|
||||
func (c *client) HTTPClient() (*http.Client, error) {
|
||||
tlsConfig, err := c.tlsConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refreshAuthToken refreshes a JWT auth token if it is invalid (e.g. expired)
|
||||
func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, configPath string) error {
|
||||
configCtx, err := localCfg.ResolveContext(ctxName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.RefreshToken == "" {
|
||||
// If we have no refresh token, there's no point in doing anything
|
||||
return nil
|
||||
}
|
||||
configCtx, err := localCfg.ResolveContext(ctxName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
@@ -184,50 +236,10 @@ func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, co
|
||||
}
|
||||
|
||||
log.Debug("Auth token no longer valid. Refreshing")
|
||||
tlsConfig, err := c.tlsConfig()
|
||||
rawIDToken, refreshToken, err := c.redeemRefreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
ctx := oidc.ClientContext(context.Background(), httpClient)
|
||||
var scheme string
|
||||
if c.PlainText {
|
||||
scheme = "http"
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
conf := &oauth2.Config{
|
||||
ClientID: common.ArgoCDCLIClientAppID,
|
||||
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, c.ServerAddr, common.DexAPIEndpoint),
|
||||
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, c.ServerAddr, common.DexAPIEndpoint),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s://%s/auth/callback", scheme, c.ServerAddr),
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
RefreshToken: c.RefreshToken,
|
||||
}
|
||||
token, err := conf.TokenSource(ctx, t).Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return errors.New("no id_token in token response")
|
||||
}
|
||||
refreshToken, _ := token.Extra("refresh_token").(string)
|
||||
c.AuthToken = rawIDToken
|
||||
c.RefreshToken = refreshToken
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
@@ -242,6 +254,41 @@ func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, co
|
||||
return nil
|
||||
}
|
||||
|
||||
// redeemRefreshToken performs the exchange of a refresh_token for a new id_token and refresh_token
|
||||
func (c *client) redeemRefreshToken() (string, string, error) {
|
||||
setConn, setIf, err := c.NewSettingsClient()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer func() { _ = setConn.Close() }()
|
||||
httpClient, err := c.HTTPClient()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
ctx := oidc.ClientContext(context.Background(), httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
oauth2conf, _, err := c.OIDCConfig(ctx, acdSet)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
RefreshToken: c.RefreshToken,
|
||||
}
|
||||
token, err := oauth2conf.TokenSource(ctx, t).Token()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return "", "", errors.New("no id_token in token response")
|
||||
}
|
||||
refreshToken, _ := token.Extra("refresh_token").(string)
|
||||
return rawIDToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// NewClientOrDie creates a new API client from a set of config options, or fails fatally if the new client creation fails.
|
||||
func NewClientOrDie(opts *ClientOptions) Client {
|
||||
client, err := NewClient(opts)
|
||||
|
||||
@@ -58,9 +58,11 @@ import (
|
||||
dexutil "github.com/argoproj/argo-cd/util/dex"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/healthz"
|
||||
httputil "github.com/argoproj/argo-cd/util/http"
|
||||
jsonutil "github.com/argoproj/argo-cd/util/json"
|
||||
jwtutil "github.com/argoproj/argo-cd/util/jwt"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/oidc"
|
||||
projectutil "github.com/argoproj/argo-cd/util/project"
|
||||
"github.com/argoproj/argo-cd/util/rbac"
|
||||
util_session "github.com/argoproj/argo-cd/util/session"
|
||||
@@ -104,7 +106,7 @@ func init() {
|
||||
type ArgoCDServer struct {
|
||||
ArgoCDServerOpts
|
||||
|
||||
ssoClientApp *dexutil.ClientApp
|
||||
ssoClientApp *oidc.ClientApp
|
||||
settings *settings_util.ArgoCDSettings
|
||||
log *log.Entry
|
||||
sessionMgr *util_session.SessionManager
|
||||
@@ -121,6 +123,7 @@ type ArgoCDServerOpts struct {
|
||||
DisableAuth bool
|
||||
Insecure bool
|
||||
Namespace string
|
||||
DexServerAddr string
|
||||
StaticAssetsDir string
|
||||
KubeClientset kubernetes.Interface
|
||||
AppClientset appclientset.Interface
|
||||
@@ -307,6 +310,8 @@ func (a *ArgoCDServer) watchSettings(ctx context.Context) {
|
||||
updateCh := make(chan struct{}, 1)
|
||||
a.settingsMgr.Subscribe(updateCh)
|
||||
|
||||
prevURL := a.settings.URL
|
||||
prevOIDCConfig := a.settings.OIDCConfigRAW
|
||||
prevDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
|
||||
errors.CheckError(err)
|
||||
prevGitHubSecret := a.settings.WebhookGitHubSecret
|
||||
@@ -325,6 +330,14 @@ func (a *ArgoCDServer) watchSettings(ctx context.Context) {
|
||||
log.Infof("dex config modified. restarting")
|
||||
break
|
||||
}
|
||||
if prevOIDCConfig != a.settings.OIDCConfigRAW {
|
||||
log.Infof("odic config modified. restarting")
|
||||
break
|
||||
}
|
||||
if prevURL != a.settings.URL {
|
||||
log.Infof("url modified. restarting")
|
||||
break
|
||||
}
|
||||
if prevGitHubSecret != a.settings.WebhookGitHubSecret {
|
||||
log.Infof("github secret modified. restarting")
|
||||
break
|
||||
@@ -431,7 +444,7 @@ func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.Res
|
||||
if !a.Insecure {
|
||||
flags = append(flags, "Secure")
|
||||
}
|
||||
cookie := util_session.MakeCookieMetadata(common.AuthCookieName, sessionResp.Token, flags...)
|
||||
cookie := httputil.MakeCookieMetadata(common.AuthCookieName, sessionResp.Token, flags...)
|
||||
w.Header().Set("Set-Cookie", cookie)
|
||||
}
|
||||
return nil
|
||||
@@ -524,10 +537,10 @@ func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) {
|
||||
}
|
||||
// Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex)
|
||||
var err error
|
||||
mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy())
|
||||
mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr))
|
||||
tlsConfig := a.settings.TLSConfig()
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
a.ssoClientApp, err = dexutil.NewClientApp(a.settings, a.sessionMgr)
|
||||
a.ssoClientApp, err = oidc.NewClientApp(a.settings)
|
||||
errors.CheckError(err)
|
||||
mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin)
|
||||
mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback)
|
||||
|
||||
@@ -27,10 +27,19 @@ func (s *Server) Get(ctx context.Context, q *SettingsQuery) (*Settings, error) {
|
||||
set := Settings{
|
||||
URL: argoCDSettings.URL,
|
||||
}
|
||||
var cfg DexConfig
|
||||
err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
|
||||
if err == nil {
|
||||
set.DexConfig = &cfg
|
||||
if argoCDSettings.DexConfig != "" {
|
||||
var cfg DexConfig
|
||||
err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
|
||||
if err == nil {
|
||||
set.DexConfig = &cfg
|
||||
}
|
||||
}
|
||||
if oidcConfig := argoCDSettings.OIDCConfig(); oidcConfig != nil {
|
||||
set.OIDCConfig = &OIDCConfig{
|
||||
Name: oidcConfig.Name,
|
||||
Issuer: oidcConfig.Issuer,
|
||||
ClientID: oidcConfig.ClientID,
|
||||
}
|
||||
}
|
||||
return &set, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (m *SettingsQuery) Reset() { *m = SettingsQuery{} }
|
||||
func (m *SettingsQuery) String() string { return proto.CompactTextString(m) }
|
||||
func (*SettingsQuery) ProtoMessage() {}
|
||||
func (*SettingsQuery) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_settings_71506a99e4ff7448, []int{0}
|
||||
return fileDescriptor_settings_843b05f20a608370, []int{0}
|
||||
}
|
||||
func (m *SettingsQuery) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -72,18 +72,19 @@ func (m *SettingsQuery) XXX_DiscardUnknown() {
|
||||
var xxx_messageInfo_SettingsQuery proto.InternalMessageInfo
|
||||
|
||||
type Settings struct {
|
||||
URL string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
DexConfig *DexConfig `protobuf:"bytes,2,opt,name=dexConfig" json:"dexConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
URL string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||
DexConfig *DexConfig `protobuf:"bytes,2,opt,name=dexConfig" json:"dexConfig,omitempty"`
|
||||
OIDCConfig *OIDCConfig `protobuf:"bytes,3,opt,name=oidcConfig" json:"oidcConfig,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Settings) Reset() { *m = Settings{} }
|
||||
func (m *Settings) String() string { return proto.CompactTextString(m) }
|
||||
func (*Settings) ProtoMessage() {}
|
||||
func (*Settings) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_settings_71506a99e4ff7448, []int{1}
|
||||
return fileDescriptor_settings_843b05f20a608370, []int{1}
|
||||
}
|
||||
func (m *Settings) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -126,6 +127,13 @@ func (m *Settings) GetDexConfig() *DexConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Settings) GetOIDCConfig() *OIDCConfig {
|
||||
if m != nil {
|
||||
return m.OIDCConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DexConfig struct {
|
||||
Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors" json:"connectors,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
@@ -137,7 +145,7 @@ func (m *DexConfig) Reset() { *m = DexConfig{} }
|
||||
func (m *DexConfig) String() string { return proto.CompactTextString(m) }
|
||||
func (*DexConfig) ProtoMessage() {}
|
||||
func (*DexConfig) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_settings_71506a99e4ff7448, []int{2}
|
||||
return fileDescriptor_settings_843b05f20a608370, []int{2}
|
||||
}
|
||||
func (m *DexConfig) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -185,7 +193,7 @@ func (m *Connector) Reset() { *m = Connector{} }
|
||||
func (m *Connector) String() string { return proto.CompactTextString(m) }
|
||||
func (*Connector) ProtoMessage() {}
|
||||
func (*Connector) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_settings_71506a99e4ff7448, []int{3}
|
||||
return fileDescriptor_settings_843b05f20a608370, []int{3}
|
||||
}
|
||||
func (m *Connector) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -228,11 +236,75 @@ func (m *Connector) GetType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"`
|
||||
ClientID string `protobuf:"bytes,3,opt,name=clientID,proto3" json:"clientID,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) Reset() { *m = OIDCConfig{} }
|
||||
func (m *OIDCConfig) String() string { return proto.CompactTextString(m) }
|
||||
func (*OIDCConfig) ProtoMessage() {}
|
||||
func (*OIDCConfig) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_settings_843b05f20a608370, []int{4}
|
||||
}
|
||||
func (m *OIDCConfig) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *OIDCConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_OIDCConfig.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalTo(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (dst *OIDCConfig) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_OIDCConfig.Merge(dst, src)
|
||||
}
|
||||
func (m *OIDCConfig) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *OIDCConfig) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_OIDCConfig.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_OIDCConfig proto.InternalMessageInfo
|
||||
|
||||
func (m *OIDCConfig) GetName() string {
|
||||
if m != nil {
|
||||
return m.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) GetIssuer() string {
|
||||
if m != nil {
|
||||
return m.Issuer
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) GetClientID() string {
|
||||
if m != nil {
|
||||
return m.ClientID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*SettingsQuery)(nil), "cluster.SettingsQuery")
|
||||
proto.RegisterType((*Settings)(nil), "cluster.Settings")
|
||||
proto.RegisterType((*DexConfig)(nil), "cluster.DexConfig")
|
||||
proto.RegisterType((*Connector)(nil), "cluster.Connector")
|
||||
proto.RegisterType((*OIDCConfig)(nil), "cluster.OIDCConfig")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
@@ -361,6 +433,16 @@ func (m *Settings) MarshalTo(dAtA []byte) (int, error) {
|
||||
}
|
||||
i += n1
|
||||
}
|
||||
if m.OIDCConfig != nil {
|
||||
dAtA[i] = 0x1a
|
||||
i++
|
||||
i = encodeVarintSettings(dAtA, i, uint64(m.OIDCConfig.Size()))
|
||||
n2, err := m.OIDCConfig.MarshalTo(dAtA[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i += n2
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
@@ -433,6 +515,45 @@ func (m *Connector) MarshalTo(dAtA []byte) (int, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) MarshalTo(dAtA []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Name) > 0 {
|
||||
dAtA[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintSettings(dAtA, i, uint64(len(m.Name)))
|
||||
i += copy(dAtA[i:], m.Name)
|
||||
}
|
||||
if len(m.Issuer) > 0 {
|
||||
dAtA[i] = 0x12
|
||||
i++
|
||||
i = encodeVarintSettings(dAtA, i, uint64(len(m.Issuer)))
|
||||
i += copy(dAtA[i:], m.Issuer)
|
||||
}
|
||||
if len(m.ClientID) > 0 {
|
||||
dAtA[i] = 0x1a
|
||||
i++
|
||||
i = encodeVarintSettings(dAtA, i, uint64(len(m.ClientID)))
|
||||
i += copy(dAtA[i:], m.ClientID)
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func encodeVarintSettings(dAtA []byte, offset int, v uint64) int {
|
||||
for v >= 1<<7 {
|
||||
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||
@@ -462,6 +583,10 @@ func (m *Settings) Size() (n int) {
|
||||
l = m.DexConfig.Size()
|
||||
n += 1 + l + sovSettings(uint64(l))
|
||||
}
|
||||
if m.OIDCConfig != nil {
|
||||
l = m.OIDCConfig.Size()
|
||||
n += 1 + l + sovSettings(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
@@ -500,6 +625,27 @@ func (m *Connector) Size() (n int) {
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *OIDCConfig) Size() (n int) {
|
||||
var l int
|
||||
_ = l
|
||||
l = len(m.Name)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovSettings(uint64(l))
|
||||
}
|
||||
l = len(m.Issuer)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovSettings(uint64(l))
|
||||
}
|
||||
l = len(m.ClientID)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovSettings(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func sovSettings(x uint64) (n int) {
|
||||
for {
|
||||
n++
|
||||
@@ -655,6 +801,39 @@ func (m *Settings) Unmarshal(dAtA []byte) error {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field OIDCConfig", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowSettings
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthSettings
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.OIDCConfig == nil {
|
||||
m.OIDCConfig = &OIDCConfig{}
|
||||
}
|
||||
if err := m.OIDCConfig.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipSettings(dAtA[iNdEx:])
|
||||
@@ -868,6 +1047,144 @@ func (m *Connector) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *OIDCConfig) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowSettings
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: OIDCConfig: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: OIDCConfig: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowSettings
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthSettings
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Name = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Issuer", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowSettings
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthSettings
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Issuer = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field ClientID", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowSettings
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthSettings
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.ClientID = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipSettings(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skippy < 0 {
|
||||
return ErrInvalidLengthSettings
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipSettings(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
@@ -974,30 +1291,34 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("server/settings/settings.proto", fileDescriptor_settings_71506a99e4ff7448)
|
||||
proto.RegisterFile("server/settings/settings.proto", fileDescriptor_settings_843b05f20a608370)
|
||||
}
|
||||
|
||||
var fileDescriptor_settings_71506a99e4ff7448 = []byte{
|
||||
// 322 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0x41, 0x4b, 0xc3, 0x40,
|
||||
0x10, 0x85, 0xd9, 0x46, 0xac, 0x19, 0x91, 0xea, 0x22, 0x12, 0x8b, 0xc4, 0x92, 0x53, 0x41, 0x4c,
|
||||
0xb4, 0x3d, 0x79, 0x12, 0x5a, 0x41, 0x10, 0x2f, 0xa6, 0x88, 0x20, 0x78, 0x48, 0xd3, 0x71, 0x8d,
|
||||
0xb4, 0x3b, 0x65, 0xb3, 0x29, 0xf6, 0xea, 0x5f, 0xf0, 0x4f, 0x79, 0x14, 0xbc, 0x8b, 0x04, 0x7f,
|
||||
0x88, 0x74, 0xdb, 0x44, 0xab, 0xb7, 0xc7, 0xf7, 0x66, 0x92, 0xb7, 0xf3, 0xc0, 0x4d, 0x51, 0x4d,
|
||||
0x50, 0x05, 0x29, 0x6a, 0x9d, 0x48, 0x91, 0x96, 0xc2, 0x1f, 0x2b, 0xd2, 0xc4, 0xab, 0xf1, 0x30,
|
||||
0x4b, 0x35, 0xaa, 0xfa, 0xb6, 0x20, 0x41, 0x86, 0x05, 0x33, 0x35, 0xb7, 0xeb, 0x7b, 0x82, 0x48,
|
||||
0x0c, 0x31, 0x88, 0xc6, 0x49, 0x10, 0x49, 0x49, 0x3a, 0xd2, 0x09, 0xc9, 0xc5, 0xb2, 0x57, 0x83,
|
||||
0x8d, 0xde, 0xe2, 0x73, 0x57, 0x19, 0xaa, 0xa9, 0x77, 0x03, 0x6b, 0x05, 0xe0, 0xbb, 0x60, 0x65,
|
||||
0x6a, 0xe8, 0xb0, 0x06, 0x6b, 0xda, 0x9d, 0x6a, 0xfe, 0xb1, 0x6f, 0x5d, 0x87, 0x97, 0xe1, 0x8c,
|
||||
0xf1, 0x23, 0xb0, 0x07, 0xf8, 0xd4, 0x25, 0x79, 0x9f, 0x08, 0xa7, 0xd2, 0x60, 0xcd, 0xf5, 0x16,
|
||||
0xf7, 0x17, 0x41, 0xfc, 0xb3, 0xc2, 0x09, 0x7f, 0x86, 0xbc, 0x53, 0xb0, 0x4b, 0xce, 0x5b, 0x00,
|
||||
0x31, 0x49, 0x89, 0xb1, 0x26, 0x95, 0x3a, 0xac, 0x61, 0x2d, 0xed, 0x77, 0x0b, 0x2b, 0xfc, 0x35,
|
||||
0xe5, 0xb5, 0xc1, 0x2e, 0x0d, 0xce, 0x61, 0x45, 0x46, 0x23, 0x9c, 0x67, 0x0b, 0x8d, 0x9e, 0x31,
|
||||
0x3d, 0x1d, 0xa3, 0x89, 0x63, 0x87, 0x46, 0xb7, 0xee, 0xa0, 0x56, 0x3c, 0xa7, 0x87, 0x6a, 0x92,
|
||||
0xc4, 0xc8, 0x2f, 0xc0, 0x3a, 0x47, 0xcd, 0x77, 0xca, 0xdf, 0x2d, 0x1d, 0xa0, 0xbe, 0xf5, 0x8f,
|
||||
0x7b, 0xce, 0xf3, 0xfb, 0xd7, 0x4b, 0x85, 0xf3, 0x4d, 0x73, 0xc4, 0xc9, 0x71, 0xd9, 0x40, 0xe7,
|
||||
0xe4, 0x35, 0x77, 0xd9, 0x5b, 0xee, 0xb2, 0xcf, 0xdc, 0x65, 0xb7, 0x07, 0x22, 0xd1, 0x0f, 0x59,
|
||||
0xdf, 0x8f, 0x69, 0x14, 0x44, 0xca, 0x74, 0xf1, 0x68, 0xc4, 0x61, 0x3c, 0x08, 0xfe, 0xb4, 0xd8,
|
||||
0x5f, 0x35, 0x05, 0xb4, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0xef, 0x0e, 0xd5, 0xb9, 0xdf, 0x01,
|
||||
0x00, 0x00,
|
||||
var fileDescriptor_settings_843b05f20a608370 = []byte{
|
||||
// 397 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xcd, 0x8a, 0xdb, 0x30,
|
||||
0x18, 0x44, 0x71, 0x49, 0xe2, 0xaf, 0x3f, 0x69, 0xd5, 0x12, 0xdc, 0x50, 0x9c, 0xe0, 0x53, 0xa0,
|
||||
0xd4, 0x6e, 0x93, 0x53, 0x4f, 0x05, 0x3b, 0x50, 0x52, 0x0a, 0xa5, 0x0a, 0xbd, 0x14, 0x7a, 0x70,
|
||||
0x14, 0xd5, 0x55, 0x71, 0xa4, 0x20, 0xcb, 0xa1, 0xb9, 0xf6, 0x15, 0xf6, 0xba, 0x0f, 0xb4, 0xc7,
|
||||
0x85, 0xbd, 0x87, 0xc5, 0xec, 0x83, 0x2c, 0x51, 0x6c, 0x27, 0xd9, 0xdd, 0xdb, 0x68, 0x46, 0x23,
|
||||
0xbe, 0xd1, 0x37, 0xe0, 0x66, 0x4c, 0xad, 0x99, 0x0a, 0x32, 0xa6, 0x35, 0x17, 0x49, 0x56, 0x03,
|
||||
0x7f, 0xa5, 0xa4, 0x96, 0xb8, 0x45, 0xd3, 0x3c, 0xd3, 0x4c, 0xf5, 0x5e, 0x25, 0x32, 0x91, 0x86,
|
||||
0x0b, 0x76, 0x68, 0x2f, 0xf7, 0xde, 0x24, 0x52, 0x26, 0x29, 0x0b, 0xe2, 0x15, 0x0f, 0x62, 0x21,
|
||||
0xa4, 0x8e, 0x35, 0x97, 0xa2, 0x34, 0x7b, 0x1d, 0x78, 0x3a, 0x2b, 0x9f, 0xfb, 0x9e, 0x33, 0xb5,
|
||||
0xf1, 0xce, 0x11, 0xb4, 0x2b, 0x06, 0xbf, 0x06, 0x2b, 0x57, 0xa9, 0x83, 0x06, 0x68, 0x68, 0x87,
|
||||
0xad, 0x62, 0xdb, 0xb7, 0x7e, 0x90, 0xaf, 0x64, 0xc7, 0xe1, 0xf7, 0x60, 0x2f, 0xd8, 0xbf, 0x48,
|
||||
0x8a, 0xdf, 0x3c, 0x71, 0x1a, 0x03, 0x34, 0x7c, 0x3c, 0xc2, 0x7e, 0x39, 0x89, 0x3f, 0xa9, 0x14,
|
||||
0x72, 0xb8, 0x84, 0x23, 0x00, 0xc9, 0x17, 0xb4, 0xb4, 0x58, 0xc6, 0xf2, 0xb2, 0xb6, 0x7c, 0x9b,
|
||||
0x4e, 0xa2, 0xbd, 0x14, 0x3e, 0x2b, 0xb6, 0x7d, 0x38, 0x9c, 0xc9, 0x91, 0xcd, 0xfb, 0x04, 0x76,
|
||||
0xfd, 0x38, 0x1e, 0x01, 0x50, 0x29, 0x04, 0xa3, 0x5a, 0xaa, 0xcc, 0x41, 0x03, 0xeb, 0x64, 0x88,
|
||||
0xa8, 0x92, 0xc8, 0xd1, 0x2d, 0x6f, 0x0c, 0x76, 0x2d, 0x60, 0x0c, 0x8f, 0x44, 0xbc, 0x64, 0xfb,
|
||||
0x80, 0xc4, 0xe0, 0x1d, 0xa7, 0x37, 0x2b, 0x66, 0x32, 0xd9, 0xc4, 0x60, 0x6f, 0x0e, 0x47, 0xf3,
|
||||
0x3c, 0xe8, 0xea, 0x42, 0x93, 0x67, 0x59, 0xce, 0x54, 0xe9, 0x2b, 0x4f, 0x78, 0x08, 0x6d, 0x9a,
|
||||
0x72, 0x26, 0xf4, 0x74, 0x62, 0x22, 0xdb, 0xe1, 0x93, 0x62, 0xdb, 0x6f, 0x47, 0x25, 0x47, 0x6a,
|
||||
0x75, 0xf4, 0x0b, 0x3a, 0xd5, 0xbf, 0xcf, 0x98, 0x5a, 0x73, 0xca, 0xf0, 0x17, 0xb0, 0x3e, 0x33,
|
||||
0x8d, 0xbb, 0x75, 0xa4, 0x93, 0x55, 0xf5, 0x5e, 0xdc, 0xe3, 0x3d, 0xe7, 0xff, 0xd5, 0xcd, 0x59,
|
||||
0x03, 0xe3, 0xe7, 0x66, 0xdd, 0xeb, 0x0f, 0x75, 0x57, 0xc2, 0x8f, 0x17, 0x85, 0x8b, 0x2e, 0x0b,
|
||||
0x17, 0x5d, 0x17, 0x2e, 0xfa, 0xf9, 0x36, 0xe1, 0xfa, 0x4f, 0x3e, 0xf7, 0xa9, 0x5c, 0x06, 0xb1,
|
||||
0x32, 0xad, 0xf9, 0x6b, 0xc0, 0x3b, 0xba, 0x08, 0xee, 0xf4, 0x6d, 0xde, 0x34, 0x55, 0x19, 0xdf,
|
||||
0x06, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xa2, 0x52, 0xc6, 0x89, 0x02, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ message SettingsQuery {
|
||||
message Settings {
|
||||
string url = 1 [(gogoproto.customname) = "URL"];
|
||||
DexConfig dexConfig = 2;
|
||||
OIDCConfig oidcConfig = 3 [(gogoproto.customname) = "OIDCConfig"];
|
||||
}
|
||||
|
||||
message DexConfig {
|
||||
@@ -27,6 +28,12 @@ message Connector {
|
||||
string type = 2;
|
||||
}
|
||||
|
||||
message OIDCConfig {
|
||||
string name = 1;
|
||||
string issuer = 2;
|
||||
string clientID = 3 [(gogoproto.customname) = "ClientID"];
|
||||
}
|
||||
|
||||
// SettingsService
|
||||
service SettingsService {
|
||||
|
||||
|
||||
@@ -1349,12 +1349,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterOIDCConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clientID": {
|
||||
"type": "string"
|
||||
},
|
||||
"issuer": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dexConfig": {
|
||||
"$ref": "#/definitions/clusterDexConfig"
|
||||
},
|
||||
"oidcConfig": {
|
||||
"$ref": "#/definitions/clusterOIDCConfig"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -4,16 +4,20 @@ package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
@@ -105,3 +109,39 @@ func AskToProceed(message string) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAndConfirmPassword is a helper to read and confirm a password from stdin
|
||||
func ReadAndConfirmPassword() (string, error) {
|
||||
for {
|
||||
fmt.Print("*** Enter new password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Print("*** Confirm new password: ")
|
||||
confirmPassword, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("\n")
|
||||
if string(password) == string(confirmPassword) {
|
||||
return string(password), nil
|
||||
}
|
||||
log.Error("Passwords do not match")
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogLevel parses and sets a logrus log level
|
||||
func SetLogLevel(logLevel string) {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
}
|
||||
|
||||
// SetGLogLevel set the glog level for the k8s go-client
|
||||
func SetGLogLevel(glogLevel int) {
|
||||
_ = flag.CommandLine.Parse([]string{})
|
||||
_ = flag.Lookup("logtostderr").Value.Set("true")
|
||||
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func GenerateDexConfigYAML(settings *settings.ArgoCDSettings) ([]byte, error) {
|
||||
if !settings.IsSSOConfigured() {
|
||||
if !settings.IsDexConfigured() {
|
||||
return nil, nil
|
||||
}
|
||||
var dexCfg map[string]interface{}
|
||||
@@ -36,7 +36,7 @@ func GenerateDexConfigYAML(settings *settings.ArgoCDSettings) ([]byte, error) {
|
||||
{
|
||||
"id": common.ArgoCDClientAppID,
|
||||
"name": common.ArgoCDClientAppName,
|
||||
"secret": settings.OAuth2ClientSecret(),
|
||||
"secret": settings.DexOAuth2ClientSecret(),
|
||||
"redirectURIs": []string{
|
||||
settings.RedirectURL(),
|
||||
},
|
||||
@@ -118,10 +118,10 @@ func replaceStringSecret(val string, secretValues map[string]string) string {
|
||||
|
||||
// needsRedirectURI returns whether or not the given connector type needs a redirectURI
|
||||
// Update this list as necessary, as new connectors are added
|
||||
// https://github.com/coreos/dex/tree/master/Documentation/connectors
|
||||
// https://github.com/dexidp/dex/tree/master/Documentation/connectors
|
||||
func needsRedirectURI(connectorType string) bool {
|
||||
switch connectorType {
|
||||
case "oidc", "saml", "microsoft", "linkedin", "gitlab", "github":
|
||||
case "oidc", "saml", "microsoft", "linkedin", "gitlab", "github", "bitbucket-cloud":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
306
util/dex/dex.go
306
util/dex/dex.go
@@ -1,56 +1,26 @@
|
||||
package dex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/dex/api"
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DexReverseProxyAddr is the address of the Dex OIDC server, which we run a reverse proxy against
|
||||
DexReverseProxyAddr = "http://dex-server:5556"
|
||||
// DexgRPCAPIAddr is the address to the Dex gRPC API server for managing dex. This is assumed to run
|
||||
// locally (as a sidecar)
|
||||
DexgRPCAPIAddr = "dex-server:5557"
|
||||
)
|
||||
|
||||
var messageRe = regexp.MustCompile(`<p>(.*)([\s\S]*?)<\/p>`)
|
||||
|
||||
type DexAPIClient struct {
|
||||
api.DexClient
|
||||
}
|
||||
|
||||
// NewDexHTTPReverseProxy returns a reverse proxy to the DEX server. Dex is assumed to be configured
|
||||
// NewDexHTTPReverseProxy returns a reverse proxy to the Dex server. Dex is assumed to be configured
|
||||
// with the external issuer URL muxed to the same path configured in server.go. In other words, if
|
||||
// ArgoCD API server wants to proxy requests at /api/dex, then the dex config yaml issuer URL should
|
||||
// also be /api/dex (e.g. issuer: https://argocd.example.com/api/dex)
|
||||
func NewDexHTTPReverseProxy() func(writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(DexReverseProxyAddr)
|
||||
func NewDexHTTPReverseProxy(serverAddr string) func(writer http.ResponseWriter, request *http.Request) {
|
||||
target, err := url.Parse(serverAddr)
|
||||
errors.CheckError(err)
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
@@ -82,273 +52,3 @@ func NewDexHTTPReverseProxy() func(writer http.ResponseWriter, request *http.Req
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDexClient() (*DexAPIClient, error) {
|
||||
conn, err := grpc.Dial(DexgRPCAPIAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %s: %v", DexgRPCAPIAddr, err)
|
||||
}
|
||||
apiClient := DexAPIClient{
|
||||
api.NewDexClient(conn),
|
||||
}
|
||||
return &apiClient, nil
|
||||
}
|
||||
|
||||
// WaitUntilReady waits until the dex gRPC server is responding
|
||||
func (d *DexAPIClient) WaitUntilReady() {
|
||||
log.Info("Waiting for dex to become ready")
|
||||
ctx := context.Background()
|
||||
for {
|
||||
vers, err := d.GetVersion(ctx, &api.VersionReq{})
|
||||
if err == nil {
|
||||
log.Infof("Dex %s (API: %d) up and running", vers.Server, vers.Api)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
type ClientApp struct {
|
||||
// OAuth2 client ID of this application (e.g. argo-cd)
|
||||
clientID string
|
||||
// OAuth2 client secret of this application
|
||||
clientSecret string
|
||||
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
|
||||
redirectURI string
|
||||
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
|
||||
issuerURL string
|
||||
// client is the HTTP client which is used to query the IDp
|
||||
client *http.Client
|
||||
// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
|
||||
// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
|
||||
secureCookie bool
|
||||
// settings holds ArgoCD settings
|
||||
settings *settings.ArgoCDSettings
|
||||
// sessionMgr holds an ArgoCD session manager
|
||||
sessionMgr *session.SessionManager
|
||||
// states holds temporary nonce tokens to which hold application state values
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
states cache.Cache
|
||||
}
|
||||
|
||||
type appState struct {
|
||||
// ReturnURL is the URL in which to redirect a user back to after completing an OAuth2 login
|
||||
ReturnURL string `json:"returnURL"`
|
||||
}
|
||||
|
||||
// NewClientApp will register the ArgoCD client app in Dex and return an object which has HTTP
|
||||
// handlers for handling the HTTP responses for login and callback
|
||||
func NewClientApp(settings *settings.ArgoCDSettings, sessionMgr *session.SessionManager) (*ClientApp, error) {
|
||||
log.Infof("Creating client app (%s)", common.ArgoCDClientAppID)
|
||||
a := ClientApp{
|
||||
clientID: common.ArgoCDClientAppID,
|
||||
clientSecret: settings.OAuth2ClientSecret(),
|
||||
redirectURI: settings.RedirectURL(),
|
||||
issuerURL: settings.IssuerURL(),
|
||||
}
|
||||
u, err := url.Parse(settings.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse redirect-uri: %v", err)
|
||||
}
|
||||
tlsConfig := settings.TLSConfig()
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
a.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
// NOTE: if we ever have replicas of ArgoCD, this needs to switch to Redis cache
|
||||
a.states = cache.NewInMemoryCache(3 * time.Minute)
|
||||
a.secureCookie = bool(u.Scheme == "https")
|
||||
a.settings = settings
|
||||
a.sessionMgr = sessionMgr
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func (a *ClientApp) oauth2Config(scopes []string) (*oauth2.Config, error) {
|
||||
provider, err := a.sessionMgr.OIDCProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oauth2.Config{
|
||||
ClientID: a.clientID,
|
||||
ClientSecret: a.clientSecret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: a.redirectURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RandString generates, from a given charset, a cryptographically-secure pseudo-random string of a given length.
|
||||
// If the random number reader is unable to gather enough entropy to generate a secure random number, an error will be returned.
|
||||
func randString(n int, charset string) (string, error) {
|
||||
var b strings.Builder
|
||||
rr := []rune(charset)
|
||||
m := big.NewInt(int64(len(rr)))
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
pos, err := rand.Int(rand.Reader, m)
|
||||
if err != nil {
|
||||
return b.String(), err
|
||||
}
|
||||
_, _ = b.WriteRune(rr[pos.Int64()])
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// generateAppState creates an app state nonce
|
||||
func (a *ClientApp) generateAppState(returnURL string) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
randStr, err := randString(10, letters)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not generate entropy: %v", err)
|
||||
}
|
||||
if returnURL == "" {
|
||||
returnURL = "/"
|
||||
}
|
||||
err = a.states.Set(&cache.Item{
|
||||
Key: randStr,
|
||||
Object: &appState{
|
||||
ReturnURL: returnURL,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// This should never happen with the in-memory cache
|
||||
log.Errorf("Failed to set app state: %v", err)
|
||||
}
|
||||
return randStr
|
||||
}
|
||||
|
||||
func (a *ClientApp) verifyAppState(state string) (*appState, error) {
|
||||
var aState appState
|
||||
err := a.states.Get(state, &aState)
|
||||
if err != nil {
|
||||
if err == cache.ErrCacheMiss {
|
||||
return nil, fmt.Errorf("unknown app state %s", state)
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to verify app state %s: %v", state, err)
|
||||
}
|
||||
}
|
||||
// TODO: purge the state string from the cache so that it is a true nonce
|
||||
return &aState, nil
|
||||
}
|
||||
|
||||
func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var opts []oauth2.AuthCodeOption
|
||||
returnURL := r.FormValue("return_url")
|
||||
scopes := []string{"openid", "profile", "email", "groups"}
|
||||
appState := a.generateAppState(returnURL)
|
||||
if r.FormValue("offline_access") != "yes" {
|
||||
// no-op
|
||||
} else if a.sessionMgr.OfflineAsScope() {
|
||||
scopes = append(scopes, "offline_access")
|
||||
} else {
|
||||
opts = append(opts, oauth2.AccessTypeOffline)
|
||||
}
|
||||
oauth2Config, err := a.oauth2Config(scopes)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
authCodeURL := oauth2Config.AuthCodeURL(appState, opts...)
|
||||
http.Redirect(w, r, authCodeURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
err error
|
||||
token *oauth2.Token
|
||||
returnURL string
|
||||
)
|
||||
|
||||
ctx := oidc.ClientContext(r.Context(), a.client)
|
||||
oauth2Config, err := a.oauth2Config(nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
if errMsg := r.FormValue("error"); errMsg != "" {
|
||||
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var aState *appState
|
||||
aState, err = a.verifyAppState(r.FormValue("state"))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
returnURL = aState.ReturnURL
|
||||
token, err = oauth2Config.Exchange(ctx, code)
|
||||
case "POST":
|
||||
// Form request from frontend to refresh a token.
|
||||
refresh := r.FormValue("refresh_token")
|
||||
if refresh == "" {
|
||||
http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
RefreshToken: refresh,
|
||||
Expiry: time.Now().UTC().Add(-time.Hour),
|
||||
}
|
||||
token, err = oauth2Config.TokenSource(ctx, t).Token()
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := a.sessionMgr.VerifyToken(rawIDToken)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid session token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flags := []string{"path=/"}
|
||||
if a.secureCookie {
|
||||
flags = append(flags, "Secure")
|
||||
}
|
||||
cookie := session.MakeCookieMetadata(common.AuthCookieName, rawIDToken, flags...)
|
||||
w.Header().Set("Set-Cookie", cookie)
|
||||
log.Infof("Web login successful claims: %v", claims)
|
||||
if os.Getenv(common.EnvVarSSODebug) == "1" {
|
||||
claimsJSON, _ := json.MarshalIndent(claims, "", " ")
|
||||
renderToken(w, a.redirectURI, rawIDToken, token.RefreshToken, claimsJSON)
|
||||
} else {
|
||||
http.Redirect(w, r, returnURL, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ClientApp) verify(tokenString string) (*oidc.IDToken, error) {
|
||||
provider, err := a.sessionMgr.OIDCProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: a.clientID})
|
||||
return verifier.Verify(context.Background(), tokenString)
|
||||
}
|
||||
|
||||
15
util/http/http.go
Normal file
15
util/http/http.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
|
||||
func MakeCookieMetadata(key, value string, flags ...string) string {
|
||||
components := []string{
|
||||
fmt.Sprintf("%s=%s", key, value),
|
||||
}
|
||||
components = append(components, flags...)
|
||||
return strings.Join(components, "; ")
|
||||
}
|
||||
407
util/oidc/oidc.go
Normal file
407
util/oidc/oidc.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gooidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
httputil "github.com/argoproj/argo-cd/util/http"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
GrantTypeAuthorizationCode = "authorization_code"
|
||||
GrantTypeImplicit = "implicit"
|
||||
ResponseTypeCode = "code"
|
||||
)
|
||||
|
||||
// OIDCConfiguration holds a subset of interested fields from the OIDC configuration spec
|
||||
type OIDCConfiguration struct {
|
||||
Issuer string `json:"issuer"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
||||
}
|
||||
|
||||
type ClientApp struct {
|
||||
// OAuth2 client ID of this application (e.g. argo-cd)
|
||||
clientID string
|
||||
// OAuth2 client secret of this application
|
||||
clientSecret string
|
||||
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
|
||||
redirectURI string
|
||||
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
|
||||
issuerURL string
|
||||
// client is the HTTP client which is used to query the IDp
|
||||
client *http.Client
|
||||
// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
|
||||
// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
|
||||
secureCookie bool
|
||||
// settings holds ArgoCD settings
|
||||
settings *settings.ArgoCDSettings
|
||||
// provider is the OIDC configuration
|
||||
provider *gooidc.Provider
|
||||
// states holds temporary nonce tokens to which hold application state values
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
states cache.Cache
|
||||
}
|
||||
|
||||
type appState struct {
|
||||
// ReturnURL is the URL in which to redirect a user back to after completing an OAuth2 login
|
||||
ReturnURL string `json:"returnURL"`
|
||||
}
|
||||
|
||||
// NewClientApp will register the ArgoCD client app (either via Dex or external OIDC) and return an
|
||||
// object which has HTTP handlers for handling the HTTP responses for login and callback
|
||||
func NewClientApp(settings *settings.ArgoCDSettings) (*ClientApp, error) {
|
||||
a := ClientApp{
|
||||
clientID: settings.OAuth2ClientID(),
|
||||
clientSecret: settings.OAuth2ClientSecret(),
|
||||
redirectURI: settings.RedirectURL(),
|
||||
issuerURL: settings.IssuerURL(),
|
||||
}
|
||||
log.Infof("Creating client app (%s)", a.clientID)
|
||||
u, err := url.Parse(settings.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse redirect-uri: %v", err)
|
||||
}
|
||||
tlsConfig := settings.TLSConfig()
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
a.client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
// NOTE: if we ever have replicas of ArgoCD, this needs to switch to Redis cache
|
||||
a.states = cache.NewInMemoryCache(3 * time.Minute)
|
||||
a.secureCookie = bool(u.Scheme == "https")
|
||||
a.settings = settings
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func (a *ClientApp) oauth2Config(scopes []string) (*oauth2.Config, error) {
|
||||
provider, err := a.oidcProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oauth2.Config{
|
||||
ClientID: a.clientID,
|
||||
ClientSecret: a.clientSecret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: a.redirectURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAppState creates an app state nonce
|
||||
func (a *ClientApp) generateAppState(returnURL string) string {
|
||||
randStr := rand.RandString(10)
|
||||
if returnURL == "" {
|
||||
returnURL = "/"
|
||||
}
|
||||
err := a.states.Set(&cache.Item{
|
||||
Key: randStr,
|
||||
Object: &appState{
|
||||
ReturnURL: returnURL,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// This should never happen with the in-memory cache
|
||||
log.Errorf("Failed to set app state: %v", err)
|
||||
}
|
||||
return randStr
|
||||
}
|
||||
|
||||
func (a *ClientApp) verifyAppState(state string) (*appState, error) {
|
||||
var aState appState
|
||||
err := a.states.Get(state, &aState)
|
||||
if err != nil {
|
||||
if err == cache.ErrCacheMiss {
|
||||
return nil, fmt.Errorf("unknown app state %s", state)
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to verify app state %s: %v", state, err)
|
||||
}
|
||||
}
|
||||
// TODO: purge the state string from the cache so that it is a true nonce
|
||||
return &aState, nil
|
||||
}
|
||||
|
||||
// HandleLogin formulates the proper OAuth2 URL (auth code or implicit) and redirects the user to
|
||||
// the IDp login & consent page
|
||||
func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
provider, err := a.oidcProvider()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
oidcConf, err := ParseConfig(provider)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
scopes := []string{"openid", "profile", "email", "groups"}
|
||||
oauth2Config, err := a.oauth2Config(scopes)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
returnURL := r.FormValue("return_url")
|
||||
stateNonce := a.generateAppState(returnURL)
|
||||
grantType := InferGrantType(oauth2Config, oidcConf)
|
||||
var url string
|
||||
switch grantType {
|
||||
case GrantTypeAuthorizationCode:
|
||||
url = oauth2Config.AuthCodeURL(stateNonce)
|
||||
case GrantTypeImplicit:
|
||||
url = ImplicitFlowURL(oauth2Config, stateNonce)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("Unsupported grant type: %v", grantType), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Infof("Performing %s flow login: %s", grantType, url)
|
||||
http.Redirect(w, r, url, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleCallback is the callback handler for an OAuth2 login flow
|
||||
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := gooidc.ClientContext(r.Context(), a.client)
|
||||
oauth2Config, err := a.oauth2Config(nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Infof("Callback: %s", r.URL)
|
||||
if errMsg := r.FormValue("error"); errMsg != "" {
|
||||
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
state := r.FormValue("state")
|
||||
if code == "" {
|
||||
// If code was not given, it implies implicit flow
|
||||
a.handleImplicitFlow(w, r, state)
|
||||
return
|
||||
}
|
||||
appState, err := a.verifyAppState(state)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
token, err := oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
idTokenRAW, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
idToken, err := a.verify(idTokenRAW)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid session token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flags := []string{"path=/"}
|
||||
if a.secureCookie {
|
||||
flags = append(flags, "Secure")
|
||||
}
|
||||
cookie := httputil.MakeCookieMetadata(common.AuthCookieName, idTokenRAW, flags...)
|
||||
w.Header().Set("Set-Cookie", cookie)
|
||||
|
||||
var claims jwt.MapClaims
|
||||
err = idToken.Claims(&claims)
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
log.Infof("Web login successful. Claims: %s", claimsJSON)
|
||||
if os.Getenv(common.EnvVarSSODebug) == "1" {
|
||||
claimsJSON, _ := json.MarshalIndent(claims, "", " ")
|
||||
renderToken(w, a.redirectURI, idTokenRAW, token.RefreshToken, claimsJSON)
|
||||
} else {
|
||||
http.Redirect(w, r, appState.ReturnURL, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
var implicitFlowTmpl = template.Must(template.New("implicit.html").Parse(`<script>
|
||||
var hash = window.location.hash.substr(1);
|
||||
var result = hash.split('&').reduce(function (result, item) {
|
||||
var parts = item.split('=');
|
||||
result[parts[0]] = parts[1];
|
||||
return result;
|
||||
}, {});
|
||||
var idToken = result['id_token'];
|
||||
var state = result['state'];
|
||||
var returnURL = "{{ .ReturnURL }}";
|
||||
if (state != "" && returnURL == "") {
|
||||
window.location.href = window.location.href.split("#")[0] + "?state=" + result['state'] + window.location.hash;
|
||||
} else if (returnURL != "") {
|
||||
document.cookie = "{{ .CookieName }}=" + idToken + "; path=/";
|
||||
window.location.href = returnURL;
|
||||
}
|
||||
</script>`))
|
||||
|
||||
// handleImplicitFlow completes an implicit OAuth2 flow. The id_token and state will be contained
|
||||
// in the URL fragment. The javascript client first redirects to the callback URL, supplying the
|
||||
// state nonce for verification, as well as looking up the return URL. Once verified, the client
|
||||
// stores the id_token from the fragment as a cookie. Finally it performs the final redirect back to
|
||||
// the return URL.
|
||||
func (a *ClientApp) handleImplicitFlow(w http.ResponseWriter, r *http.Request, state string) {
|
||||
type implicitFlowValues struct {
|
||||
CookieName string
|
||||
ReturnURL string
|
||||
}
|
||||
vals := implicitFlowValues{
|
||||
CookieName: common.AuthCookieName,
|
||||
}
|
||||
if state != "" {
|
||||
appState, err := a.verifyAppState(state)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
vals.ReturnURL = appState.ReturnURL
|
||||
}
|
||||
renderTemplate(w, implicitFlowTmpl, vals)
|
||||
}
|
||||
|
||||
func (a *ClientApp) oidcProvider() (*gooidc.Provider, error) {
|
||||
if a.provider != nil {
|
||||
return a.provider, nil
|
||||
}
|
||||
provider, err := NewOIDCProvider(a.issuerURL, a.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.provider = provider
|
||||
return a.provider, nil
|
||||
}
|
||||
|
||||
func (a *ClientApp) verify(tokenString string) (*gooidc.IDToken, error) {
|
||||
provider, err := a.oidcProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier := provider.Verifier(&gooidc.Config{ClientID: a.clientID})
|
||||
return verifier.Verify(context.Background(), tokenString)
|
||||
}
|
||||
|
||||
// NewOIDCProvider initializes an OIDC provider, querying the well known oidc configuration path
|
||||
// http://example-argocd.com/api/dex/.well-known/openid-configuration
|
||||
func NewOIDCProvider(issuerURL string, client *http.Client) (*gooidc.Provider, error) {
|
||||
log.Infof("Initializing OIDC provider (issuer: %s)", issuerURL)
|
||||
ctx := gooidc.ClientContext(context.Background(), client)
|
||||
provider, err := gooidc.NewProvider(ctx, issuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
|
||||
}
|
||||
s, _ := ParseConfig(provider)
|
||||
log.Infof("OIDC supported scopes: %v", s.ScopesSupported)
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// ImplicitFlowURL is an adaptation of oauth2.Config::AuthCodeURL() which returns a URL
|
||||
// appropriate for an OAuth2 implicit login flow (as opposed to authorization code flow).
|
||||
func ImplicitFlowURL(c *oauth2.Config, state string, opts ...oauth2.AuthCodeOption) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(c.Endpoint.AuthURL)
|
||||
v := url.Values{
|
||||
"response_type": {"id_token"},
|
||||
"nonce": {rand.RandString(10)},
|
||||
"client_id": {c.ClientID},
|
||||
"redirect_uri": condVal(c.RedirectURL),
|
||||
"scope": condVal(strings.Join(c.Scopes, " ")),
|
||||
"state": condVal(state),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
switch opt {
|
||||
case oauth2.AccessTypeOnline:
|
||||
v.Set("access_type", "online")
|
||||
case oauth2.AccessTypeOffline:
|
||||
v.Set("access_type", "offline")
|
||||
case oauth2.ApprovalForce:
|
||||
v.Set("approval_prompt", "force")
|
||||
}
|
||||
}
|
||||
if strings.Contains(c.Endpoint.AuthURL, "?") {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
buf.WriteString(v.Encode())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func condVal(v string) []string {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
// OfflineAccess returns whether or not 'offline_access' is a supported scope
|
||||
func OfflineAccess(scopes []string) bool {
|
||||
if len(scopes) == 0 {
|
||||
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
|
||||
// one. If missing, assume that the provider follows the spec and has
|
||||
// an "offline_access" scope.
|
||||
return true
|
||||
}
|
||||
// See if scopes_supported has the "offline_access" scope.
|
||||
for _, scope := range scopes {
|
||||
if scope == gooidc.ScopeOfflineAccess {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseConfig parses the OIDC Config into the concrete datastructure
|
||||
func ParseConfig(provider *gooidc.Provider) (*OIDCConfiguration, error) {
|
||||
var conf OIDCConfiguration
|
||||
err := provider.Claims(&conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &conf, nil
|
||||
}
|
||||
|
||||
// InferGrantType infers the proper grant flow depending on the OAuth2 client config and OIDC configuration.
|
||||
// Returns either: "authorization_code" or "implicit"
|
||||
func InferGrantType(oauth2conf *oauth2.Config, oidcConf *OIDCConfiguration) string {
|
||||
if oauth2conf.ClientSecret != "" {
|
||||
// If we know the client secret, we are using the 'authorization_code' flow
|
||||
return GrantTypeAuthorizationCode
|
||||
}
|
||||
if len(oidcConf.ResponseTypesSupported) == 1 && oidcConf.ResponseTypesSupported[0] == ResponseTypeCode {
|
||||
// If we don't have the secret, check the supported response types. If the list is a single
|
||||
// response type of type 'code', then grant type is 'authorization_code'. This is the Dex
|
||||
// case, which does not support implicit login flow (https://github.com/dexidp/dex/issues/1254)
|
||||
return GrantTypeAuthorizationCode
|
||||
}
|
||||
// If we don't have the client secret (e.g. SPA app), we can assume to be implicit
|
||||
return GrantTypeImplicit
|
||||
}
|
||||
49
util/oidc/oidc_test.go
Normal file
49
util/oidc/oidc_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
spaOauth2Conf = &oauth2.Config{
|
||||
ClientID: "spa-id",
|
||||
}
|
||||
|
||||
webOauth2Conf = &oauth2.Config{
|
||||
ClientID: "spa-id",
|
||||
ClientSecret: "my-super-secret",
|
||||
}
|
||||
)
|
||||
|
||||
func TestInferGrantType(t *testing.T) {
|
||||
var grantType string
|
||||
dexRAW, err := ioutil.ReadFile("testdata/dex.json")
|
||||
assert.NoError(t, err)
|
||||
var dexConfig OIDCConfiguration
|
||||
err = json.Unmarshal(dexRAW, &dexConfig)
|
||||
assert.NoError(t, err)
|
||||
grantType = InferGrantType(spaOauth2Conf, &dexConfig)
|
||||
// Dex does not support implicit login flow (https://github.com/dexidp/dex/issues/1254)
|
||||
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
|
||||
grantType = InferGrantType(webOauth2Conf, &dexConfig)
|
||||
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
|
||||
|
||||
testFiles := []string{"testdata/okta.json", "testdata/auth0.json", "testdata/onelogin.json"}
|
||||
for _, path := range testFiles {
|
||||
oktaRAW, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
var oktaConfig OIDCConfiguration
|
||||
err = json.Unmarshal(oktaRAW, &oktaConfig)
|
||||
assert.NoError(t, err)
|
||||
grantType = InferGrantType(spaOauth2Conf, &oktaConfig)
|
||||
assert.Equal(t, GrantTypeImplicit, grantType)
|
||||
grantType = InferGrantType(webOauth2Conf, &oktaConfig)
|
||||
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dex
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
70
util/oidc/testdata/auth0.json
vendored
Normal file
70
util/oidc/testdata/auth0.json
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"issuer": "https://argocd-test.auth0.com/",
|
||||
"authorization_endpoint": "https://argocd-test.auth0.com/authorize",
|
||||
"token_endpoint": "https://argocd-test.auth0.com/oauth/token",
|
||||
"userinfo_endpoint": "https://argocd-test.auth0.com/userinfo",
|
||||
"mfa_challenge_endpoint": "https://argocd-test.auth0.com/mfa/challenge",
|
||||
"jwks_uri": "https://argocd-test.auth0.com/.well-known/jwks.json",
|
||||
"registration_endpoint": "https://argocd-test.auth0.com/oidc/register",
|
||||
"revocation_endpoint": "https://argocd-test.auth0.com/oauth/revoke",
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"profile",
|
||||
"offline_access",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"nickname",
|
||||
"email",
|
||||
"email_verified",
|
||||
"picture",
|
||||
"created_at",
|
||||
"identities",
|
||||
"phone",
|
||||
"address"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token"
|
||||
],
|
||||
"response_modes_supported": [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"HS256",
|
||||
"RS256"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post"
|
||||
],
|
||||
"claims_supported": [
|
||||
"aud",
|
||||
"auth_time",
|
||||
"created_at",
|
||||
"email",
|
||||
"email_verified",
|
||||
"exp",
|
||||
"family_name",
|
||||
"given_name",
|
||||
"iat",
|
||||
"identities",
|
||||
"iss",
|
||||
"name",
|
||||
"nickname",
|
||||
"phone_number",
|
||||
"picture",
|
||||
"sub"
|
||||
],
|
||||
"request_uri_parameter_supported": false
|
||||
}
|
||||
36
util/oidc/testdata/dex.json
vendored
Normal file
36
util/oidc/testdata/dex.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"issuer": "https://argocd.example.com/api/dex",
|
||||
"authorization_endpoint": "https://argocd.example.com/api/dex/auth",
|
||||
"token_endpoint": "https://argocd.example.com/api/dex/token",
|
||||
"jwks_uri": "https://argocd.example.com/api/dex/keys",
|
||||
"response_types_supported": [
|
||||
"code"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"email",
|
||||
"groups",
|
||||
"profile",
|
||||
"offline_access"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic"
|
||||
],
|
||||
"claims_supported": [
|
||||
"aud",
|
||||
"email",
|
||||
"email_verified",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"locale",
|
||||
"name",
|
||||
"sub"
|
||||
]
|
||||
}
|
||||
115
util/oidc/testdata/okta.json
vendored
Normal file
115
util/oidc/testdata/okta.json
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"issuer": "https://dev-123456.oktapreview.com",
|
||||
"authorization_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/authorize",
|
||||
"token_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/token",
|
||||
"userinfo_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/userinfo",
|
||||
"registration_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/clients",
|
||||
"jwks_uri": "https://dev-123456.oktapreview.com/oauth2/v1/keys",
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"id_token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"id_token token",
|
||||
"code id_token token"
|
||||
],
|
||||
"response_modes_supported": [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post",
|
||||
"okta_post_message"
|
||||
],
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"implicit",
|
||||
"refresh_token",
|
||||
"password"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
"address",
|
||||
"phone",
|
||||
"offline_access",
|
||||
"groups"
|
||||
],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none"
|
||||
],
|
||||
"claims_supported": [
|
||||
"iss",
|
||||
"ver",
|
||||
"sub",
|
||||
"aud",
|
||||
"iat",
|
||||
"exp",
|
||||
"jti",
|
||||
"auth_time",
|
||||
"amr",
|
||||
"idp",
|
||||
"nonce",
|
||||
"name",
|
||||
"nickname",
|
||||
"preferred_username",
|
||||
"given_name",
|
||||
"middle_name",
|
||||
"family_name",
|
||||
"email",
|
||||
"email_verified",
|
||||
"profile",
|
||||
"zoneinfo",
|
||||
"locale",
|
||||
"address",
|
||||
"phone_number",
|
||||
"picture",
|
||||
"website",
|
||||
"gender",
|
||||
"birthdate",
|
||||
"updated_at",
|
||||
"at_hash",
|
||||
"c_hash"
|
||||
],
|
||||
"code_challenge_methods_supported": [
|
||||
"S256"
|
||||
],
|
||||
"introspection_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/introspect",
|
||||
"introspection_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none"
|
||||
],
|
||||
"revocation_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/revoke",
|
||||
"revocation_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none"
|
||||
],
|
||||
"end_session_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/logout",
|
||||
"request_parameter_supported": true,
|
||||
"request_object_signing_alg_values_supported": [
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512"
|
||||
]
|
||||
}
|
||||
87
util/oidc/testdata/onelogin.json
vendored
Normal file
87
util/oidc/testdata/onelogin.json
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"acr_values_supported": [
|
||||
"onelogin:nist:level:1:re-auth"
|
||||
],
|
||||
"authorization_endpoint": "https://argocd-dev.onelogin.com/oidc/auth",
|
||||
"claims_parameter_supported": true,
|
||||
"claims_supported": [
|
||||
"acr",
|
||||
"auth_time",
|
||||
"company",
|
||||
"custom_fields",
|
||||
"department",
|
||||
"email",
|
||||
"family_name",
|
||||
"given_name",
|
||||
"groups",
|
||||
"iss",
|
||||
"locale_code",
|
||||
"name",
|
||||
"phone_number",
|
||||
"preferred_username",
|
||||
"sub",
|
||||
"title",
|
||||
"updated_at"
|
||||
],
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"implicit",
|
||||
"refresh_token",
|
||||
"password"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"issuer": "https://openid-connect.onelogin.com/oidc",
|
||||
"jwks_uri": "https://argocd-dev.onelogin.com/oidc/certs",
|
||||
"request_parameter_supported": false,
|
||||
"request_uri_parameter_supported": false,
|
||||
"response_modes_supported": [
|
||||
"form_post",
|
||||
"fragment",
|
||||
"query"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"id_token token",
|
||||
"id_token"
|
||||
],
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"name",
|
||||
"profile",
|
||||
"groups",
|
||||
"email",
|
||||
"phone"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public"
|
||||
],
|
||||
"token_endpoint": "https://argocd-dev.onelogin.com/oidc/token",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none"
|
||||
],
|
||||
"userinfo_endpoint": "https://argocd-dev.onelogin.com/oidc/me",
|
||||
"userinfo_signing_alg_values_supported": [],
|
||||
"code_challenge_methods_supported": [
|
||||
"plain",
|
||||
"S256"
|
||||
],
|
||||
"introspection_endpoint": "https://argocd-dev.onelogin.com/oidc/token/introspection",
|
||||
"introspection_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none"
|
||||
],
|
||||
"revocation_endpoint": "https://argocd-dev.onelogin.com/oidc/token/revocation",
|
||||
"revocation_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none"
|
||||
],
|
||||
"claim_types_supported": [
|
||||
"normal"
|
||||
]
|
||||
}
|
||||
37
util/rand/rand.go
Normal file
37
util/rand/rand.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
// RandString generates, from a given charset, a cryptographically-secure pseudo-random string of a given length.
|
||||
func RandString(n int) string {
|
||||
return RandStringCharset(n, letterBytes)
|
||||
}
|
||||
|
||||
func RandStringCharset(n int, charset string) string {
|
||||
b := make([]byte, n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(charset) {
|
||||
b[i] = charset[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dex
|
||||
package rand
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -6,20 +6,11 @@ import (
|
||||
|
||||
func TestRandString(t *testing.T) {
|
||||
var ss string
|
||||
var err error
|
||||
|
||||
ss, err = randString(10, "A")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate entropy: %v", err)
|
||||
}
|
||||
ss = RandStringCharset(10, "A")
|
||||
if ss != "AAAAAAAAAA" {
|
||||
t.Errorf("Expected 10 As, but got %q", ss)
|
||||
}
|
||||
|
||||
ss, err = randString(5, "ABC123")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate entropy: %v", err)
|
||||
}
|
||||
ss = RandStringCharset(5, "ABC123")
|
||||
if len(ss) != 5 {
|
||||
t.Errorf("Expected random string of length 10, but got %q", ss)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
jwtutil "github.com/argoproj/argo-cd/util/jwt"
|
||||
oidcutil "github.com/argoproj/argo-cd/util/oidc"
|
||||
passwordutil "github.com/argoproj/argo-cd/util/password"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
@@ -27,10 +28,6 @@ type SessionManager struct {
|
||||
settings *settings.ArgoCDSettings
|
||||
client *http.Client
|
||||
provider *oidc.Provider
|
||||
|
||||
// Does the provider use "offline_access" scope to request a refresh token
|
||||
// or does it use "access_type=offline" (e.g. Google)?
|
||||
offlineAsScope bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -153,7 +150,7 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, error) {
|
||||
return mgr.Parse(tokenString)
|
||||
default:
|
||||
// Dex signed token
|
||||
provider, err := mgr.OIDCProvider()
|
||||
provider, err := mgr.oidcProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -208,20 +205,11 @@ func Username(ctx context.Context) string {
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
|
||||
func MakeCookieMetadata(key, value string, flags ...string) string {
|
||||
components := []string{
|
||||
fmt.Sprintf("%s=%s", key, value),
|
||||
}
|
||||
components = append(components, flags...)
|
||||
return strings.Join(components, "; ")
|
||||
}
|
||||
|
||||
// OIDCProvider lazily initializes, memoizes, and returns the OIDC provider.
|
||||
// We have to initialize the provider lazily since ArgoCD is an OIDC client to itself, which
|
||||
// presents a chicken-and-egg problem of (1) serving dex over HTTP, and (2) querying the OIDC
|
||||
// provider (ourselves) to initialize the app.
|
||||
func (mgr *SessionManager) OIDCProvider() (*oidc.Provider, error) {
|
||||
// oidcProvider lazily initializes, memoizes, and returns the OIDC provider.
|
||||
// We have to initialize the provider lazily since ArgoCD can be an OIDC client to itself (in the
|
||||
// case of dex reverse proxy), which presents a chicken-and-egg problem of (1) serving dex over
|
||||
// HTTP, and (2) querying the OIDC provider (ourself) to initialize the app.
|
||||
func (mgr *SessionManager) oidcProvider() (*oidc.Provider, error) {
|
||||
if mgr.provider != nil {
|
||||
return mgr.provider, nil
|
||||
}
|
||||
@@ -234,48 +222,14 @@ func (mgr *SessionManager) initializeOIDCProvider() (*oidc.Provider, error) {
|
||||
if !mgr.settings.IsSSOConfigured() {
|
||||
return nil, fmt.Errorf("SSO is not configured")
|
||||
}
|
||||
issuerURL := mgr.settings.IssuerURL()
|
||||
log.Infof("Initializing OIDC provider (issuer: %s)", issuerURL)
|
||||
ctx := oidc.ClientContext(context.Background(), mgr.client)
|
||||
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||
provider, err := oidcutil.NewOIDCProvider(mgr.settings.IssuerURL(), mgr.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
|
||||
}
|
||||
// Returns the scopes the provider supports
|
||||
// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
var s struct {
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
}
|
||||
if err := provider.Claims(&s); err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse provider scopes_supported: %v", err)
|
||||
}
|
||||
log.Infof("OpenID supported scopes: %v", s.ScopesSupported)
|
||||
offlineAsScope := false
|
||||
if len(s.ScopesSupported) == 0 {
|
||||
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
|
||||
// one. If missing, assume that the provider follows the spec and has
|
||||
// an "offline_access" scope.
|
||||
offlineAsScope = true
|
||||
} else {
|
||||
// See if scopes_supported has the "offline_access" scope.
|
||||
for _, scope := range s.ScopesSupported {
|
||||
if scope == oidc.ScopeOfflineAccess {
|
||||
offlineAsScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
mgr.provider = provider
|
||||
mgr.offlineAsScope = offlineAsScope
|
||||
return mgr.provider, nil
|
||||
}
|
||||
|
||||
// OfflineAsScope returns whether or not the OIDC provider supports offline as a scope
|
||||
func (mgr *SessionManager) OfflineAsScope() bool {
|
||||
_, _ = mgr.OIDCProvider() // forces offlineAsScope to be determined
|
||||
return mgr.offlineAsScope
|
||||
}
|
||||
|
||||
type debugTransport struct {
|
||||
t http.RoundTripper
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -24,6 +22,7 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/password"
|
||||
tlsutil "github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
@@ -36,8 +35,10 @@ type ArgoCDSettings struct {
|
||||
// Admin superuser password storage
|
||||
AdminPasswordHash string `json:"adminPasswordHash,omitempty"`
|
||||
AdminPasswordMtime time.Time `json:"adminPasswordMtime,omitempty"`
|
||||
// DexConfig is contains portions of a dex config yaml
|
||||
// DexConfig contains portions of a dex config yaml
|
||||
DexConfig string `json:"dexConfig,omitempty"`
|
||||
// OIDCConfigRAW holds OIDC configuration as a raw string
|
||||
OIDCConfigRAW string `json:"oidcConfig,omitempty"`
|
||||
// ServerSignature holds the key used to generate JWT tokens.
|
||||
ServerSignature []byte `json:"serverSignature,omitempty"`
|
||||
// Certificate holds the certificate/private key for the ArgoCD API server.
|
||||
@@ -53,6 +54,13 @@ type ArgoCDSettings struct {
|
||||
Secrets map[string]string `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
ClientID string `json:"clientID,omitempty"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// settingAdminPasswordHashKey designates the key for a root password hash inside a Kubernetes secret.
|
||||
settingAdminPasswordHashKey = "admin.password"
|
||||
@@ -68,6 +76,8 @@ const (
|
||||
settingURLKey = "url"
|
||||
// settingDexConfigKey designates the key for the dex config
|
||||
settingDexConfigKey = "dex.config"
|
||||
// settingsOIDCConfigKey designates the key for OIDC config
|
||||
settingsOIDCConfigKey = "oidc.config"
|
||||
// settingsWebhookGitHubSecret is the key for the GitHub shared webhook secret
|
||||
settingsWebhookGitHubSecretKey = "webhook.github.secret"
|
||||
// settingsWebhookGitLabSecret is the key for the GitLab shared webhook secret
|
||||
@@ -115,6 +125,7 @@ func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) {
|
||||
|
||||
func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) {
|
||||
settings.DexConfig = argoCDCM.Data[settingDexConfigKey]
|
||||
settings.OIDCConfigRAW = argoCDCM.Data[settingsOIDCConfigKey]
|
||||
settings.URL = argoCDCM.Data[settingURLKey]
|
||||
}
|
||||
|
||||
@@ -183,8 +194,22 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
|
||||
if argoCDCM.Data == nil {
|
||||
argoCDCM.Data = make(map[string]string)
|
||||
}
|
||||
argoCDCM.Data[settingURLKey] = settings.URL
|
||||
argoCDCM.Data[settingDexConfigKey] = settings.DexConfig
|
||||
if settings.URL != "" {
|
||||
argoCDCM.Data[settingURLKey] = settings.URL
|
||||
} else {
|
||||
delete(argoCDCM.Data, settingURLKey)
|
||||
}
|
||||
if settings.DexConfig != "" {
|
||||
argoCDCM.Data[settingDexConfigKey] = settings.DexConfig
|
||||
} else {
|
||||
delete(argoCDCM.Data, settings.DexConfig)
|
||||
}
|
||||
if settings.OIDCConfigRAW != "" {
|
||||
argoCDCM.Data[settingsOIDCConfigKey] = settings.OIDCConfigRAW
|
||||
} else {
|
||||
delete(argoCDCM.Data, settingsOIDCConfigKey)
|
||||
}
|
||||
|
||||
if createCM {
|
||||
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(argoCDCM)
|
||||
} else {
|
||||
@@ -253,6 +278,16 @@ func NewSettingsManager(clientset kubernetes.Interface, namespace string) *Setti
|
||||
|
||||
// IsSSOConfigured returns whether or not single-sign-on is configured
|
||||
func (a *ArgoCDSettings) IsSSOConfigured() bool {
|
||||
if a.IsDexConfigured() {
|
||||
return true
|
||||
}
|
||||
if a.OIDCConfig() != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) IsDexConfigured() bool {
|
||||
if a.URL == "" {
|
||||
return false
|
||||
}
|
||||
@@ -265,6 +300,19 @@ func (a *ArgoCDSettings) IsSSOConfigured() bool {
|
||||
return len(dexCfg) > 0
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) OIDCConfig() *OIDCConfig {
|
||||
if a.OIDCConfigRAW == "" {
|
||||
return nil
|
||||
}
|
||||
var oidcConfig OIDCConfig
|
||||
err := yaml.Unmarshal([]byte(a.OIDCConfigRAW), &oidcConfig)
|
||||
if err != nil {
|
||||
log.Warnf("invalid oidc config: %v", err)
|
||||
return nil
|
||||
}
|
||||
return &oidcConfig
|
||||
}
|
||||
|
||||
// TLSConfig returns a tls.Config with the configured certificates
|
||||
func (a *ArgoCDSettings) TLSConfig() *tls.Config {
|
||||
if a.Certificate == nil {
|
||||
@@ -282,18 +330,44 @@ func (a *ArgoCDSettings) TLSConfig() *tls.Config {
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) IssuerURL() string {
|
||||
return a.URL + common.DexAPIEndpoint
|
||||
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
|
||||
return oidcConfig.Issuer
|
||||
}
|
||||
if a.DexConfig != "" {
|
||||
return a.URL + common.DexAPIEndpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) OAuth2ClientID() string {
|
||||
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
|
||||
return oidcConfig.ClientID
|
||||
}
|
||||
if a.DexConfig != "" {
|
||||
return common.ArgoCDClientAppID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) OAuth2ClientSecret() string {
|
||||
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
|
||||
return oidcConfig.ClientSecret
|
||||
}
|
||||
if a.DexConfig != "" {
|
||||
return a.DexOAuth2ClientSecret()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *ArgoCDSettings) RedirectURL() string {
|
||||
return a.URL + common.CallbackEndpoint
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret calculates an arbitrary, but predictable OAuth2 client secret string derived
|
||||
// DexOAuth2ClientSecret calculates an arbitrary, but predictable OAuth2 client secret string derived
|
||||
// from the server secret. This is called by the dex startup wrapper (argocd-util rundex), as well
|
||||
// as the API server, such that they both independently come to the same conclusion of what the
|
||||
// OAuth2 shared client secret should be.
|
||||
func (a *ArgoCDSettings) OAuth2ClientSecret() string {
|
||||
func (a *ArgoCDSettings) DexOAuth2ClientSecret() string {
|
||||
h := sha256.New()
|
||||
_, err := h.Write(a.ServerSignature)
|
||||
if err != nil {
|
||||
@@ -407,27 +481,6 @@ func (mgr *SettingsManager) notifySubscribers() {
|
||||
}
|
||||
}
|
||||
|
||||
func ReadAndConfirmPassword() (string, error) {
|
||||
for {
|
||||
fmt.Print("*** Enter new password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Print("*** Confirm new password: ")
|
||||
confirmPassword, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Print("\n")
|
||||
if string(password) == string(confirmPassword) {
|
||||
return string(password), nil
|
||||
}
|
||||
log.Error("Passwords do not match")
|
||||
}
|
||||
}
|
||||
|
||||
func isIncompleteSettingsError(err error) bool {
|
||||
_, ok := err.(*incompleteSettingsError)
|
||||
return ok
|
||||
@@ -454,7 +507,7 @@ func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, update
|
||||
if cdSettings.AdminPasswordHash == "" || updateSuperuser {
|
||||
passwordRaw := defaultPassword
|
||||
if passwordRaw == "" {
|
||||
passwordRaw, err = ReadAndConfirmPassword()
|
||||
passwordRaw, err = cli.ReadAndConfirmPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user