From dc9aa475e4ca4f9bbf3aa76f04629eab067692bc Mon Sep 17 00:00:00 2001 From: Josh Dolitsky Date: Mon, 7 Jun 2021 18:10:32 -0400 Subject: [PATCH] Add new LoginWithOpts Signed-off-by: Josh Dolitsky --- pkg/auth/client.go | 39 +---------- pkg/auth/client_opts.go | 111 +++++++++++++++++++++++++++++ pkg/auth/client_opts_test.go | 124 +++++++++++++++++++++++++++++++++ pkg/auth/client_test.go | 64 ----------------- pkg/auth/docker/client_test.go | 25 +++++++ pkg/auth/docker/login.go | 45 ++++++++++-- pkg/auth/docker/resolver.go | 1 + 7 files changed, 302 insertions(+), 107 deletions(-) create mode 100644 pkg/auth/client_opts.go create mode 100644 pkg/auth/client_opts_test.go delete mode 100644 pkg/auth/client_test.go diff --git a/pkg/auth/client.go b/pkg/auth/client.go index 5beacb5b..2c3be4a6 100644 --- a/pkg/auth/client.go +++ b/pkg/auth/client.go @@ -28,25 +28,13 @@ var ( ErrNotLoggedIn = errors.New("not logged in") ) -type ( - // ResolverOption allows specifying various settings on the resolver. - ResolverOption func(*ResolverSettings) - - // ResolverSettings represent all the various settings on a resolver. - ResolverSettings struct { - // Headers are the HTTP request header fields sent by the resolver. - Client *http.Client - // PlainHTTP specifies to use plain http and not https. - PlainHTTP bool - // Client is the http client to used when making registry requests. - Headers http.Header - } -) - // Client provides authentication operations for remotes. type Client interface { // Login logs in to a remote server identified by the hostname. + // Deprecated: use LoginWithOpts Login(ctx context.Context, hostname, username, secret string, insecure bool) error + // LoginWithOpts logs in to a remote server identified by the hostname with custom options + LoginWithOpts(options ...LoginOption) error // Logout logs out from a remote server identified by the hostname. Logout(ctx context.Context, hostname string) error // Resolver returns a new authenticated resolver. @@ -55,24 +43,3 @@ type Client interface { // ResolverWithOpts returns a new authenticated resolver with custom options. ResolverWithOpts(options ...ResolverOption) (remotes.Resolver, error) } - -// WithResolverClient returns a function that sets the Client setting on resolver. -func WithResolverClient(client *http.Client) ResolverOption { - return func(settings *ResolverSettings) { - settings.Client = client - } -} - -// WithResolverPlainHTTP returns a function that sets the PlainHTTP setting to true on resolver. -func WithResolverPlainHTTP() ResolverOption { - return func(settings *ResolverSettings) { - settings.PlainHTTP = true - } -} - -// WithResolverHeaders returns a function that sets the Headers setting on resolver. -func WithResolverHeaders(headers http.Header) ResolverOption { - return func(settings *ResolverSettings) { - settings.Headers = headers - } -} diff --git a/pkg/auth/client_opts.go b/pkg/auth/client_opts.go new file mode 100644 index 00000000..4f71454b --- /dev/null +++ b/pkg/auth/client_opts.go @@ -0,0 +1,111 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "net/http" +) + +type ( + // LoginOption allows specifying various settings on login. + LoginOption func(*LoginSettings) + + // LoginSettings represent all the various settings on login. + LoginSettings struct { + Context context.Context + Hostname string + Username string + Secret string + Insecure bool + UserAgent string + } +) + +// WithLoginContext returns a function that sets the Context setting on login. +func WithLoginContext(context context.Context) LoginOption { + return func(settings *LoginSettings) { + settings.Context = context + } +} + +// WithLoginHostname returns a function that sets the Hostname setting on login. +func WithLoginHostname(hostname string) LoginOption { + return func(settings *LoginSettings) { + settings.Hostname = hostname + } +} + +// WithLoginUsername returns a function that sets the Username setting on login. +func WithLoginUsername(username string) LoginOption { + return func(settings *LoginSettings) { + settings.Username = username + } +} + +// WithLoginSecret returns a function that sets the Secret setting on login. +func WithLoginSecret(secret string) LoginOption { + return func(settings *LoginSettings) { + settings.Secret = secret + } +} + +// WithLoginInsecure returns a function that sets the Insecure setting to true on login. +func WithLoginInsecure() LoginOption { + return func(settings *LoginSettings) { + settings.Insecure = true + } +} + +// WithLoginUserAgent returns a function that sets the UserAgent setting on login. +func WithLoginUserAgent(userAgent string) LoginOption { + return func(settings *LoginSettings) { + settings.UserAgent = userAgent + } +} + +type ( + // ResolverOption allows specifying various settings on the resolver. + ResolverOption func(*ResolverSettings) + + // ResolverSettings represent all the various settings on a resolver. + ResolverSettings struct { + Client *http.Client + PlainHTTP bool + Headers http.Header + } +) + +// WithResolverClient returns a function that sets the Client setting on resolver. +func WithResolverClient(client *http.Client) ResolverOption { + return func(settings *ResolverSettings) { + settings.Client = client + } +} + +// WithResolverPlainHTTP returns a function that sets the PlainHTTP setting to true on resolver. +func WithResolverPlainHTTP() ResolverOption { + return func(settings *ResolverSettings) { + settings.PlainHTTP = true + } +} + +// WithResolverHeaders returns a function that sets the Headers setting on resolver. +func WithResolverHeaders(headers http.Header) ResolverOption { + return func(settings *ResolverSettings) { + settings.Headers = headers + } +} diff --git a/pkg/auth/client_opts_test.go b/pkg/auth/client_opts_test.go new file mode 100644 index 00000000..cde8abe9 --- /dev/null +++ b/pkg/auth/client_opts_test.go @@ -0,0 +1,124 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" +) + +type ClientOptsSuite struct { + suite.Suite +} + +func (suite *ClientOptsSuite) TestWithLoginContext() { + settings := &LoginSettings{} + suite.Nil(settings.Context, "settings.Context is nil by default") + + ctx := context.Background() + opt := WithLoginContext(ctx) + opt(settings) + suite.Equal(ctx, settings.Context, "Able to override settings.Context") +} + +func (suite *ClientOptsSuite) TestWithLoginHostname() { + settings := &LoginSettings{} + suite.Equal("", settings.Hostname, "settings.Hostname is empty string by default") + + hostname := "example.com" + opt := WithLoginHostname(hostname) + opt(settings) + suite.Equal(hostname, settings.Hostname, "Able to override settings.Hostname") +} + +func (suite *ClientOptsSuite) TestWithLoginUsername() { + settings := &LoginSettings{} + suite.Equal("", settings.Username, "settings.Username is empty string by default") + + username := "fran" + opt := WithLoginUsername(username) + opt(settings) + suite.Equal(username, settings.Username, "Able to override settings.Username") +} + +func (suite *ClientOptsSuite) TestWithLoginSecret() { + settings := &LoginSettings{} + suite.Equal("", settings.Secret, "settings.Secret is empty string by default") + + secret := "shhhhhhhhhh" + opt := WithLoginSecret(secret) + opt(settings) + suite.Equal(secret, settings.Secret, "Able to override settings.Secret") +} + +func (suite *ClientOptsSuite) TestWithLoginInsecure() { + settings := &LoginSettings{} + suite.Equal(false, settings.Insecure, "settings.Insecure is false by default") + + opt := WithLoginInsecure() + opt(settings) + suite.Equal(true, settings.Insecure, "Able to override settings.Insecure") +} + +func (suite *ClientOptsSuite) TestWithLoginUserAgent() { + settings := &LoginSettings{} + suite.Equal("", settings.UserAgent, "settings.UserAgent is empty string by default") + + userAgent := "superclient" + opt := WithLoginUserAgent(userAgent) + opt(settings) + suite.Equal(userAgent, settings.UserAgent, "Able to override settings.UserAgent") +} + +func (suite *ClientOptsSuite) TestWithResolverClient() { + settings := &ResolverSettings{} + suite.Nil(settings.Client, "settings.Client is nil by default") + + defaultClient := http.DefaultClient + opt := WithResolverClient(defaultClient) + opt(settings) + suite.Equal(defaultClient, settings.Client, "Able to override settings.Client") +} + +func (suite *ClientOptsSuite) TestWithResolverPlainHTTP() { + settings := &ResolverSettings{} + suite.Equal(false, settings.PlainHTTP, "settings.PlainHTTP is false by default") + + plainHTTP := true + opt := WithResolverPlainHTTP() + opt(settings) + suite.Equal(plainHTTP, settings.PlainHTTP, "Able to override settings.PlainHTTP") +} + +func (suite *ClientOptsSuite) TestWithResolverHeaders() { + settings := &ResolverSettings{} + suite.Nil(settings.Headers, "settings.Headers is nil by default") + + key := "User-Agent" + value := "oras-go/test" + headers := http.Header{} + headers.Set(key, value) + opt := WithResolverHeaders(headers) + opt(settings) + suite.Equal(settings.Headers.Get(key), value, "Able to override settings.Headers") +} + +func TestClientOptsSuite(t *testing.T) { + suite.Run(t, new(ClientOptsSuite)) +} diff --git a/pkg/auth/client_test.go b/pkg/auth/client_test.go deleted file mode 100644 index 52400b55..00000000 --- a/pkg/auth/client_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package auth - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/suite" -) - -type ClientSuite struct { - suite.Suite -} - -func (suite *ClientSuite) TestWithResolverClient() { - settings := &ResolverSettings{} - suite.Nil(settings.Client, "settings.Client is nil by default") - - defaultClient := http.DefaultClient - opt := WithResolverClient(defaultClient) - opt(settings) - suite.Equal(defaultClient, settings.Client, "Able to override settings.Client") -} - -func (suite *ClientSuite) TestWithResolverPlainHTTP() { - settings := &ResolverSettings{} - suite.Equal(false, settings.PlainHTTP, "settings.PlainHTTP is false by default") - - plainHTTP := true - opt := WithResolverPlainHTTP() - opt(settings) - suite.Equal(plainHTTP, settings.PlainHTTP, "Able to override settings.PlainHTTP") -} - -func (suite *ClientSuite) TestWithResolverHeaders() { - settings := &ResolverSettings{} - suite.Nil(settings.Headers, "settings.Headers is nil by default") - - key := "User-Agent" - value := "oras-go/test" - headers := http.Header{} - headers.Set(key, value) - opt := WithResolverHeaders(headers) - opt(settings) - suite.Equal(settings.Headers.Get(key), value, "Able to override settings.Headers") -} - -func TestClientSuite(t *testing.T) { - suite.Run(t, new(ClientSuite)) -} diff --git a/pkg/auth/docker/client_test.go b/pkg/auth/docker/client_test.go index 5213616a..d64433f0 100644 --- a/pkg/auth/docker/client_test.go +++ b/pkg/auth/docker/client_test.go @@ -32,6 +32,8 @@ import ( "github.com/phayes/freeport" "github.com/stretchr/testify/suite" "golang.org/x/crypto/bcrypt" + + iface "github.com/oras-project/oras-go/pkg/auth" ) var ( @@ -128,6 +130,29 @@ func (suite *DockerClientTestSuite) Test_0_Login() { err = suite.Client.Login(newContext(), suite.DockerRegistryHost, testUsername, testPassword, false) suite.Nil(err, "no error logging into registry with valid credentials") } + +func (suite *DockerClientTestSuite) Test_1_LoginWithOpts() { + var err error + + opts := []iface.LoginOption{ + iface.WithLoginContext(newContext()), + iface.WithLoginHostname(suite.DockerRegistryHost), + iface.WithLoginUsername("oscar"), + iface.WithLoginSecret("opponent"), + } + err = suite.Client.LoginWithOpts(opts...) + suite.NotNil(err, "error logging into registry with invalid credentials (LoginWithOpts)") + + opts = []iface.LoginOption{ + iface.WithLoginContext(newContext()), + iface.WithLoginHostname(suite.DockerRegistryHost), + iface.WithLoginUsername(testUsername), + iface.WithLoginSecret(testPassword), + } + err = suite.Client.LoginWithOpts(opts...) + suite.Nil(err, "no error logging into registry with valid credentials (LoginWithOpts)") +} + func (suite *DockerClientTestSuite) Test_2_Logout() { var err error diff --git a/pkg/auth/docker/login.go b/pkg/auth/docker/login.go index d7efe509..a1f3b3fc 100644 --- a/pkg/auth/docker/login.go +++ b/pkg/auth/docker/login.go @@ -21,24 +21,47 @@ import ( ctypes "github.com/docker/cli/cli/config/types" "github.com/docker/docker/api/types" "github.com/docker/docker/registry" + + iface "github.com/oras-project/oras-go/pkg/auth" ) // Login logs in to a docker registry identified by the hostname. +// Deprecated: use LoginWithOpts func (c *Client) Login(ctx context.Context, hostname, username, secret string, insecure bool) error { - hostname = resolveHostname(hostname) + settings := &iface.LoginSettings{ + Context: ctx, + Hostname: hostname, + Username: username, + Secret: secret, + Insecure: insecure, + } + return c.login(settings) +} + +// LoginWithOpts logs in to a docker registry identified by the hostname with custom options. +func (c *Client) LoginWithOpts(options ...iface.LoginOption) error { + settings := &iface.LoginSettings{} + for _, option := range options { + option(settings) + } + return c.login(settings) +} + +func (c *Client) login(settings *iface.LoginSettings) error { + hostname := resolveHostname(settings.Hostname) cred := types.AuthConfig{ - Username: username, + Username: settings.Username, ServerAddress: hostname, } - if username == "" { - cred.IdentityToken = secret + if settings.Username == "" { + cred.IdentityToken = settings.Secret } else { - cred.Password = secret + cred.Password = settings.Secret } opts := registry.ServiceOptions{} - if insecure { + if settings.Insecure { opts.InsecureRegistries = []string{hostname} } @@ -47,7 +70,15 @@ func (c *Client) Login(ctx context.Context, hostname, username, secret string, i if err != nil { return err } - if _, token, err := remote.Auth(ctx, &cred, "oras"); err != nil { + ctx := settings.Context + if ctx == nil { + ctx = context.Background() + } + userAgent := settings.UserAgent + if userAgent == "" { + userAgent = "oras" + } + if _, token, err := remote.Auth(ctx, &cred, userAgent); err != nil { return err } else if token != "" { cred.Username = "" diff --git a/pkg/auth/docker/resolver.go b/pkg/auth/docker/resolver.go index 2584988a..84999400 100644 --- a/pkg/auth/docker/resolver.go +++ b/pkg/auth/docker/resolver.go @@ -23,6 +23,7 @@ import ( "github.com/containerd/containerd/remotes/docker" ctypes "github.com/docker/cli/cli/config/types" "github.com/docker/docker/registry" + iface "github.com/oras-project/oras-go/pkg/auth" )