Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(account): token auth and management support #354

Merged
merged 19 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]

### Added
- Added support for token-based authentication in the client, including functions for token management.
paketeserrano marked this conversation as resolved.
Show resolved Hide resolved
- managed load balancer: support for redirect rule HTTP status
kangasta marked this conversation as resolved.
Show resolved Hide resolved

## [8.14.0]
Expand Down
28 changes: 19 additions & 9 deletions examples/upcloud-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (
"github.com/davecgh/go-spew/spew"
)

var username, password string
var username, password, token string

func init() {
flag.StringVar(&username, "username", "", "UpCloud username")
flag.StringVar(&password, "password", "", "UpCloud password")
flag.StringVar(&password, "token", "", "UpCloud API token")
}

func main() {
Expand All @@ -34,21 +35,30 @@ func run() int {
if username == "" {
username = os.Getenv("UPCLOUD_USERNAME")
}
if token == "" {
token = os.Getenv("UPCLOUD_TOKEN")
}

command := flag.Arg(0)

if len(username) == 0 {
fmt.Fprintln(os.Stderr, "Username must be specified")
return 1
}
var authCfg client.ConfigFn
if len(token) > 0 {
authCfg = client.WithBearerAuth(token)
} else {
if len(username) == 0 {
fmt.Fprintln(os.Stderr, "Username or token must be specified")
return 1
}

if len(password) == 0 {
fmt.Fprintln(os.Stderr, "Password must be specified")
return 2
if len(password) == 0 {
fmt.Fprintln(os.Stderr, "Password or token must be specified")
return 2
}
authCfg = client.WithBasicAuth(username, password)
}

fmt.Println("Creating new client")
c := client.New(username, password)
c := client.New("", "", authCfg)
paketeserrano marked this conversation as resolved.
Show resolved Hide resolved
s := service.New(c)

switch command {
Expand Down
27 changes: 26 additions & 1 deletion upcloud/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type LogFn func(context.Context, string, ...any)
type config struct {
username string
password string
token string
baseURL string
httpClient *http.Client
logger LogFn
Expand Down Expand Up @@ -152,7 +153,11 @@ func (c *Client) addDefaultHeaders(r *http.Request) {
r.Header.Set(userAgent, c.UserAgent)
}
if _, ok := r.Header[authorization]; !ok && strings.HasPrefix(r.URL.String(), c.config.baseURL) {
r.SetBasicAuth(c.config.username, c.config.password)
if c.config.token != "" {
r.Header.Set(authorization, "Bearer "+c.config.token)
} else {
r.SetBasicAuth(c.config.username, c.config.password)
}
}
}

Expand Down Expand Up @@ -248,6 +253,24 @@ func WithHTTPClient(httpClient *http.Client) ConfigFn {
}
}

// WithBasicAuth configures the client to use basic auth credentials for authentication
func WithBasicAuth(username, password string) ConfigFn {
return func(c *config) {
c.username = username
c.password = password
c.token = ""
}
}

// WithBearerAuth (EXPERIMENTAL) configures the client to use bearer token for authentication
func WithBearerAuth(apiToken string) ConfigFn {
return func(c *config) {
c.token = apiToken
c.username = ""
c.password = ""
}
}

// WithTimeout modifies the client's httpClient timeout
func WithTimeout(timeout time.Duration) ConfigFn {
return func(c *config) {
Expand All @@ -264,6 +287,8 @@ func WithLogger(logger LogFn) ConfigFn {

// New creates and returns a new client configured with the specified user and password and optional
// config functions.
// TODO: we should get rid of username, password here, but it's a breaking change. Credentials can be now set with
// configurators client.WithBasicAuth("user", "pass") or client.WithBearerAuth("ucat_token")
func New(username, password string, c ...ConfigFn) *Client {
config := config{
username: username,
Expand Down
57 changes: 57 additions & 0 deletions upcloud/request/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package request

import (
"fmt"
"time"
)

const basePath = "/account/tokens"

// GetTokenDetailsRequest (EXPERIMENTAL) represents a request to get token details. Will not return the actual API token.
type GetTokenDetailsRequest struct {
ID string
}

// RequestURL (EXPERIMENTAL) implements the Request interface.
func (r *GetTokenDetailsRequest) RequestURL() string {
return fmt.Sprintf("%s/%s", basePath, r.ID)
}

// GetTokensRequest (EXPERIMENTAL) represents a request to get a list of tokens. Will not return the actual API tokens.
type GetTokensRequest struct {
Page *Page
}

// RequestURL (EXPERIMENTAL) implements the Request interface.
func (r *GetTokensRequest) RequestURL() string {
if r.Page != nil {
f := make([]QueryFilter, 0)
f = append(f, r.Page)
return fmt.Sprintf("%s?%s", basePath, encodeQueryFilters(f))
}

return basePath
}

// CreateTokenRequest (EXPERIMENTAL) represents a request to create a new network.
type CreateTokenRequest struct {
Name string `json:"name"`
ExpiresAt time.Time `json:"expires_at"`
CanCreateSubTokens bool `json:"can_create_tokens"`
AllowedIPRanges []string `json:"allowed_ip_ranges"`
}

// RequestURL (EXPERIMENTAL) implements the Request interface.
func (r *CreateTokenRequest) RequestURL() string {
return basePath
}

// DeleteTokenRequest (EXPERIMENTAL) represents a request to delete a token.
type DeleteTokenRequest struct {
ID string
}

// RequestURL (EXPERIMENTAL) implements the Request interface.
func (r *DeleteTokenRequest) RequestURL() string {
return fmt.Sprintf("%s/%s", basePath, r.ID)
}
49 changes: 49 additions & 0 deletions upcloud/request/token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package request

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestGetTokenDetailsRequest(t *testing.T) {
assert.Equal(t, "/account/tokens/foo", (&GetTokenDetailsRequest{ID: "foo"}).RequestURL())
}

func TestGetTokensRequest(t *testing.T) {
assert.Equal(t, "/account/tokens", (&GetTokensRequest{}).RequestURL())
assert.Equal(t, "/account/tokens", (&GetTokensRequest{}).RequestURL())
assert.Equal(t, "/account/tokens?limit=10&offset=10", (&GetTokensRequest{
Page: &Page{
Size: 10,
Number: 2,
},
}).RequestURL())
}

func TestDeleteTokenRequest(t *testing.T) {
assert.Equal(t, "/account/tokens/foo", (&DeleteTokenRequest{ID: "foo"}).RequestURL())
}

func TestCreateTokenRequest(t *testing.T) {
want := `
{
"name": "my_1st_token",
"expires_at": "2025-01-01T00:00:00Z",
"can_create_tokens": true,
"allowed_ip_ranges": ["0.0.0.0/0", "::/0"]
}
`
req := &CreateTokenRequest{
Name: "my_1st_token",
ExpiresAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
CanCreateSubTokens: true,
AllowedIPRanges: []string{"0.0.0.0/0", "::/0"},
}
got, err := json.Marshal(req)
assert.NoError(t, err)
assert.JSONEq(t, want, string(got))
assert.Equal(t, "/account/tokens", req.RequestURL())
}
143 changes: 143 additions & 0 deletions upcloud/service/fixtures/token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
version: 1
interactions:
- request:
body: '{"name":"my_1st_token","expires_at":"2025-12-01T00:00:00Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"]}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens
method: POST
response:
body: '{"token":"ucat_01JFJ0HDPBXE0DJBP9DXNBEGGP","id":"0c0cb933-d5d5-4027-a4f5-20019f30a913","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T12:26:36.619315Z","expires_at":"2025-12-01T00:00:00.000013Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false}'
headers:
Content-Length:
- "292"
Content-Type:
- application/json
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 201 Created
code: 201
duration: ""
- request:
body: '{"name":"my_2nd_token","expires_at":"2025-12-01T00:00:00Z","can_create_tokens":false,"allowed_ip_ranges":["0.0.0.0/1","::/0"]}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens
method: POST
response:
body: '{"token":"ucat_01JFJ0HDR7EHQVZER2KZXBR5NC","id":"0c54f4bf-0b31-47da-b9f5-5cebeda621a4","name":"my_2nd_token","type":"workspace","created_at":"2024-12-20T12:26:36.679304Z","expires_at":"2025-12-01T00:00:00.000013Z","can_create_tokens":false,"allowed_ip_ranges":["0.0.0.0/1","::/0"],"gui":false}'
headers:
Content-Length:
- "293"
Content-Type:
- application/json
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 201 Created
code: 201
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens/0c0cb933-d5d5-4027-a4f5-20019f30a913
method: GET
response:
body: '{"id":"0c0cb933-d5d5-4027-a4f5-20019f30a913","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T12:26:36.619315Z","expires_at":"2025-12-01T00:00:00.000013Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false}'
headers:
Content-Length:
- "250"
Content-Type:
- application/json
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens
method: GET
response:
body: '[{"id":"0c2adaf6-0805-4f18-bb45-03fce1bc1c2d","name":"token","type":"workspace","created_at":"2024-12-19T11:46:09.888763Z","expires_at":"2024-12-19T12:16:09.888531Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c1a21dc-11b0-47aa-9979-b45e03b38788","name":"token","type":"workspace","created_at":"2024-12-19T11:57:33.40507Z","expires_at":"2024-12-19T12:27:33.404897Z","last_used_at":"2024-12-19T12:01:13.538016Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c09ff1a-aec0-4a22-bbca-01970a710ecc","name":"token","type":"workspace","created_at":"2024-12-19T12:17:17.14145Z","expires_at":"2024-12-19T12:47:17.141249Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0cc7fdd4-af6f-4da4-9a59-a7b923d27317","name":"my_1st_token","type":"workspace","created_at":"2024-12-19T13:14:40.617399Z","expires_at":"2024-12-19T14:14:40.532908Z","can_create_tokens":false,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c0d735c-4414-46f1-a1e8-64bb27667196","name":"my_1st_token","type":"workspace","created_at":"2024-12-19T13:17:35.961859Z","expires_at":"2024-12-19T14:17:35.845322Z","can_create_tokens":false,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0cd3aa86-b1ed-416b-82a9-427778762d43","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T07:44:49.162896Z","expires_at":"2024-12-20T08:44:49.066726Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c3869ea-bb43-4588-a16d-02d07a394d94","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T07:49:59.192905Z","expires_at":"2025-01-01T00:00:00.000012Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c0cb933-d5d5-4027-a4f5-20019f30a913","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T12:26:36.619315Z","expires_at":"2025-12-01T00:00:00.000013Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false},{"id":"0c54f4bf-0b31-47da-b9f5-5cebeda621a4","name":"my_2nd_token","type":"workspace","created_at":"2024-12-20T12:26:36.679304Z","expires_at":"2025-12-01T00:00:00.000013Z","can_create_tokens":false,"allowed_ip_ranges":["0.0.0.0/1","::/0"],"gui":false}]'
headers:
Content-Length:
- "2285"
Content-Type:
- application/json
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 200 OK
code: 200
duration: ""
- request:
body: '{"name":"my_1st_token","expires_at":"2025-12-01T00:00:00Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"]}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens
method: POST
response:
body: '{"token":"ucat_01JFJ0HDVC5NQFFD0BYE5N0VBG","id":"0c62c862-2f4a-41d3-8f07-1cfbded87295","name":"my_1st_token","type":"workspace","created_at":"2024-12-20T12:26:36.78057Z","expires_at":"2025-12-01T00:00:00.000018Z","can_create_tokens":true,"allowed_ip_ranges":["0.0.0.0/0","::/0"],"gui":false}'
headers:
Content-Length:
- "291"
Content-Type:
- application/json
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 201 Created
code: 201
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- upcloud-go-api/8.14.0
url: https://api.upcloud.com/1.3/account/tokens/0c62c862-2f4a-41d3-8f07-1cfbded87295
method: DELETE
response:
body: ""
headers:
Date:
- Fri, 20 Dec 2024 12:26:36 GMT
status: 204 No Content
code: 204
duration: ""
Loading
Loading