Skip to content

Commit be7e3e7

Browse files
CLOUDP-279930: Admin SDK helper with examples (#461)
Co-authored-by: Lovisa Berggren <[email protected]>
1 parent 841de36 commit be7e3e7

File tree

8 files changed

+143
-23
lines changed

8 files changed

+143
-23
lines changed

admin/atlas_client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/mongodb-forks/digest"
9+
"go.mongodb.org/atlas-sdk/v20241023001/auth/credentials"
910
"go.mongodb.org/atlas-sdk/v20241023001/internal/core"
1011
)
1112

@@ -52,6 +53,34 @@ func UseDigestAuth(apiKey, apiSecret string) ClientModifier {
5253
}
5354
}
5455

56+
// UseOAuthAuth provides OAuthAuth authentication for Go SDK.
57+
// Method is provided as helper to create a default HTTP client that supports OAuth (Service Accounts) authentication.
58+
// credentials.LocalTokenCache can be supplied to reuse OAuth Token across application restarts.
59+
// Warning: for advanced use cases please use credentials.NewTokenSource directly in your code pass it to UseHTTPClient method.
60+
// Warning: any previously set httpClient will be overwritten. To fully customize HttpClient use UseHTTPClient method.
61+
func UseOAuthAuth(clientID, clientSecret string, tokenCache credentials.LocalTokenCache) ClientModifier {
62+
return func(c *Configuration) error {
63+
var tokenSource credentials.TokenSource
64+
if tokenCache != nil {
65+
tokenSource = credentials.NewTokenSourceWithOptions(credentials.AtlasTokenSourceOptions{
66+
ClientID: clientID,
67+
ClientSecret: clientSecret,
68+
TokenCache: tokenCache,
69+
BaseURL: &c.Servers[0].URL,
70+
})
71+
} else {
72+
tokenSource = credentials.NewTokenSourceWithOptions(credentials.AtlasTokenSourceOptions{
73+
ClientID: clientID,
74+
ClientSecret: clientSecret,
75+
BaseURL: &c.Servers[0].URL,
76+
})
77+
}
78+
httpClient := credentials.NewHTTPClientWithOAuthToken(tokenSource)
79+
c.HTTPClient = httpClient
80+
return nil
81+
}
82+
}
83+
5584
// Advanced modifiers.
5685

5786
// UseHTTPClient set custom http client implementation.

auth/credentials/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func NewTokenSourceWithOptions(opts AtlasTokenSourceOptions) TokenSource {
6363
ctx = *opts.Context
6464
}
6565

66+
if opts.TokenCache == nil {
67+
opts.TokenCache = &InMemoryTokenCache{}
68+
}
69+
6670
return &OAuthTokenSource{
6771
clientID: opts.ClientID,
6872
clientSecret: opts.ClientSecret,

auth/credentials/oauth.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func (c *OAuthTokenSource) RevokeToken() error {
5050
if err != nil {
5151
return err
5252
}
53-
if tokenString != nil && *tokenString != "" {
54-
err := c.revokeTokenInRemoteServer(*tokenString)
53+
if tokenString != "" {
54+
err := c.revokeTokenInRemoteServer(tokenString)
5555
if err != nil {
5656
return err
5757
}
@@ -70,12 +70,12 @@ func (c *OAuthTokenSource) RevokeToken() error {
7070
func (c *OAuthTokenSource) GetValidToken() (*Token, error) {
7171
// Try to retrieve the Token string from the Token source
7272
tokenString, err := c.tokenCache.RetrieveToken(c.ctx)
73-
if err != nil || tokenString == nil {
73+
if err != nil || tokenString == "" {
7474
return c.refreshToken()
7575
}
7676

7777
// Parse the Token string into the Token structure (mock parse operation)
78-
c.token, err = parseToken(*tokenString)
78+
c.token, err = parseToken(tokenString)
7979
if err != nil || c.token.expired() {
8080
// Token is invalid or expired, refresh it
8181
return c.refreshToken()
@@ -169,17 +169,17 @@ func (c *OAuthTokenSource) fetchTokenFromRemoteServer() (*Token, error) {
169169
defer resp.Body.Close()
170170

171171
if resp.StatusCode != http.StatusOK {
172+
msg, _ := io.ReadAll(resp.Body)
172173
if resp.StatusCode == http.StatusTooManyRequests {
173-
msg, _ := io.ReadAll(resp.Body)
174174
formattedMessage := fmt.Sprintf("%v %v: HTTP %v Detail: %v Reason: %v",
175175
"POST", c.tokenURL, resp.StatusCode,
176176
"Token request was rate limited", string(msg))
177177
return nil, errors.New(formattedMessage)
178178
}
179179
formattedMessage := fmt.Sprintf("%v %v: HTTP %v Detail: %v Reason: %v",
180180
"POST", c.tokenURL, resp.StatusCode,
181-
"Failed to obtain Access Token when fetching new OAuth Token from remote server",
182-
resp.Header.Get("www-authenticate"))
181+
"Failed to obtain Access Token when fetching new OAuth Token from remote server for client "+c.clientID,
182+
string(msg))
183183
return nil, errors.New(formattedMessage)
184184
}
185185
// tokenRemoteResponse represents successful response from token endpoint

auth/credentials/oauth_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ type MockTokenCache struct {
6666
}
6767

6868
// Retrieve returns the stored Token
69-
func (m *MockTokenCache) RetrieveToken(_ context.Context) (*string, error) {
70-
return &m.token, nil
69+
func (m *MockTokenCache) RetrieveToken(_ context.Context) (string, error) {
70+
return m.token, nil
7171
}
7272

7373
// Save saves the Token

auth/credentials/token_cache.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,34 @@ import (
1010
// Interface allows integrators to fully control how access Token is catched on their side.
1111
type LocalTokenCache interface {
1212
// RetrieveToken should return the access Token string.
13-
RetrieveToken(ctx context.Context) (*string, error)
13+
RetrieveToken(ctx context.Context) (string, error)
1414

1515
// SaveToken should save the access Token string.
1616
SaveToken(ctx context.Context, token string) error
1717
}
1818

19-
// InMemoryTokenCache is an default implementation of LocalTokenCache that stores the Token in memory.
19+
// InMemoryTokenCache is a default implementation of LocalTokenCache that stores the Token in memory.
2020
// Implementation provides locking mechanism in order to prevent overriding access tokens
2121
type InMemoryTokenCache struct {
22-
token *string
22+
token string
2323
mu sync.Mutex
2424
}
2525

26-
func (s *InMemoryTokenCache) RetrieveToken(_ context.Context) (*string, error) {
26+
func (s *InMemoryTokenCache) RetrieveToken(_ context.Context) (string, error) {
2727
// Locking will avoid Token overrides when sharing TokenCache in multiple threads
2828
s.mu.Lock()
2929
defer s.mu.Unlock()
3030

31-
if s.token != nil {
31+
if s.token != "" {
3232
return s.token, nil
3333
}
34-
return nil, errors.New("InMemoryTokenCache: Token not found. Token needs to be refreshed in backend")
34+
return "", errors.New("InMemoryTokenCache: Token not found. Token needs to be refreshed in backend")
3535
}
3636

3737
func (s *InMemoryTokenCache) SaveToken(_ context.Context, token string) error {
3838
// Locking will avoid Token overrides when sharing TokenCache in multiple threads
3939
s.mu.Lock()
4040
defer s.mu.Unlock()
41-
s.token = &token
41+
s.token = token
4242
return nil
4343
}

examples/auth/basic_client.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"go.mongodb.org/atlas-sdk/v20241023001/admin"
7+
"log"
8+
"os"
9+
)
10+
11+
// Basic example for Service Account OAuth Authentication
12+
// Covers creation of the HTTP client with Service Account OAuth authentication
13+
// Required env variables to run example:
14+
// export MONGODB_ATLAS_CLIENT_ID="your_client_id"
15+
// export MONGODB_ATLAS_CLIENT_SECRET="your_client_secret"
16+
func main() {
17+
// Fetch clientID and clientSecret from environment variables
18+
clientID := os.Getenv("MONGODB_ATLAS_CLIENT_ID")
19+
clientSecret := os.Getenv("MONGODB_ATLAS_CLIENT_SECRET")
20+
21+
if clientID == "" || clientSecret == "" {
22+
log.Fatal("Missing CLIENT_ID or CLIENT_SECRET environment variables")
23+
}
24+
25+
ctx := context.Background()
26+
// Create Admin API Client with OAuth credentials.
27+
// Skips optional tokenCache parameter
28+
sdk, err := admin.NewClient(
29+
admin.UseOAuthAuth(clientID, clientSecret, nil),
30+
)
31+
32+
if err != nil {
33+
log.Fatalf("Error: %v", err)
34+
}
35+
36+
request := sdk.ProjectsApi.ListProjectsWithParams(ctx,
37+
&admin.ListProjectsApiParams{
38+
ItemsPerPage: admin.PtrInt(1),
39+
IncludeCount: admin.PtrBool(true),
40+
PageNum: admin.PtrInt(1),
41+
})
42+
43+
if err != nil {
44+
log.Fatalf("Error making request: %v", err)
45+
}
46+
projects, _, err := request.IncludeCount(true).PageNum(1).Execute()
47+
if err != nil {
48+
log.Fatalf("Error: %v", err)
49+
}
50+
51+
if projects.Results == nil {
52+
fmt.Printf("projects should not be empty: %v", projects)
53+
}
54+
}

examples/auth/credentials.go renamed to examples/auth_advanced/advanced_client.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import (
1212
"go.mongodb.org/atlas-sdk/v20241023001/auth/credentials"
1313
)
1414

15-
// Example for Service Account Authentication
16-
// Required env variables
15+
// Advanced Example for Service Account OAuth Authentication
16+
// Provides:
17+
// 1. Custom Token Cache in order to store OAuth Token for further reuse across application restarts
18+
// 2. Usage of logout (Token Revocation) method.
19+
// 3. Using custom Context and HttpTransports
20+
//
21+
// Required env variables to run example:
1722
// export MONGODB_ATLAS_CLIENT_ID="your_client_id"
1823
// export MONGODB_ATLAS_CLIENT_SECRET="your_client_secret"
1924
// export MONGODB_ATLAS_URL=https://cloud.mongodb.com
@@ -77,6 +82,7 @@ func main() {
7782
fmt.Printf("projects should not be empty: %v", projects)
7883
}
7984

85+
// Revoke Token in the client (logout)
8086
err = tokenSource.RevokeToken()
8187
if err != nil {
8288
log.Fatalf("Error: %v", err)
@@ -89,7 +95,7 @@ type MyTokenCache struct {
8995
mu sync.Mutex
9096
}
9197

92-
func (s *MyTokenCache) RetrieveToken(ctx context.Context) (*string, error) {
98+
func (s *MyTokenCache) RetrieveToken(_ context.Context) (string, error) {
9399
// Locking added to ensure no race condition happens with SaveToken
94100
s.mu.Lock()
95101
defer s.mu.Unlock()
@@ -99,13 +105,13 @@ func (s *MyTokenCache) RetrieveToken(ctx context.Context) (*string, error) {
99105
var tkn string
100106
err := json.Unmarshal(s.fileContent, &tkn)
101107
if err != nil {
102-
return nil, err
108+
return "", err
103109
}
104110

105-
return &tkn, nil
111+
return tkn, nil
106112
}
107113

108-
func (s *MyTokenCache) SaveToken(ctx context.Context, tkn string) error {
114+
func (s *MyTokenCache) SaveToken(_ context.Context, tkn string) error {
109115
// Locking added to ensure no race condition happens with RetrieveToken
110116
s.mu.Lock()
111117
defer s.mu.Unlock()

tools/config/go-templates/atlas_client.mustache

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,35 @@ func UseDigestAuth(apiKey, apiSecret string) ClientModifier {
5353
}
5454
}
5555

56-
// Advanced modifiers.
56+
// UseOAuthAuth provides OAuthAuth authentication for Go SDK.
57+
// Method is provided as helper to create a default HTTP client that supports OAuth (Service Accounts) authentication.
58+
// credentials.LocalTokenCache can be supplied to reuse OAuth Token across application restarts.
59+
// Warning: for advanced use cases please use credentials.NewTokenSource directly in your code pass it to UseHTTPClient method.
60+
// Warning: any previously set httpClient will be overwritten. To fully customize HttpClient use UseHTTPClient method.
61+
func UseOAuthAuth(clientID, clientSecret string, tokenCache credentials.LocalTokenCache) ClientModifier {
62+
return func(c *Configuration) error {
63+
var tokenSource credentials.TokenSource
64+
if tokenCache != nil {
65+
tokenSource = credentials.NewTokenSourceWithOptions(credentials.AtlasTokenSourceOptions{
66+
ClientID: clientID,
67+
ClientSecret: clientSecret,
68+
TokenCache: tokenCache,
69+
BaseURL: &c.Servers[0].URL,
70+
})
71+
} else {
72+
tokenSource = credentials.NewTokenSourceWithOptions(credentials.AtlasTokenSourceOptions{
73+
ClientID: clientID,
74+
ClientSecret: clientSecret,
75+
BaseURL: &c.Servers[0].URL,
76+
})
77+
}
78+
httpClient := credentials.NewHTTPClientWithOAuthToken(tokenSource)
79+
c.HTTPClient = httpClient
80+
return nil
81+
}
82+
}
5783

84+
// Advanced modifiers.
5885

5986
// UseHTTPClient set custom http client implementation.
6087
//

0 commit comments

Comments
 (0)