From 764b7a627d952ccc9b7724560eb4cddf640d0ade Mon Sep 17 00:00:00 2001 From: Alexander Matyushentsev Date: Tue, 22 Feb 2022 17:57:11 -0800 Subject: [PATCH] refactor: use argocd-git-ask-pass to pass git credentials to git/kustomize (#8516) refactor: use argocd-git-ask-pass to pass git credentials to git/kustomize (#8516) Signed-off-by: Alexander Matyushentsev --- .github/workflows/ci-build.yaml | 3 + Dockerfile | 1 - .../commands/argocd_git_ask_pass.go | 57 ++ .../commands/argocd_repo_server.go | 5 +- cmd/argocd/commands/app.go | 2 +- cmd/main.go | 3 + .../application/v1alpha1/repository_types.go | 8 +- reposerver/askpass/askpass.pb.go | 646 ++++++++++++++++++ reposerver/askpass/askpass.proto | 20 + reposerver/askpass/common.go | 10 + reposerver/askpass/server.go | 90 +++ reposerver/askpass/server_test.go | 25 + reposerver/repository/repository.go | 26 +- reposerver/repository/repository_test.go | 11 +- reposerver/server.go | 10 +- test/e2e/custom_tool_test.go | 20 +- test/fixture/path/hack_path.go | 4 +- util/git/client.go | 2 +- util/git/creds.go | 80 ++- util/git/git_test.go | 4 +- 20 files changed, 958 insertions(+), 69 deletions(-) create mode 100644 cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go create mode 100644 reposerver/askpass/askpass.pb.go create mode 100644 reposerver/askpass/askpass.proto create mode 100644 reposerver/askpass/common.go create mode 100644 reposerver/askpass/server.go create mode 100644 reposerver/askpass/server_test.go diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 3646323dc858f..e957031dce77f 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -376,6 +376,9 @@ jobs: - name: Add /usr/local/bin to PATH run: | echo "/usr/local/bin" >> $GITHUB_PATH + - name: Add ./dist to PATH + run: | + echo "$(pwd)/dist" >> $GITHUB_PATH - name: Download Go dependencies run: | go mod download diff --git a/Dockerfile b/Dockerfile index 4216e590124d7..0e9398ff3ebd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,6 @@ RUN groupadd -g 999 argocd && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks diff --git a/cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go b/cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go new file mode 100644 index 0000000000000..8c3596562f481 --- /dev/null +++ b/cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go @@ -0,0 +1,57 @@ +package commands + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/argoproj/argo-cd/v2/util/git" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + + "github.com/argoproj/argo-cd/v2/reposerver/askpass" + "github.com/argoproj/argo-cd/v2/util/errors" + grpc_util "github.com/argoproj/argo-cd/v2/util/grpc" + "github.com/argoproj/argo-cd/v2/util/io" +) + +const ( + // cliName is the name of the CLI + cliName = "argocd-git-ask-pass" +) + +func NewCommand() *cobra.Command { + var command = cobra.Command{ + Use: cliName, + Short: "Argo CD git credential helper", + DisableAutoGenTag: true, + Run: func(c *cobra.Command, args []string) { + if len(os.Args) != 2 { + errors.CheckError(fmt.Errorf("expected 1 argument, got %d", len(os.Args)-1)) + } + nonce := os.Getenv(git.ASKPASS_NONCE_ENV) + if nonce == "" { + errors.CheckError(fmt.Errorf("%s is not set", git.ASKPASS_NONCE_ENV)) + } + conn, err := grpc_util.BlockingDial(context.Background(), "unix", askpass.SocketPath, nil, grpc.WithInsecure()) + errors.CheckError(err) + defer io.Close(conn) + client := askpass.NewAskPassServiceClient(conn) + + creds, err := client.GetCredentials(context.Background(), &askpass.CredentialsRequest{Nonce: nonce}) + errors.CheckError(err) + switch { + case strings.HasPrefix(os.Args[1], "Username"): + fmt.Println(creds.Username) + case strings.HasPrefix(os.Args[1], "Password"): + fmt.Println(creds.Password) + default: + errors.CheckError(fmt.Errorf("unknown credential type '%s'", os.Args[1])) + } + }, + } + + return &command +} diff --git a/cmd/argocd-repo-server/commands/argocd_repo_server.go b/cmd/argocd-repo-server/commands/argocd_repo_server.go index ef0a1895256fa..1d3c2eaf1ae1d 100644 --- a/cmd/argocd-repo-server/commands/argocd_repo_server.go +++ b/cmd/argocd-repo-server/commands/argocd_repo_server.go @@ -18,6 +18,7 @@ import ( "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/reposerver" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" + "github.com/argoproj/argo-cd/v2/reposerver/askpass" reposervercache "github.com/argoproj/argo-cd/v2/reposerver/cache" "github.com/argoproj/argo-cd/v2/reposerver/metrics" "github.com/argoproj/argo-cd/v2/reposerver/repository" @@ -94,6 +95,7 @@ func NewCommand() *cobra.Command { cache, err := cacheSrc() errors.CheckError(err) + askPassServer := askpass.NewServer() metricsServer := metrics.NewMetricsServer() cacheutil.CollectMetrics(redisClient, metricsServer) server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, repository.RepoServerInitConstants{ @@ -102,7 +104,7 @@ func NewCommand() *cobra.Command { PauseGenerationOnFailureForMinutes: getPauseGenerationOnFailureForMinutes(), PauseGenerationOnFailureForRequests: getPauseGenerationOnFailureForRequests(), SubmoduleEnabled: getSubmoduleEnabled(), - }) + }, askPassServer) errors.CheckError(err) grpc := server.CreateGRPC() @@ -133,6 +135,7 @@ func NewCommand() *cobra.Command { }) http.Handle("/metrics", metricsServer.GetHandler()) go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }() + go func() { errors.CheckError(askPassServer.Run(askpass.SocketPath)) }() if gpg.IsGPGEnabled() { log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath()) diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 8d036492f6786..8ba57296476c3 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -775,7 +775,7 @@ func getLocalObjectsString(app *argoappv1.Application, local, localRepoRoot, app ApiVersions: apiVersions, Plugins: configManagementPlugins, TrackingMethod: trackingMethod, - }, true) + }, true, &git.NoopCredsStore{}) errors.CheckError(err) return res.Manifests diff --git a/cmd/main.go b/cmd/main.go index 8db5f903a6bf6..1756d2ddf9f5f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,6 +10,7 @@ import ( appcontroller "github.com/argoproj/argo-cd/v2/cmd/argocd-application-controller/commands" cmpserver "github.com/argoproj/argo-cd/v2/cmd/argocd-cmp-server/commands" dex "github.com/argoproj/argo-cd/v2/cmd/argocd-dex/commands" + gitaskpass "github.com/argoproj/argo-cd/v2/cmd/argocd-git-ask-pass/commands" notification "github.com/argoproj/argo-cd/v2/cmd/argocd-notification/commands" reposerver "github.com/argoproj/argo-cd/v2/cmd/argocd-repo-server/commands" apiserver "github.com/argoproj/argo-cd/v2/cmd/argocd-server/commands" @@ -42,6 +43,8 @@ func main() { command = dex.NewCommand() case "argocd-notifications": command = notification.NewCommand() + case "argocd-git-ask-pass": + command = gitaskpass.NewCommand() default: command = cli.NewCommand() } diff --git a/pkg/apis/application/v1alpha1/repository_types.go b/pkg/apis/application/v1alpha1/repository_types.go index dc54c604b0b39..862627f216f0a 100644 --- a/pkg/apis/application/v1alpha1/repository_types.go +++ b/pkg/apis/application/v1alpha1/repository_types.go @@ -166,18 +166,18 @@ func (repo *Repository) CopyCredentialsFrom(source *RepoCreds) { } // GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository -func (repo *Repository) GetGitCreds() git.Creds { +func (repo *Repository) GetGitCreds(store git.CredsStore) git.Creds { if repo == nil { return git.NopCreds{} } if repo.Password != "" { - return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy) + return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store) } if repo.SSHPrivateKey != "" { - return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure()) + return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store) } if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 { - return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure()) + return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), store) } return git.NopCreds{} } diff --git a/reposerver/askpass/askpass.pb.go b/reposerver/askpass/askpass.pb.go new file mode 100644 index 0000000000000..d1d2a4612a9ac --- /dev/null +++ b/reposerver/askpass/askpass.pb.go @@ -0,0 +1,646 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: reposerver/askpass/askpass.proto + +package askpass + +import ( + context "context" + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type CredentialsRequest struct { + Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CredentialsRequest) Reset() { *m = CredentialsRequest{} } +func (m *CredentialsRequest) String() string { return proto.CompactTextString(m) } +func (*CredentialsRequest) ProtoMessage() {} +func (*CredentialsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_099f282cab154dba, []int{0} +} +func (m *CredentialsRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CredentialsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CredentialsRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CredentialsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CredentialsRequest.Merge(m, src) +} +func (m *CredentialsRequest) XXX_Size() int { + return m.Size() +} +func (m *CredentialsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CredentialsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CredentialsRequest proto.InternalMessageInfo + +func (m *CredentialsRequest) GetNonce() string { + if m != nil { + return m.Nonce + } + return "" +} + +type CredentialsResponse struct { + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CredentialsResponse) Reset() { *m = CredentialsResponse{} } +func (m *CredentialsResponse) String() string { return proto.CompactTextString(m) } +func (*CredentialsResponse) ProtoMessage() {} +func (*CredentialsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_099f282cab154dba, []int{1} +} +func (m *CredentialsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CredentialsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CredentialsResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CredentialsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CredentialsResponse.Merge(m, src) +} +func (m *CredentialsResponse) XXX_Size() int { + return m.Size() +} +func (m *CredentialsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CredentialsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CredentialsResponse proto.InternalMessageInfo + +func (m *CredentialsResponse) GetUsername() string { + if m != nil { + return m.Username + } + return "" +} + +func (m *CredentialsResponse) GetPassword() string { + if m != nil { + return m.Password + } + return "" +} + +func init() { + proto.RegisterType((*CredentialsRequest)(nil), "askpass.CredentialsRequest") + proto.RegisterType((*CredentialsResponse)(nil), "askpass.CredentialsResponse") +} + +func init() { proto.RegisterFile("reposerver/askpass/askpass.proto", fileDescriptor_099f282cab154dba) } + +var fileDescriptor_099f282cab154dba = []byte{ + // 231 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x28, 0x4a, 0x2d, 0xc8, + 0x2f, 0x4e, 0x2d, 0x2a, 0x4b, 0x2d, 0xd2, 0x4f, 0x2c, 0xce, 0x2e, 0x48, 0x2c, 0x2e, 0x86, 0xd1, + 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xec, 0x50, 0xae, 0x92, 0x16, 0x97, 0x90, 0x73, 0x51, + 0x6a, 0x4a, 0x6a, 0x5e, 0x49, 0x66, 0x62, 0x4e, 0x71, 0x50, 0x6a, 0x61, 0x69, 0x6a, 0x71, 0x89, + 0x90, 0x08, 0x17, 0x6b, 0x5e, 0x7e, 0x5e, 0x72, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10, + 0x84, 0xa3, 0xe4, 0xcb, 0x25, 0x8c, 0xa2, 0xb6, 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0x55, 0x48, 0x8a, + 0x8b, 0xa3, 0xb4, 0x38, 0xb5, 0x28, 0x2f, 0x31, 0x17, 0xa6, 0x1e, 0xce, 0x07, 0xc9, 0x81, 0xac, + 0x29, 0xcf, 0x2f, 0x4a, 0x91, 0x60, 0x82, 0xc8, 0xc1, 0xf8, 0x46, 0xf1, 0x5c, 0x7c, 0x8e, 0xc5, + 0xd9, 0x01, 0x89, 0xc5, 0xc5, 0xc1, 0xa9, 0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xbe, 0x5c, 0x7c, + 0xee, 0xa9, 0x25, 0x48, 0x76, 0x08, 0x49, 0xeb, 0xc1, 0xdc, 0x8d, 0xe9, 0x4a, 0x29, 0x19, 0xec, + 0x92, 0x10, 0x67, 0x29, 0x31, 0x38, 0xd9, 0x9f, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, + 0x83, 0x47, 0x72, 0x8c, 0x51, 0x86, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, + 0xfa, 0x89, 0x45, 0xe9, 0xf9, 0x05, 0x45, 0xf9, 0x59, 0x60, 0x86, 0x6e, 0x72, 0x8a, 0x7e, 0x99, + 0x91, 0x3e, 0x66, 0x98, 0x25, 0xb1, 0x81, 0x03, 0xcb, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x5a, + 0x1e, 0xa9, 0xaf, 0x50, 0x01, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// AskPassServiceClient is the client API for AskPassService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type AskPassServiceClient interface { + GetCredentials(ctx context.Context, in *CredentialsRequest, opts ...grpc.CallOption) (*CredentialsResponse, error) +} + +type askPassServiceClient struct { + cc *grpc.ClientConn +} + +func NewAskPassServiceClient(cc *grpc.ClientConn) AskPassServiceClient { + return &askPassServiceClient{cc} +} + +func (c *askPassServiceClient) GetCredentials(ctx context.Context, in *CredentialsRequest, opts ...grpc.CallOption) (*CredentialsResponse, error) { + out := new(CredentialsResponse) + err := c.cc.Invoke(ctx, "/askpass.AskPassService/GetCredentials", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AskPassServiceServer is the server API for AskPassService service. +type AskPassServiceServer interface { + GetCredentials(context.Context, *CredentialsRequest) (*CredentialsResponse, error) +} + +// UnimplementedAskPassServiceServer can be embedded to have forward compatible implementations. +type UnimplementedAskPassServiceServer struct { +} + +func (*UnimplementedAskPassServiceServer) GetCredentials(ctx context.Context, req *CredentialsRequest) (*CredentialsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCredentials not implemented") +} + +func RegisterAskPassServiceServer(s *grpc.Server, srv AskPassServiceServer) { + s.RegisterService(&_AskPassService_serviceDesc, srv) +} + +func _AskPassService_GetCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CredentialsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AskPassServiceServer).GetCredentials(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/askpass.AskPassService/GetCredentials", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AskPassServiceServer).GetCredentials(ctx, req.(*CredentialsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _AskPassService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "askpass.AskPassService", + HandlerType: (*AskPassServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetCredentials", + Handler: _AskPassService_GetCredentials_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "reposerver/askpass/askpass.proto", +} + +func (m *CredentialsRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CredentialsRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CredentialsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Nonce) > 0 { + i -= len(m.Nonce) + copy(dAtA[i:], m.Nonce) + i = encodeVarintAskpass(dAtA, i, uint64(len(m.Nonce))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *CredentialsResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CredentialsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CredentialsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Password) > 0 { + i -= len(m.Password) + copy(dAtA[i:], m.Password) + i = encodeVarintAskpass(dAtA, i, uint64(len(m.Password))) + i-- + dAtA[i] = 0x12 + } + if len(m.Username) > 0 { + i -= len(m.Username) + copy(dAtA[i:], m.Username) + i = encodeVarintAskpass(dAtA, i, uint64(len(m.Username))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintAskpass(dAtA []byte, offset int, v uint64) int { + offset -= sovAskpass(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *CredentialsRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Nonce) + if l > 0 { + n += 1 + l + sovAskpass(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *CredentialsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Username) + if l > 0 { + n += 1 + l + sovAskpass(uint64(l)) + } + l = len(m.Password) + if l > 0 { + n += 1 + l + sovAskpass(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovAskpass(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozAskpass(x uint64) (n int) { + return sovAskpass(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *CredentialsRequest) 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 ErrIntOverflowAskpass + } + 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: CredentialsRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CredentialsRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAskpass + } + 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 ErrInvalidLengthAskpass + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAskpass + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Nonce = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAskpass(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAskpass + } + 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 (m *CredentialsResponse) 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 ErrIntOverflowAskpass + } + 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: CredentialsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CredentialsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAskpass + } + 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 ErrInvalidLengthAskpass + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAskpass + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Username = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Password", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAskpass + } + 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 ErrInvalidLengthAskpass + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAskpass + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Password = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAskpass(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAskpass + } + 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 skipAskpass(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAskpass + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAskpass + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowAskpass + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthAskpass + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupAskpass + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthAskpass + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthAskpass = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowAskpass = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupAskpass = fmt.Errorf("proto: unexpected end of group") +) diff --git a/reposerver/askpass/askpass.proto b/reposerver/askpass/askpass.proto new file mode 100644 index 0000000000000..4547edc3a0306 --- /dev/null +++ b/reposerver/askpass/askpass.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; +option go_package = "github.com/argoproj/argo-cd/v2/reposerver/askpass"; + +package askpass; + +message CredentialsRequest { + string nonce = 1; +} + +message CredentialsResponse { + string username = 1; + string password = 2; +} + + +// AskPassService +service AskPassService { + rpc GetCredentials(CredentialsRequest) returns (CredentialsResponse) { + } +} diff --git a/reposerver/askpass/common.go b/reposerver/askpass/common.go new file mode 100644 index 0000000000000..3f30cc0f2b0f8 --- /dev/null +++ b/reposerver/askpass/common.go @@ -0,0 +1,10 @@ +package askpass + +var ( + SocketPath = "/tmp/reposerver-ask-pass.sock" +) + +type Creds struct { + Username string + Password string +} diff --git a/reposerver/askpass/server.go b/reposerver/askpass/server.go new file mode 100644 index 0000000000000..c34e3c332890d --- /dev/null +++ b/reposerver/askpass/server.go @@ -0,0 +1,90 @@ +package askpass + +import ( + "context" + "net" + "os" + "sync" + + "github.com/google/uuid" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/argoproj/argo-cd/v2/util/git" + "github.com/argoproj/argo-cd/v2/util/io" +) + +type Server interface { + git.CredsStore + AskPassServiceServer + Run(path string) error +} + +type server struct { + lock sync.Mutex + creds map[string]Creds +} + +// NewServer returns a new server +func NewServer() *server { + return &server{ + creds: make(map[string]Creds), + } +} + +func (s *server) GetCredentials(_ context.Context, q *CredentialsRequest) (*CredentialsResponse, error) { + if q.Nonce == "" { + return nil, status.Errorf(codes.InvalidArgument, "missing nonce") + } + creds, ok := s.getCreds(q.Nonce) + if !ok { + return nil, status.Errorf(codes.NotFound, "unknown nonce") + } + return &CredentialsResponse{Username: creds.Username, Password: creds.Password}, nil +} + +func (s *server) Start(path string) (io.Closer, error) { + _ = os.Remove(path) + listener, err := net.Listen("unix", path) + if err != nil { + return nil, err + } + server := grpc.NewServer() + RegisterAskPassServiceServer(server, s) + go func() { + _ = server.Serve(listener) + }() + return io.NewCloser(listener.Close), nil +} + +func (s *server) Run(path string) error { + _, err := s.Start(path) + return err +} + +// Add adds a new credential to the server and returns associated id +func (s *server) Add(username string, password string) string { + s.lock.Lock() + defer s.lock.Unlock() + id := uuid.New().String() + s.creds[id] = Creds{ + Username: username, + Password: password, + } + return id +} + +// Remove removes the credential with the given id +func (s *server) Remove(id string) { + s.lock.Lock() + defer s.lock.Unlock() + delete(s.creds, id) +} + +func (s *server) getCreds(id string) (*Creds, bool) { + s.lock.Lock() + defer s.lock.Unlock() + creds, ok := s.creds[id] + return &creds, ok +} diff --git a/reposerver/askpass/server_test.go b/reposerver/askpass/server_test.go new file mode 100644 index 0000000000000..311592d7f0aa7 --- /dev/null +++ b/reposerver/askpass/server_test.go @@ -0,0 +1,25 @@ +package askpass + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdd(t *testing.T) { + s := NewServer() + nonce := s.Add("foo", "bar") + + assert.Equal(t, "foo", s.creds[nonce].Username) + assert.Equal(t, "bar", s.creds[nonce].Password) +} + +func TestRemove(t *testing.T) { + s := NewServer() + s.creds["some-id"] = Creds{Username: "foo"} + + s.Remove("some-id") + + _, ok := s.creds["some-id"] + assert.False(t, ok) +} diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index aca744ddaaa33..82cb9fa18df7b 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -67,6 +67,7 @@ const ( // Service implements ManifestService interface type Service struct { + gitCredsStore git.CredsStore repoLock *repositoryLock cache *reposervercache.Cache parallelismLimitSemaphore *semaphore.Weighted @@ -88,7 +89,7 @@ type RepoServerInitConstants struct { } // NewService returns a new instance of the Manifest service -func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, initConstants RepoServerInitConstants, resourceTracking argo.ResourceTracking) *Service { +func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, initConstants RepoServerInitConstants, resourceTracking argo.ResourceTracking, gitCredsStore git.CredsStore) *Service { var parallelismLimitSemaphore *semaphore.Weighted if initConstants.ParallelismLimit > 0 { parallelismLimitSemaphore = semaphore.NewWeighted(initConstants.ParallelismLimit) @@ -106,6 +107,7 @@ func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cac }, initConstants: initConstants, now: time.Now, + gitCredsStore: gitCredsStore, } } @@ -329,7 +331,7 @@ func (s *Service) runManifestGen(ctx context.Context, repoRoot, commitSHA, cache var manifestGenResult *apiclient.ManifestResponse opContext, err := opContextSrc() if err == nil { - manifestGenResult, err = GenerateManifests(ctx, opContext.appPath, repoRoot, commitSHA, q, false) + manifestGenResult, err = GenerateManifests(ctx, opContext.appPath, repoRoot, commitSHA, q, false, s.gitCredsStore) } if err != nil { @@ -866,7 +868,7 @@ func getRepoCredential(repoCredentials []*v1alpha1.RepoCreds, repoURL string) *v } // GenerateManifests generates manifests from a path -func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool) (*apiclient.ManifestResponse, error) { +func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore) (*apiclient.ManifestResponse, error) { var targetObjs []*unstructured.Unstructured var dest *v1alpha1.ApplicationDestination @@ -891,13 +893,13 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, if q.KustomizeOptions != nil { kustomizeBinary = q.KustomizeOptions.BinaryPath } - k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(), repoURL, kustomizeBinary) + k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary) targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions, env) case v1alpha1.ApplicationSourceTypePlugin: if q.ApplicationSource.Plugin != nil && q.ApplicationSource.Plugin.Name != "" { - targetObjs, err = runConfigManagementPlugin(appPath, repoRoot, env, q, q.Repo.GetGitCreds()) + targetObjs, err = runConfigManagementPlugin(appPath, repoRoot, env, q, q.Repo.GetGitCreds(gitCredsStore)) } else { - targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, env, q, q.Repo.GetGitCreds()) + targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, env, q, q.Repo.GetGitCreds(gitCredsStore)) if err != nil { err = fmt.Errorf("plugin sidecar failed. %s", err.Error()) } @@ -1419,7 +1421,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD return err } case v1alpha1.ApplicationSourceTypeKustomize: - if err := populateKustomizeAppDetails(res, q, opContext.appPath, commitSHA); err != nil { + if err := populateKustomizeAppDetails(res, q, opContext.appPath, commitSHA, s.gitCredsStore); err != nil { return err } } @@ -1566,13 +1568,13 @@ func findHelmValueFilesInPath(path string) ([]string, error) { return result, nil } -func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, appPath string, reversion string) error { +func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, appPath string, reversion string, credsStore git.CredsStore) error { res.Kustomize = &apiclient.KustomizeAppSpec{} kustomizeBinary := "" if q.KustomizeOptions != nil { kustomizeBinary = q.KustomizeOptions.BinaryPath } - k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(), q.Repo.Repo, kustomizeBinary) + k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary) fakeManifestRequest := apiclient.ManifestRequest{ AppName: q.AppName, Namespace: "", // FIXME: omit it for now @@ -1674,7 +1676,7 @@ func fileParameters(q *apiclient.RepoServerAppDetailsQuery) []v1alpha1.HelmFileP func (s *Service) newClient(repo *v1alpha1.Repository, opts ...git.ClientOpts) (git.Client, error) { opts = append(opts, git.WithEventHandlers(metrics.NewGitClientEventHandlers(s.metricsServer))) - return s.newGitClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.EnableLFS, repo.Proxy, opts...) + return s.newGitClient(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.EnableLFS, repo.Proxy, opts...) } // newClientResolveRevision is a helper to perform the common task of instantiating a git client @@ -1776,7 +1778,7 @@ func (s *Service) TestRepository(ctx context.Context, q *apiclient.TestRepositor } checks := map[string]func() error{ "git": func() error { - return git.TestRepo(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy) + return git.TestRepo(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy) }, "helm": func() error { if repo.EnableOCI { @@ -1834,7 +1836,7 @@ func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevis AmbiguousRevision: fmt.Sprintf("%v (%v)", ambiguousRevision, version.String()), }, nil } else { - gitClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy) + gitClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy) if err != nil { return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err } diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go index f214733e7513a..6c8162f4cdb6e 100644 --- a/reposerver/repository/repository_test.go +++ b/reposerver/repository/repository_test.go @@ -72,7 +72,7 @@ func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) { cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)), 1*time.Minute, 1*time.Minute, - ), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking()) + ), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}) chart := "my-chart" version := "1.1.0" @@ -140,7 +140,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) { assert.Equal(t, countOfManifests, len(res1.Manifests)) // this will test concatenated manifests to verify we split YAMLs correctly - res2, err := GenerateManifests(context.Background(), "./testdata/concatenated", "/", "", &q, false) + res2, err := GenerateManifests(context.Background(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}) assert.NoError(t, err) assert.Equal(t, 3, len(res2.Manifests)) } @@ -1050,9 +1050,8 @@ func TestRunCustomTool(t *testing.T) { assert.Equal(t, obj.GetName(), "test-app") assert.Equal(t, obj.GetNamespace(), "test-namespace") - assert.Equal(t, "git-ask-pass.sh", obj.GetAnnotations()["GIT_ASKPASS"]) - assert.Equal(t, "foo", obj.GetAnnotations()["GIT_USERNAME"]) - assert.Equal(t, "bar", obj.GetAnnotations()["GIT_PASSWORD"]) + assert.Empty(t, obj.GetAnnotations()["GIT_USERNAME"]) + assert.Empty(t, obj.GetAnnotations()["GIT_PASSWORD"]) // Git client is mocked, so the revision is always mock.Anything assert.Equal(t, map[string]string{"revision": "prefix-mock.Anything"}, obj.GetLabels()) } @@ -1062,7 +1061,7 @@ func TestGenerateFromUTF16(t *testing.T) { Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, } - res1, err := GenerateManifests(context.Background(), "./testdata/utf-16", "/", "", &q, false) + res1, err := GenerateManifests(context.Background(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}) assert.Nil(t, err) assert.Equal(t, 2, len(res1.Manifests)) } diff --git a/reposerver/server.go b/reposerver/server.go index ecc86ca95de65..fb2e861c3dc39 100644 --- a/reposerver/server.go +++ b/reposerver/server.go @@ -5,8 +5,6 @@ import ( "fmt" "os" - "github.com/argoproj/argo-cd/v2/util/argo" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" @@ -24,7 +22,9 @@ import ( "github.com/argoproj/argo-cd/v2/reposerver/metrics" "github.com/argoproj/argo-cd/v2/reposerver/repository" "github.com/argoproj/argo-cd/v2/server/version" + "github.com/argoproj/argo-cd/v2/util/argo" "github.com/argoproj/argo-cd/v2/util/env" + "github.com/argoproj/argo-cd/v2/util/git" grpc_util "github.com/argoproj/argo-cd/v2/util/grpc" tlsutil "github.com/argoproj/argo-cd/v2/util/tls" ) @@ -33,6 +33,7 @@ import ( type ArgoCDRepoServer struct { log *log.Entry metricsServer *metrics.MetricsServer + gitCredsStore git.CredsStore cache *reposervercache.Cache opts []grpc.ServerOption initConstants repository.RepoServerInitConstants @@ -42,7 +43,7 @@ type ArgoCDRepoServer struct { var tlsHostList []string = []string{"localhost", "reposerver"} // NewServer returns a new instance of the Argo CD Repo server -func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, tlsConfCustomizer tlsutil.ConfigCustomizer, initConstants repository.RepoServerInitConstants) (*ArgoCDRepoServer, error) { +func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, tlsConfCustomizer tlsutil.ConfigCustomizer, initConstants repository.RepoServerInitConstants, gitCredsStore git.CredsStore) (*ArgoCDRepoServer, error) { var tlsConfig *tls.Config // Generate or load TLS server certificates to use with this instance of @@ -85,6 +86,7 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach cache: cache, initConstants: initConstants, opts: serverOpts, + gitCredsStore: gitCredsStore, }, nil } @@ -94,7 +96,7 @@ func (a *ArgoCDRepoServer) CreateGRPC() *grpc.Server { versionpkg.RegisterVersionServiceServer(server, version.NewServer(nil, func() (bool, error) { return true, nil })) - manifestService := repository.NewService(a.metricsServer, a.cache, a.initConstants, argo.NewResourceTracking()) + manifestService := repository.NewService(a.metricsServer, a.cache, a.initConstants, argo.NewResourceTracking(), a.gitCredsStore) apiclient.RegisterRepoServerServiceServer(server, manifestService) healthService := health.NewServer() diff --git a/test/e2e/custom_tool_test.go b/test/e2e/custom_tool_test.go index dfeaec573b4b6..26adcbc4eac1e 100644 --- a/test/e2e/custom_tool_test.go +++ b/test/e2e/custom_tool_test.go @@ -26,7 +26,7 @@ func TestCustomToolWithGitCreds(t *testing.T) { Name: Name(), Generate: Command{ Command: []string{"sh", "-c"}, - Args: []string{`echo "{\"kind\": \"ConfigMap\", \"apiVersion\": \"v1\", \"metadata\": { \"name\": \"$ARGOCD_APP_NAME\", \"namespace\": \"$ARGOCD_APP_NAMESPACE\", \"annotations\": {\"GitAskpass\": \"$GIT_ASKPASS\", \"GitUsername\": \"$GIT_USERNAME\", \"GitPassword\": \"$GIT_PASSWORD\"}}}"`}, + Args: []string{`echo "{\"kind\": \"ConfigMap\", \"apiVersion\": \"v1\", \"metadata\": { \"name\": \"$ARGOCD_APP_NAME\", \"namespace\": \"$ARGOCD_APP_NAMESPACE\", \"annotations\": {\"GitAskpass\": \"$GIT_ASKPASS\"}}}"`}, }, }, ). @@ -45,17 +45,7 @@ func TestCustomToolWithGitCreds(t *testing.T) { And(func(app *Application) { output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitAskpass}") assert.NoError(t, err) - assert.Equal(t, "git-ask-pass.sh", output) - }). - And(func(app *Application) { - output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitUsername}") - assert.NoError(t, err) - assert.Equal(t, GitUsername, output) - }). - And(func(app *Application) { - output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitPassword}") - assert.NoError(t, err) - assert.Equal(t, GitPassword, output) + assert.Equal(t, "argocd", output) }) } @@ -89,17 +79,17 @@ func TestCustomToolWithGitCredsTemplate(t *testing.T) { And(func(app *Application) { output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitAskpass}") assert.NoError(t, err) - assert.Equal(t, "git-ask-pass.sh", output) + assert.Equal(t, "argocd", output) }). And(func(app *Application) { output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitUsername}") assert.NoError(t, err) - assert.Equal(t, GitUsername, output) + assert.Empty(t, output) }). And(func(app *Application) { output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitPassword}") assert.NoError(t, err) - assert.Equal(t, GitPassword, output) + assert.Empty(t, output) }) } diff --git a/test/fixture/path/hack_path.go b/test/fixture/path/hack_path.go index e15102224d365..41bf98961c3d1 100644 --- a/test/fixture/path/hack_path.go +++ b/test/fixture/path/hack_path.go @@ -16,10 +16,10 @@ func (h AddBinDirToPath) Close() { _ = os.Setenv("PATH", h.originalPath) } -// add the hack path which has the git-ask-pass.sh shell script +// add the hack path which has the argocd binary func NewBinDirToPath() AddBinDirToPath { originalPath := os.Getenv("PATH") - binDir, err := filepath.Abs("../../hack") + binDir, err := filepath.Abs("../../dist") errors.CheckError(err) err = os.Setenv("PATH", fmt.Sprintf("%s:%s", originalPath, binDir)) errors.CheckError(err) diff --git a/util/git/client.go b/util/git/client.go index 17b5ada889eee..cde2ad3cd4c50 100644 --- a/util/git/client.go +++ b/util/git/client.go @@ -623,7 +623,7 @@ func (m *nativeGitClient) runCredentialedCmd(command string, args ...string) err func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd) (string, error) { cmd.Dir = m.root - cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Env = append(os.Environ(), cmd.Env...) // Set $HOME to nowhere, so we can be execute Git regardless of any external // authentication keys (e.g. in ~/.ssh) -- this is especially important for // running tests on local machines and/or CircleCI. diff --git a/util/git/creds.go b/util/git/creds.go index 8e44209ede502..1d94325c7d693 100644 --- a/util/git/creds.go +++ b/util/git/creds.go @@ -11,22 +11,29 @@ import ( "strings" "time" - gocache "github.com/patrickmn/go-cache" - argoio "github.com/argoproj/gitops-engine/pkg/utils/io" + "github.com/argoproj/gitops-engine/pkg/utils/text" "github.com/bradleyfalzon/ghinstallation/v2" + gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" "github.com/argoproj/argo-cd/v2/common" - certutil "github.com/argoproj/argo-cd/v2/util/cert" + argoioutils "github.com/argoproj/argo-cd/v2/util/io" ) -// In memory cache for storing github APP api token credentials var ( + // In memory cache for storing github APP api token credentials githubAppTokenCache *gocache.Cache ) +const ( + // ASKPASS_NONCE_ENV is the environment variable that is used to pass the nonce to the askpass script + ASKPASS_NONCE_ENV = "ARGOCD_GIT_ASKPASS_NONCE" + // githubAccessTokenUsername is a username that is used to with the github access token + githubAccessTokenUsername = "x-access-token" +) + func init() { githubAppCredsExp := common.GithubAppCredsExpirationDuration if exp := os.Getenv(common.EnvGithubAppCredsExpirationDuration); exp != "" { @@ -38,10 +45,34 @@ func init() { githubAppTokenCache = gocache.New(githubAppCredsExp, 1*time.Minute) } +type NoopCredsStore struct { +} + +func (d NoopCredsStore) Add(username string, password string) string { + return "" +} + +func (d NoopCredsStore) Remove(id string) { +} + +type CredsStore interface { + Add(username string, password string) string + Remove(id string) +} + type Creds interface { Environ() (io.Closer, []string, error) } +func getGitAskPassEnv(id string) []string { + return []string{ + fmt.Sprintf("GIT_ASKPASS=%s", "argocd"), + fmt.Sprintf("%s=%s", ASKPASS_NONCE_ENV, id), + "GIT_TERMINAL_PROMPT=0", + "ARGOCD_BINARY_NAME=argocd-git-ask-pass", + } +} + // nop implementation type NopCloser struct { } @@ -84,9 +115,11 @@ type HTTPSCreds struct { clientCertKey string // HTTP/HTTPS proxy used to access repository proxy string + // temporal credentials store + store CredsStore } -func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool, proxy string) GenericHTTPSCreds { +func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool, proxy string, store CredsStore) GenericHTTPSCreds { return HTTPSCreds{ username, password, @@ -94,18 +127,15 @@ func NewHTTPSCreds(username string, password string, clientCertData string, clie clientCertData, clientCertKey, proxy, + store, } } // Get additional required environment variables for executing git client to // access specific repository via HTTPS. func (c HTTPSCreds) Environ() (io.Closer, []string, error) { - env := []string{fmt.Sprintf("GIT_ASKPASS=%s", "git-ask-pass.sh"), fmt.Sprintf("GIT_USERNAME=%s", c.username), fmt.Sprintf("GIT_PASSWORD=%s", c.password)} - if c.username != "" { - env = append(env, fmt.Sprintf("GIT_USERNAME=%s", c.username)) - } else { - env = append(env, fmt.Sprintf("GIT_USERNAME=%s", "x-access-token")) - } + var env []string + httpCloser := authFilePaths(make([]string, 0)) // GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at @@ -157,9 +187,13 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) { } // GIT_SSL_KEY is the full path to a client certificate's key to be used env = append(env, fmt.Sprintf("GIT_SSL_KEY=%s", keyFile.Name())) - } - return httpCloser, env, nil + nonce := c.store.Add(text.FirstNonEmpty(c.username, githubAccessTokenUsername), c.password) + env = append(env, getGitAskPassEnv(nonce)...) + return argoioutils.NewCloser(func() error { + c.store.Remove(nonce) + return httpCloser.Close() + }), env, nil } func (g HTTPSCreds) HasClientCert() bool { @@ -179,10 +213,11 @@ type SSHCreds struct { sshPrivateKey string caPath string insecure bool + store CredsStore } -func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool) SSHCreds { - return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey} +func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool, store CredsStore) SSHCreds { + return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey, store} } type sshPrivateKeyFile string @@ -249,11 +284,12 @@ type GitHubAppCreds struct { clientCertKey string insecure bool proxy string + store CredsStore } // NewGitHubAppCreds provide github app credentials -func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, repoURL string, clientCertData string, clientCertKey string, insecure bool) GenericHTTPSCreds { - return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, repoURL: repoURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure} +func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, repoURL string, clientCertData string, clientCertKey string, insecure bool, store CredsStore) GenericHTTPSCreds { + return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, repoURL: repoURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure, store: store} } func (g GitHubAppCreds) Environ() (io.Closer, []string, error) { @@ -261,8 +297,7 @@ func (g GitHubAppCreds) Environ() (io.Closer, []string, error) { if err != nil { return NopCloser{}, nil, err } - - env := []string{fmt.Sprintf("GIT_ASKPASS=%s", "git-ask-pass.sh"), "GIT_USERNAME=x-access-token", fmt.Sprintf("GIT_PASSWORD=%s", token)} + var env []string httpCloser := authFilePaths(make([]string, 0)) // GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at @@ -316,7 +351,12 @@ func (g GitHubAppCreds) Environ() (io.Closer, []string, error) { env = append(env, fmt.Sprintf("GIT_SSL_KEY=%s", keyFile.Name())) } - return httpCloser, env, nil + nonce := g.store.Add(githubAccessTokenUsername, token) + env = append(env, getGitAskPassEnv(nonce)...) + return argoioutils.NewCloser(func() error { + g.store.Remove(nonce) + return httpCloser.Close() + }), env, nil } // getAccessToken fetches GitHub token using the app id, install id, and private key. diff --git a/util/git/git_test.go b/util/git/git_test.go index 068b83395628e..1d36370ac3a92 100644 --- a/util/git/git_test.go +++ b/util/git/git_test.go @@ -146,7 +146,7 @@ func TestCustomHTTPClient(t *testing.T) { assert.NotEqual(t, "", string(keyData)) // Get HTTPSCreds with client cert creds specified, and insecure connection - creds := NewHTTPSCreds("test", "test", string(certData), string(keyData), false, "http://proxy:5000") + creds := NewHTTPSCreds("test", "test", string(certData), string(keyData), false, "http://proxy:5000", &NoopCredsStore{}) client := GetRepoHTTPClient("https://localhost:9443/foo/bar", false, creds, "http://proxy:5000") assert.NotNil(t, client) assert.NotNil(t, client.Transport) @@ -177,7 +177,7 @@ func TestCustomHTTPClient(t *testing.T) { }() // Get HTTPSCreds without client cert creds, but insecure connection - creds = NewHTTPSCreds("test", "test", "", "", true, "") + creds = NewHTTPSCreds("test", "test", "", "", true, "", &NoopCredsStore{}) client = GetRepoHTTPClient("https://localhost:9443/foo/bar", true, creds, "") assert.NotNil(t, client) assert.NotNil(t, client.Transport)