diff --git a/go.mod b/go.mod index 981e06e..981897d 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/docker/go-units v0.4.0 github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect + github.com/google/uuid v1.1.2 github.com/gorilla/mux v1.8.0 // indirect github.com/jinzhu/gorm v1.9.16 // indirect github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a diff --git a/internal/hub/client.go b/internal/hub/client.go index a767d13..e3f46dd 100644 --- a/internal/hub/client.go +++ b/internal/hub/client.go @@ -159,6 +159,7 @@ func (c *Client) login(hubBaseURL string, hubAuthConfig types.AuthConfig) (strin func (c *Client) doRequest(req *http.Request, reqOps ...RequestOp) ([]byte, error) { req.Header["Accept"] = []string{"application/json"} + req.Header["Content-Type"] = []string{"application/json"} req.Header["User-Agent"] = []string{fmt.Sprintf("hub-tool/%s", internal.Version)} for _, op := range reqOps { if err := op(req); err != nil { diff --git a/internal/hub/tags.go b/internal/hub/tags.go index 140df07..a91b751 100644 --- a/internal/hub/tags.go +++ b/internal/hub/tags.go @@ -80,7 +80,7 @@ func (c *Client) GetTags(repository string, reqOps ...RequestOp) ([]Tag, error) } if c.fetchAllElements { for next != "" { - pageTags, n, err := c.getTagsPage(next, repository) + pageTags, n, err := c.getTagsPage(next, repository, reqOps...) if err != nil { return nil, err } diff --git a/internal/hub/tokens.go b/internal/hub/tokens.go new file mode 100644 index 0000000..a975c3a --- /dev/null +++ b/internal/hub/tokens.go @@ -0,0 +1,230 @@ +/* + Copyright 2020 Docker Hub Tool 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 hub + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/google/uuid" +) + +const ( + // TokensURL path to the Hub API listing the Personal Access Tokens + TokensURL = "/v2/api_tokens" + // TokenURL path to the Hub API Personal Access Token + TokenURL = "/v2/api_tokens/%s" +) + +//Token is a personal access token. The token field will only be filled at creation and can never been accessed again. +type Token struct { + UUID uuid.UUID + ClientID string + CreatorIP string + CreatorUA string + CreatedAt time.Time + LastUsed time.Time + GeneratedBy string + IsActive bool + Token string + Description string +} + +// CreateToken creates a Personal Access Token and returns the token field only once +func (c *Client) CreateToken(description string) (*Token, error) { + data, err := json.Marshal(hubTokenRequest{Description: description}) + if err != nil { + return nil, err + } + body := bytes.NewBuffer(data) + req, err := http.NewRequest("POST", c.domain+TokensURL, body) + if err != nil { + return nil, err + } + response, err := c.doRequest(req, WithHubToken(c.token)) + if err != nil { + return nil, err + } + var tokenResponse hubTokenResult + if err := json.Unmarshal(response, &tokenResponse); err != nil { + return nil, err + } + token, err := convertToken(tokenResponse) + if err != nil { + return nil, err + } + return &token, nil +} + +//GetTokens calls the hub repo API and returns all the information on all tokens +func (c *Client) GetTokens() ([]Token, error) { + u, err := url.Parse(c.domain + TokensURL) + if err != nil { + return nil, err + } + q := url.Values{} + q.Add("page_size", fmt.Sprintf("%v", itemsPerPage)) + q.Add("page", "1") + u.RawQuery = q.Encode() + + tokens, next, err := c.getTokensPage(u.String()) + if err != nil { + return nil, err + } + if c.fetchAllElements { + for next != "" { + pageTokens, n, err := c.getTokensPage(next) + if err != nil { + return nil, err + } + next = n + tokens = append(tokens, pageTokens...) + } + } + + return tokens, nil +} + +//GetToken calls the hub repo API and returns the information on one token +func (c *Client) GetToken(tokenUUID string) (*Token, error) { + req, err := http.NewRequest("GET", c.domain+fmt.Sprintf(TokenURL, tokenUUID), nil) + if err != nil { + return nil, err + } + response, err := c.doRequest(req, WithHubToken(c.token)) + if err != nil { + return nil, err + } + var tokenResponse hubTokenResult + if err := json.Unmarshal(response, &tokenResponse); err != nil { + return nil, err + } + token, err := convertToken(tokenResponse) + if err != nil { + return nil, err + } + return &token, nil +} + +// UpdateToken updates a token's description and activeness +func (c *Client) UpdateToken(tokenUUID, description string, isActive bool) (*Token, error) { + data, err := json.Marshal(hubTokenRequest{Description: description, IsActive: isActive}) + if err != nil { + return nil, err + } + body := bytes.NewBuffer(data) + req, err := http.NewRequest("PATCH", c.domain+fmt.Sprintf(TokenURL, tokenUUID), body) + if err != nil { + return nil, err + } + response, err := c.doRequest(req, WithHubToken(c.token)) + if err != nil { + return nil, err + } + var tokenResponse hubTokenResult + if err := json.Unmarshal(response, &tokenResponse); err != nil { + return nil, err + } + token, err := convertToken(tokenResponse) + if err != nil { + return nil, err + } + return &token, nil +} + +//RevokeToken revoke a token from personal access token +func (c *Client) RevokeToken(tokenUUID string) error { + //DELETE https://hub.docker.com/v2/api_tokens/8208674e-d08a-426f-b6f4-e3aba7058459 => 202 + req, err := http.NewRequest("DELETE", c.domain+fmt.Sprintf(TokenURL, tokenUUID), nil) + if err != nil { + return err + } + _, err = c.doRequest(req, WithHubToken(c.token)) + return err +} + +func (c *Client) getTokensPage(url string) ([]Token, string, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, "", err + } + response, err := c.doRequest(req, WithHubToken(c.token)) + if err != nil { + return nil, "", err + } + var hubResponse hubTokenResponse + if err := json.Unmarshal(response, &hubResponse); err != nil { + return nil, "", err + } + var tokens []Token + for _, result := range hubResponse.Results { + token, err := convertToken(result) + if err != nil { + return nil, "", err + } + tokens = append(tokens, token) + } + return tokens, hubResponse.Next, nil +} + +type hubTokenRequest struct { + Description string `json:"token_label,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type hubTokenResponse struct { + Count int `json:"count"` + Next string `json:"next,omitempty"` + Previous string `json:"previous,omitempty"` + Results []hubTokenResult `json:"results,omitempty"` +} + +type hubTokenResult struct { + UUID string `json:"uuid"` + ClientID string `json:"client_id"` + CreatorIP string `json:"creator_ip"` + CreatorUA string `json:"creator_ua"` + CreatedAt time.Time `json:"created_at"` + LastUsed time.Time `json:"last_used,omitempty"` + GeneratedBy string `json:"generated_by"` + IsActive bool `json:"is_active"` + Token string `json:"token"` + TokenLabel string `json:"token_label"` +} + +func convertToken(response hubTokenResult) (Token, error) { + u, err := uuid.Parse(response.UUID) + if err != nil { + return Token{}, err + } + return Token{ + UUID: u, + ClientID: response.ClientID, + CreatorIP: response.CreatorIP, + CreatorUA: response.CreatorUA, + CreatedAt: response.CreatedAt, + LastUsed: response.LastUsed, + GeneratedBy: response.GeneratedBy, + IsActive: response.IsActive, + Token: response.Token, + Description: response.TokenLabel, + }, nil +}