Skip to content

Commit f8cd4fc

Browse files
authored
Change agent to support OAuth2 auth (#159)
* Change agent to support OAuth2 auth Signed-off-by: Jose Fuentes <[email protected]> * Use single character shorthand for credentials file Signed-off-by: Jose Fuentes <[email protected]> * Fix initialization of oauth client Signed-off-by: Jose Fuentes <[email protected]> * Delete not related files Signed-off-by: Jose Fuentes <[email protected]> * Fix typo Signed-off-by: Jose Fuentes <[email protected]>
1 parent 1a17308 commit f8cd4fc

File tree

6 files changed

+305
-49
lines changed

6 files changed

+305
-49
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ define LDFLAGS
1515
-X "github.com/jetstack/preflight/pkg/version.Platform=$(GOOS)/$(GOARCH)" \
1616
-X "github.com/jetstack/preflight/pkg/version.Commit=$(COMMIT)" \
1717
-X "github.com/jetstack/preflight/pkg/version.BuildDate=$(DATE)" \
18-
-X "github.com/jetstack/preflight/pkg/version.GoVersion=$(GOVERSION)"
18+
-X "github.com/jetstack/preflight/pkg/version.GoVersion=$(GOVERSION)" \
19+
-X "github.com/jetstack/preflight/pkg/client.clientID=$(OAUTH_CLIENT_ID)" \
20+
-X "github.com/jetstack/preflight/pkg/client.clientSecret=$(OAUTH_CLIENT_SECRET)" \
21+
-X "github.com/jetstack/preflight/pkg/client.authServer=$(OAUTH_AUTH_SERVER)"
1922
endef
2023

2124
GO_BUILD:=go build -ldflags '$(LDFLAGS)'

cmd/agent.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,11 @@ func init() {
3636
3600,
3737
"Time between scans, in seconds.",
3838
)
39+
agentCmd.PersistentFlags().StringVarP(
40+
&agent.CredentialsPath,
41+
"credentials-file",
42+
"k",
43+
"",
44+
"(Experimental) Location of the credentials file. For OAuth2 based authentication.",
45+
)
3946
}

pkg/agent/credentials.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package agent
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/hashicorp/go-multierror"
8+
)
9+
10+
// Credentials defines the format of the credentials.json file.
11+
type Credentials struct {
12+
// UserID is the ID or email for the user or service account.
13+
UserID string `json:"user_id"`
14+
// UserSecret is the secret for the user or service account.
15+
UserSecret string `json:"user_secret"`
16+
}
17+
18+
func (c *Credentials) validate() error {
19+
var result *multierror.Error
20+
21+
if c.UserID == "" {
22+
result = multierror.Append(result, fmt.Errorf("user_id cannot be empty"))
23+
}
24+
25+
if c.UserSecret == "" {
26+
result = multierror.Append(result, fmt.Errorf("user_secret cannot be empty"))
27+
}
28+
29+
return result.ErrorOrNil()
30+
}
31+
32+
// ParseCredentials reads credentials into a struct used. Performs validations.
33+
func ParseCredentials(data []byte) (*Credentials, error) {
34+
var credentials Credentials
35+
36+
err := json.Unmarshal(data, &credentials)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
if err = credentials.validate(); err != nil {
42+
return nil, err
43+
}
44+
45+
return &credentials, nil
46+
}

pkg/agent/run.go

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package agent
22

33
import (
4-
"bytes"
54
"context"
6-
"encoding/json"
75
"fmt"
86
"io/ioutil"
97
"log"
10-
"net/http"
118
"net/url"
129
"os"
1310
"time"
1411

1512
"github.com/jetstack/preflight/api"
13+
"github.com/jetstack/preflight/pkg/client"
1614
"github.com/jetstack/preflight/pkg/datagatherer"
1715
"github.com/spf13/cobra"
1816
)
@@ -26,6 +24,9 @@ var AuthToken string
2624
// Period is the number of seconds between scans
2725
var Period uint
2826

27+
// CredentialsPath is where the agent will try to loads the credentials. (Experimental)
28+
var CredentialsPath string
29+
2930
// Run starts the agent process
3031
func Run(cmd *cobra.Command, args []string) {
3132
ctx := context.Background()
@@ -43,21 +44,10 @@ func Run(cmd *cobra.Command, args []string) {
4344
log.Fatalf("Failed to parse config file: %s", err)
4445
}
4546

46-
// AuthToken flag takes preference over token in configuration file.
47-
if AuthToken == "" {
48-
AuthToken = config.Token
49-
} else {
50-
log.Printf("Using authorization token from flag.")
51-
}
52-
5347
if config.Token != "" {
5448
config.Token = "(redacted)"
5549
}
5650

57-
if AuthToken == "" {
58-
log.Fatalf("Missing authorization token. Cannot continue.")
59-
}
60-
6151
serverURL, err := url.Parse(fmt.Sprintf("%s://%s%s", config.Endpoint.Protocol, config.Endpoint.Host, config.Endpoint.Path))
6252
if err != nil {
6353
log.Fatalf("Failed to build URL: %s", err)
@@ -70,6 +60,47 @@ func Run(cmd *cobra.Command, args []string) {
7060

7161
log.Printf("Loaded config: \n%s", dump)
7262

63+
var credentials *Credentials
64+
if CredentialsPath != "" {
65+
file, err = os.Open(CredentialsPath)
66+
if err != nil {
67+
log.Fatalf("Failed to load credentials from file %s", CredentialsPath)
68+
}
69+
defer file.Close()
70+
71+
b, err = ioutil.ReadAll(file)
72+
73+
credentials, err = ParseCredentials(b)
74+
if err != nil {
75+
log.Fatalf("Failed to parse credentials file: %s", err)
76+
}
77+
}
78+
79+
var preflightClient *client.PreflightClient
80+
if credentials != nil {
81+
log.Printf("A credentials file was specified. Using OAuth2 authentication...")
82+
preflightClient, err = client.New(credentials.UserID, credentials.UserSecret, serverURL.String())
83+
if err != nil {
84+
log.Fatalf("Error creating preflight client: %+v", err)
85+
}
86+
} else {
87+
// AuthToken flag takes preference over token in configuration file.
88+
if AuthToken == "" {
89+
AuthToken = config.Token
90+
} else {
91+
log.Printf("Using authorization token from flag.")
92+
}
93+
94+
if AuthToken == "" {
95+
log.Fatalf("Missing authorization token. Cannot continue.")
96+
}
97+
98+
preflightClient, err = client.NewWithBasicAuth(AuthToken, serverURL.String())
99+
if err != nil {
100+
log.Fatalf("Error creating preflight client: %+v", err)
101+
}
102+
}
103+
73104
dataGatherers := make(map[string]datagatherer.DataGatherer)
74105

75106
for _, dgConfig := range config.DataGatherers {
@@ -108,44 +139,13 @@ func Run(cmd *cobra.Command, args []string) {
108139
for {
109140
log.Println("Running Agent...")
110141
log.Println("Posting data to ", serverURL)
111-
err = postData(serverURL, AuthToken, readings)
142+
err = preflightClient.PostDataReadings(readings)
112143
// TODO: handle errors gracefully: e.g. handle retries when it is possible
113144
if err != nil {
114145
log.Fatalf("Post to server failed: %+v", err)
146+
} else {
147+
log.Println("Data sent successfully.")
115148
}
116149
time.Sleep(time.Duration(Period) * time.Second)
117150
}
118151
}
119-
120-
func postData(serverURL *url.URL, token string, readings []*api.DataReading) error {
121-
data, err := json.Marshal(readings)
122-
if err != nil {
123-
return err
124-
}
125-
126-
req, err := http.NewRequest(http.MethodPost, serverURL.String(), bytes.NewBuffer(data))
127-
req.Header.Set("Content-Type", "application/json")
128-
129-
if len(token) > 0 {
130-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
131-
}
132-
133-
client := &http.Client{}
134-
resp, err := client.Do(req)
135-
if err != nil {
136-
return err
137-
}
138-
139-
body, err := ioutil.ReadAll(resp.Body)
140-
if err != nil {
141-
return err
142-
}
143-
144-
if code := resp.StatusCode; code < 200 || code >= 300 {
145-
return fmt.Errorf("Received response with status code %d. Body: %s", code, string(body))
146-
}
147-
148-
log.Println("Data sent successfully. Server says: ", string(body))
149-
150-
return nil
151-
}

pkg/client/accessToken.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package client
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
"strings"
9+
"time"
10+
11+
"github.com/juju/errors"
12+
)
13+
14+
type accessToken struct {
15+
bearer string
16+
expirationDate time.Time
17+
}
18+
19+
func (t *accessToken) needsRenew() bool {
20+
return t.bearer == "" || time.Now().After(t.expirationDate)
21+
}
22+
23+
// getValidAccessToken returns a valid access token. It will fetch a new access
24+
// token from the auth server in case the current access token does not exist
25+
// or it is expired.
26+
func (c *PreflightClient) getValidAccessToken() (*accessToken, error) {
27+
if c.accessToken.needsRenew() {
28+
err := c.renewAccessToken()
29+
if err != nil {
30+
return nil, err
31+
}
32+
}
33+
34+
return c.accessToken, nil
35+
}
36+
37+
func (c *PreflightClient) renewAccessToken() error {
38+
url := fmt.Sprintf("https://%s/oauth/token", authServer)
39+
// TODO: audience will be dynamic in the future, but at the moment this client only sends readings.
40+
audience := "https://preflight.jetstack.io/api/v1/datareading"
41+
payload := fmt.Sprintf("grant_type=password&client_id=%s&client_secret=%s&audience=%s&username=%s&password=%s", clientID, clientSecret, audience, c.userID, c.userSecret)
42+
req, err := http.NewRequest("POST", url, strings.NewReader(payload))
43+
if err != nil {
44+
return errors.Trace(err)
45+
}
46+
req.Header.Add("content-type", "application/x-www-form-urlencoded")
47+
48+
res, err := http.DefaultClient.Do(req)
49+
if err != nil {
50+
return errors.Trace(err)
51+
}
52+
53+
body, err := ioutil.ReadAll(res.Body)
54+
if err != nil {
55+
return errors.Trace(err)
56+
}
57+
58+
defer res.Body.Close()
59+
60+
response := struct {
61+
Bearer string `json:"access_token"`
62+
ExpiresIn uint `json:"expires_in"`
63+
}{}
64+
65+
err = json.Unmarshal(body, &response)
66+
if err != nil {
67+
return errors.Trace(err)
68+
}
69+
70+
if response.ExpiresIn == 0 {
71+
return fmt.Errorf("got wrong expiration for access token")
72+
}
73+
74+
c.accessToken.bearer = response.Bearer
75+
c.accessToken.expirationDate = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
76+
77+
return nil
78+
}

0 commit comments

Comments
 (0)