forked from vmware/go-vcloud-director
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi_token.go
190 lines (168 loc) · 6.19 KB
/
api_token.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/*
* Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/
package govcd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/vmware/go-vcloud-director/v2/types/v56"
"github.com/vmware/go-vcloud-director/v2/util"
)
// SetApiToken behaves similarly to SetToken, with the difference that it will
// return full information about the bearer token, so that the caller can make decisions about token expiration
func (vcdClient *VCDClient) SetApiToken(org, apiToken string) (*types.ApiTokenRefresh, error) {
tokenRefresh, err := vcdClient.GetBearerTokenFromApiToken(org, apiToken)
if err != nil {
return nil, err
}
err = vcdClient.SetToken(org, BearerTokenHeader, tokenRefresh.AccessToken)
if err != nil {
return nil, err
}
return tokenRefresh, nil
}
// SetServiceAccountApiToken reads the current Service Account API token,
// sets the client's bearer token and fetches a new API token for next
// authentication request using SetApiToken and overwrites the old file.
func (vcdClient *VCDClient) SetServiceAccountApiToken(org, apiTokenFile string) error {
if vcdClient.Client.APIVCDMaxVersionIs("< 37.0") {
version, err := vcdClient.Client.GetVcdFullVersion()
if err == nil {
return fmt.Errorf("minimum version for Service Account authentication is 10.4 - Version detected: %s", version.Version)
}
// If we can't get the VCD version, we return API version info
return fmt.Errorf("minimum API version for Service Account authentication is 37.0 - Version detected: %s", vcdClient.Client.APIVersion)
}
saApiToken := &types.ApiTokenRefresh{}
// Read file contents and unmarshal them to saApiToken
err := readFileAndUnmarshalJSON(apiTokenFile, saApiToken)
if err != nil {
return err
}
// Get bearer token and update the refresh token for the next authentication request
saApiToken, err = vcdClient.SetApiToken(org, saApiToken.RefreshToken)
if err != nil {
return err
}
// leave only the refresh token to not leave any sensitive information
saApiToken = &types.ApiTokenRefresh{
RefreshToken: saApiToken.RefreshToken,
TokenType: "Service Account",
UpdatedBy: vcdClient.Client.UserAgent,
UpdatedOn: time.Now().Format(time.RFC3339),
}
err = marshalJSONAndWriteToFile(apiTokenFile, saApiToken, 0600)
if err != nil {
return err
}
return nil
}
// GetBearerTokenFromApiToken uses an API token to retrieve a bearer token
// using the refresh token operation.
func (vcdClient *VCDClient) GetBearerTokenFromApiToken(org, token string) (*types.ApiTokenRefresh, error) {
if vcdClient.Client.APIVCDMaxVersionIs("< 36.1") {
version, err := vcdClient.Client.GetVcdFullVersion()
if err == nil {
return nil, fmt.Errorf("minimum version for API token is 10.3.1 - Version detected: %s", version.Version)
}
// If we can't get the VCD version, we return API version info
return nil, fmt.Errorf("minimum API version for API token is 36.1 - Version detected: %s", vcdClient.Client.APIVersion)
}
var userDef string
newUrl := new(url.URL)
newUrl.Scheme = vcdClient.Client.VCDHREF.Scheme
newUrl.Host = vcdClient.Client.VCDHREF.Host
urlStr := newUrl.String()
if strings.EqualFold(org, "system") {
userDef = "provider"
} else {
userDef = fmt.Sprintf("tenant/%s", org)
}
reqUrl := fmt.Sprintf("%s/oauth/%s/token", urlStr, userDef)
reqHref, err := url.ParseRequestURI(reqUrl)
if err != nil {
return nil, fmt.Errorf("error getting request URL from %s : %s", reqUrl, err)
}
data := bytes.NewBufferString(fmt.Sprintf("grant_type=refresh_token&refresh_token=%s", token))
req := vcdClient.Client.NewRequest(nil, http.MethodPost, *reqHref, data)
req.Header.Add("Accept", "application/*;version=36.1")
resp, err := vcdClient.Client.Http.Do(req)
if err != nil {
return nil, err
}
var body []byte
var tokenDef types.ApiTokenRefresh
if resp.Body != nil {
body, err = io.ReadAll(resp.Body)
}
// The default response data to show in the logs is a string of asterisks
responseData := "[" + strings.Repeat("*", 10) + "]"
// If users request to see sensitive data, we pass the unchanged response body
if util.LogPasswords {
responseData = string(body)
}
util.ProcessResponseOutput("GetBearerTokenFromApiToken", resp, responseData)
if len(body) == 0 {
return nil, fmt.Errorf("refresh token was empty: %s", resp.Status)
}
if err != nil {
return nil, fmt.Errorf("error extracting refresh token: %s", err)
}
err = json.Unmarshal(body, &tokenDef)
if err != nil {
return nil, fmt.Errorf("error decoding token text: %s", err)
}
if tokenDef.AccessToken == "" {
// If the access token is empty, the body should contain a composite error message.
// Attempting to decode it and return as much information as possible
var errorBody map[string]string
err2 := json.Unmarshal(body, &errorBody)
if err2 == nil {
errorMessage := ""
for k, v := range errorBody {
if v == "null" || v == "" {
continue
}
errorMessage += fmt.Sprintf("%s: %s - ", k, v)
}
return nil, fmt.Errorf("%s: %s", errorMessage, resp.Status)
}
// If decoding the error fails, we return the raw body (possibly an unencoded internal server error)
return nil, fmt.Errorf("access token retrieved from API token was empty - %s %s", resp.Status, string(body))
}
return &tokenDef, nil
}
// readFileAndUnmarshalJSON reads a file and unmarshals it to the given variable
func readFileAndUnmarshalJSON(filename string, object any) error {
data, err := os.ReadFile(path.Clean(filename))
if err != nil {
return fmt.Errorf("failed to read from file: %s", err)
}
err = json.Unmarshal(data, object)
if err != nil {
return fmt.Errorf("failed to unmarshal file contents to the object: %s", err)
}
return nil
}
// marshalJSONAndWriteToFile marshalls the given object into JSON and writes
// to a file with the given permissions in octal format (e.g 0600)
func marshalJSONAndWriteToFile(filename string, object any, permissions int) error {
data, err := json.MarshalIndent(object, " ", " ")
if err != nil {
return fmt.Errorf("error marshalling object to JSON: %s", err)
}
err = os.WriteFile(filename, data, fs.FileMode(permissions))
if err != nil {
return fmt.Errorf("error writing to the file: %s", err)
}
return nil
}