diff --git a/pkg/apiclient/apiclient.go b/pkg/apiclient/apiclient.go index 694c1bce5d82e..1f0972490e11e 100644 --- a/pkg/apiclient/apiclient.go +++ b/pkg/apiclient/apiclient.go @@ -13,6 +13,7 @@ import ( "github.com/argoproj/argo-cd/server/cluster" "github.com/argoproj/argo-cd/server/repository" "github.com/argoproj/argo-cd/server/session" + config_util "github.com/argoproj/argo-cd/util/config" grpc_util "github.com/argoproj/argo-cd/util/grpc" log "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -24,6 +25,7 @@ const ( EnvArgoCDServer = "ARGOCD_SERVER" ) +// ServerClient defines an interface for interaction with an Argo CD server. type ServerClient interface { NewConn() (*grpc.ClientConn, error) NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error) @@ -36,6 +38,7 @@ type ServerClient interface { NewSessionClientOrDie() (*grpc.ClientConn, session.SessionServiceClient) } +// ClientOptions hold address, security, and other settings for the API client. type ClientOptions struct { ServerAddr string Insecure bool @@ -46,6 +49,7 @@ type client struct { ClientOptions } +// NewClient creates a new API client from a set of config options. func NewClient(opts *ClientOptions) (ServerClient, error) { clientOpts := *opts if clientOpts.ServerAddr == "" { @@ -59,6 +63,7 @@ func NewClient(opts *ClientOptions) (ServerClient, error) { }, 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) ServerClient { client, err := NewClient(opts) if err != nil { @@ -67,6 +72,42 @@ func NewClientOrDie(opts *ClientOptions) ServerClient { return client } +// JwtCredentials holds a token for authentication. +type jwtCredentials struct { + Token string +} + +func (c jwtCredentials) RequireTransportSecurity() bool { + return false +} + +func (c jwtCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) { + return map[string]string{ + "tokens": c.Token, + }, nil +} + +// endpointCredentials retrieves from configuration the login token for a given endpoint. +func endpointCredentials(endpoint string) jwtCredentials { + credentials := jwtCredentials{} + + localConfig, err := config_util.ReadLocalConfig() + if err != nil { + return credentials + } + + token, ok := localConfig.Sessions[endpoint] + if !ok { + // Use blank-key token, if it exists, as a fallback + token, ok = localConfig.Sessions[""] + } + + if ok { + credentials.Token = token + } + return credentials +} + func (c *client) NewConn() (*grpc.ClientConn, error) { var creds credentials.TransportCredentials if c.CertFile != "" { @@ -93,7 +134,7 @@ func (c *client) NewConn() (*grpc.ClientConn, error) { creds = credentials.NewTLS(&tlsConfig) } } - return grpc_util.BlockingDial(context.Background(), "tcp", c.ServerAddr, creds) + return grpc_util.BlockingDial(context.Background(), "tcp", c.ServerAddr, creds, grpc.WithPerRPCCredentials(endpointCredentials(c.ServerAddr))) } func (c *client) NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error) { diff --git a/server/server.go b/server/server.go index ee7f90b92c676..a8236675012e0 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "os" "strings" argocd "github.com/argoproj/argo-cd" @@ -21,14 +22,18 @@ import ( "github.com/argoproj/argo-cd/util/config" grpc_util "github.com/argoproj/argo-cd/util/grpc" jsonutil "github.com/argoproj/argo-cd/util/json" + util_session "github.com/argoproj/argo-cd/util/session" tlsutil "github.com/argoproj/argo-cd/util/tls" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" "github.com/grpc-ecosystem/grpc-gateway/runtime" log "github.com/sirupsen/logrus" "github.com/soheilhy/cmux" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" "k8s.io/client-go/kubernetes" ) @@ -144,10 +149,12 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server { // This is because TLS handshaking occurs in cmux handling sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_logrus.StreamServerInterceptor(a.log), + grpc_auth.StreamServerInterceptor(a.authenticate), grpc_util.PanicLoggerStreamServerInterceptor(a.log), ))) sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_logrus.UnaryServerInterceptor(a.log), + grpc_auth.UnaryServerInterceptor(a.authenticate), grpc_util.PanicLoggerUnaryServerInterceptor(a.log), ))) @@ -254,3 +261,24 @@ func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runt panic(err) } } + +// Authenticate checks for the presence of a token when accessing server-side resources. +func (a *ArgoCDServer) authenticate(ctx context.Context) (context.Context, error) { + if os.Getenv("REQUIREAUTH") != "1" { + return ctx, nil + } + + if md, ok := metadata.FromIncomingContext(ctx); ok { + mgr := util_session.MakeSessionManager(a.settings.ServerSignature) + tokens := md["tokens"] + for _, token := range tokens { + _, err := mgr.Parse(token) + if err == nil { + return ctx, nil + } + } + return ctx, grpc.Errorf(codes.Unauthenticated, "user is not allowed access") + } + + return ctx, grpc.Errorf(codes.Unauthenticated, "empty metadata") +} diff --git a/server/session/session.go b/server/session/session.go index 37b55414da6e4..8a6a0ca5c2cb0 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -64,3 +64,11 @@ func (s *Server) Create(ctx context.Context, q *SessionRequest) (*SessionRespons } return &SessionResponse{token}, err } + +// AuthFuncOverride overrides the authentication function and let us not require auth to receive auth. +// Without this function here, ArgoCDServer.authenticate would be invoked and credentials checked. +// Since this service is generally invoked when the user has _no_ credentials, that would create a +// chicken-and-egg situation if we didn't place this here to allow traffic to pass through. +func (s *Server) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) { + return ctx, nil +} diff --git a/util/session/sessionmanager.go b/util/session/sessionmanager.go index e5a6b0c5d7ba8..8865bddba42f0 100644 --- a/util/session/sessionmanager.go +++ b/util/session/sessionmanager.go @@ -72,8 +72,10 @@ func (mgr SessionManager) Parse(tokenString string) (*SessionManagerTokenClaims, return mgr.serverSecretKey, nil }) - if claims, ok := token.Claims.(*SessionManagerTokenClaims); ok && token.Valid { - return claims, nil + if token != nil { + if claims, ok := token.Claims.(*SessionManagerTokenClaims); ok && token.Valid { + return claims, nil + } } return nil, err }