Skip to content

Commit

Permalink
registry: Revoke API token on log out. (#1068)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewsomething authored Dec 7, 2021
1 parent f6469ed commit b1f1b24
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 11 deletions.
3 changes: 3 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ const (
// ArgGCExcludeUnreferencedBlobs indicates that a garbage collection should
// not delete unreferenced blobs.
ArgGCExcludeUnreferencedBlobs = "exclude-unreferenced-blobs"
// ArgRegistryAuthorizationServerEndpoint is the endpoint of the OAuth authorization server
// used to revoke credentials on logout.
ArgRegistryAuthorizationServerEndpoint = "authorization-server-endpoint"

// 1-Click Args

Expand Down
27 changes: 21 additions & 6 deletions commands/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ type dockerConfig struct {
} `json:"auths"`
}

// DOSecretOperatorAnnotation is the annotation key so that dosecret operator can do it's magic
// and help users pull private images automatically in their DOKS clusters
const DOSecretOperatorAnnotation = "digitalocean.com/dosecret-identifier"
const (
// DOSecretOperatorAnnotation is the annotation key so that dosecret operator can do it's magic
// and help users pull private images automatically in their DOKS clusters
DOSecretOperatorAnnotation = "digitalocean.com/dosecret-identifier"

oauthTokenRevokeEndpoint = "https://cloud.digitalocean.com/v1/oauth/revoke"
)

// Registry creates the registry command
func Registry() *Command {
Expand Down Expand Up @@ -80,8 +84,9 @@ func Registry() *Command {
"The length of time the registry credentials will be valid for in seconds. By default, the credentials do not expire.")

logoutRegDesc := "This command logs Docker out of the private container registry, revoking access to it."
CmdBuilder(cmd, RunRegistryLogout, "logout", "Log out Docker from a container registry",
cmdRunRegistryLogout := CmdBuilder(cmd, RunRegistryLogout, "logout", "Log out Docker from a container registry",
logoutRegDesc, Writer)
AddStringFlag(cmdRunRegistryLogout, doctl.ArgRegistryAuthorizationServerEndpoint, "", oauthTokenRevokeEndpoint, "The endpoint of the OAuth authorization server used to revoke credentials on logout.")

kubeManifestDesc := `This command outputs a YAML-formatted Kubernetes secret manifest that can be used to grant a Kubernetes cluster pull access to your private container registry.
Expand Down Expand Up @@ -486,12 +491,22 @@ func RunDockerConfig(c *CmdConfig) error {

// RunRegistryLogout logs Docker out of the registry
func RunRegistryLogout(c *CmdConfig) error {
endpoint, err := c.Doit.GetString(c.NS, doctl.ArgRegistryAuthorizationServerEndpoint)
if err != nil {
return err
}

server := c.Registry().Endpoint()
fmt.Printf("Removing login credentials for %s\n", server)

cf := dockerconf.LoadDefaultConfigFile(os.Stderr)
dockerCreds := cf.GetCredentialsStore(server)
err := dockerCreds.Erase(server)
authConfig, err := dockerCreds.Get(server)
if err != nil {
return err
}

err = dockerCreds.Erase(server)
if err != nil {
_, isSnap := os.LookupEnv("SNAP")
if os.IsPermission(err) && isSnap {
Expand All @@ -502,7 +517,7 @@ func RunRegistryLogout(c *CmdConfig) error {
return err
}

return nil
return c.Registry().RevokeOAuthToken(authConfig.Password, endpoint)
}

// Repository Run Commands
Expand Down
3 changes: 3 additions & 0 deletions commands/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/digitalocean/doctl/do"
"github.com/digitalocean/doctl/do/mocks"
"github.com/digitalocean/godo"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
k8sapiv1 "k8s.io/api/core/v1"
k8sscheme "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -538,7 +539,9 @@ func TestRegistryLogin(t *testing.T) {

func TestRegistryLogout(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
config.Doit.Set(config.NS, doctl.ArgRegistryAuthorizationServerEndpoint, "http://example.com")
tm.registry.EXPECT().Endpoint().Return(do.RegistryHostname)
tm.registry.EXPECT().RevokeOAuthToken(gomock.Any(), "http://example.com").Times(1).Return(nil)

config.Out = os.Stderr
err := RunRegistryLogout(config)
Expand Down
14 changes: 14 additions & 0 deletions do/mocks/RegistryService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions do/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ package do

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/digitalocean/godo"
)
Expand Down Expand Up @@ -69,6 +73,7 @@ type RegistryService interface {
ListGarbageCollections(string) ([]GarbageCollection, error)
CancelGarbageCollection(string, string) (*GarbageCollection, error)
GetSubscriptionTiers() ([]RegistrySubscriptionTier, error)
RevokeOAuthToken(token string, endpoint string) error
}

type registryService struct {
Expand Down Expand Up @@ -262,3 +267,26 @@ func (rs *registryService) GetSubscriptionTiers() ([]RegistrySubscriptionTier, e

return ret, nil
}

func (rs *registryService) RevokeOAuthToken(token string, endpoint string) error {
data := url.Values{}
data.Set("token", token)
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := http.Client{}

resp, err := client.Do(req)
if resp == nil {
return err
}

if resp.StatusCode != http.StatusOK {
return errors.New("error revoking token: " + http.StatusText(resp.StatusCode))
}

return err
}
4 changes: 2 additions & 2 deletions integration/registry_kubernetes_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ var _ = suite("registry/kubernetes-manifest", func(t *testing.T, when spec.G, it
})

const (
registryDockerCredentialsResponse = `{"auths":{"registry.digitalocean.com":{"auth":"YjdkMDNhNjk0N2IyMTdlZmI2ZjNlYzNiZDM1MDQ1ODI6YjdkMDNhNjk0N2IyMTdlZmI2ZjNlYzNiZDM1MDQ1ODIK"}}}`
registryDockerCredentialsResponse = `{"auths":{"registry.digitalocean.com":{"auth":"OGY4NzJlYWZjNTJmMTczODdkYTU2ZTUyZTgxMGMwYTYwMGM5ZjE2MzRjYTgxZjVhMDgzNmY3MTJiZjZiMzFlYzo4Zjg3MmVhZmM1MmYxNzM4N2RhNTZlNTJlODEwYzBhNjAwYzlmMTYzNGNhODFmNWEwODM2ZjcxMmJmNmIzMWVj"}}}`
registryKubernetesManifestOutput = `
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5kaWdpdGFsb2NlYW4uY29tIjp7ImF1dGgiOiJZamRrTUROaE5qazBOMkl5TVRkbFptSTJaak5sWXpOaVpETTFNRFExT0RJNllqZGtNRE5oTmprME4ySXlNVGRsWm1JMlpqTmxZek5pWkRNMU1EUTFPRElLIn19fQ==
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5kaWdpdGFsb2NlYW4uY29tIjp7ImF1dGgiOiJPR1k0TnpKbFlXWmpOVEptTVRjek9EZGtZVFUyWlRVeVpUZ3hNR013WVRZd01HTTVaakUyTXpSallUZ3haalZoTURnek5tWTNNVEppWmpaaU16RmxZem80WmpnM01tVmhabU0xTW1ZeE56TTROMlJoTlRabE5USmxPREV3WXpCaE5qQXdZemxtTVRZek5HTmhPREZtTldFd09ETTJaamN4TW1KbU5tSXpNV1ZqIn19fQ==
kind: Secret
metadata:
creationTimestamp: null
Expand Down
29 changes: 26 additions & 3 deletions integration/registry_logout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (

var _ = suite("registry/logout", func(t *testing.T, when spec.G, it spec.S) {
var (
expect *require.Assertions
server *httptest.Server
expect *require.Assertions
server *httptest.Server
oAuthServer *httptest.Server
)

it.Before(func() {
Expand All @@ -36,6 +37,27 @@ var _ = suite("registry/logout", func(t *testing.T, when spec.G, it spec.S) {
t.Fatalf("received unknown request: %s", dump)

}))

oAuthServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if http.MethodPost != req.Method {
t.Fatalf("method = %v, expected %v", req.Method, http.MethodPost)
}

authHeader := req.Header.Get("Authorization")
token := strings.TrimPrefix(strings.ToLower(authHeader), "bearer ")
if token == "" {
t.Fatalf("no token in auth header")
}

req.ParseForm()
bodyToken := req.Form.Get("token")
if token != bodyToken {
t.Fatalf("expected tokens to match: body = %v, header %v", bodyToken, token)
}

w.WriteHeader(http.StatusOK)

}))
})

it("removes the registry from the docker config.json file", func() {
Expand All @@ -51,12 +73,13 @@ var _ = suite("registry/logout", func(t *testing.T, when spec.G, it spec.S) {
"-u", server.URL,
"registry",
"logout",
"--authorization-server-endpoint", oAuthServer.URL,
)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONFIG=%s", tmpDir))

output, err := cmd.CombinedOutput()
expect.NoError(err)
expect.NoError(err, string(output))

fileBytes, err := ioutil.ReadFile(config)
expect.NoError(err)
Expand Down

0 comments on commit b1f1b24

Please sign in to comment.