Skip to content

Commit bfc511c

Browse files
authored
Merge pull request #10 from overmindtech/api-keys
Implement Authentication using API Keys
2 parents bab82bd + 3bc9c03 commit bfc511c

File tree

8 files changed

+123
-25
lines changed

8 files changed

+123
-25
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,45 @@
11
# ovm-cli
2+
23
CLI to interact with the overmind API
4+
5+
```
6+
Usage:
7+
ovm-cli [command]
8+
9+
Available Commands:
10+
change-from-tfplan Creates a new Change from a given terraform plan file
11+
completion Generate the autocompletion script for the specified shell
12+
get-affected-bookmarks Calculates the bookmarks that would be overlapping with a snapshot.
13+
get-bookmark Displays the contents of a bookmark.
14+
get-snapshot Displays the contents of a snapshot.
15+
help Help about any command
16+
request Runs a request against the overmind API
17+
18+
Flags:
19+
--apikey-url string The overmind API Keys endpoint (defaults to --url)
20+
--auth0-client-id string OAuth Client ID to use when connecting with auth (default "j3LylZtIosVPZtouKI8WuVHmE6Lluva1")
21+
--auth0-domain string Auth0 domain to connect to (default "om-prod.eu.auth0.com")
22+
--config string config file (default is redacted.yaml)
23+
--gateway-url string The overmind Gateway endpoint (defaults to /api/gateway on --url)
24+
-h, --help help for ovm-cli
25+
--honeycomb-api-key string If specified, configures opentelemetry libraries to submit traces to honeycomb
26+
--json-log Set to true to emit logs as json for easier parsing
27+
--log string Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace (default "info")
28+
--run-mode string Set the run mode for this service, 'release', 'debug' or 'test'. Defaults to 'release'. (default "release")
29+
--sentry-dsn string If specified, configures sentry libraries to capture errors
30+
--stdout-trace-dump Dump all otel traces to stdout for debugging
31+
--token string The API token to use for authentication, also read from OVM_TOKEN environment variable
32+
--url string The overmind API endpoint (default "https://api.prod.overmind.tech")
33+
34+
Use "ovm-cli [command] --help" for more information about a command.
35+
```
36+
37+
## Examples
38+
39+
Upload a terraform plan to overmind for Blast Radius Analysis:
40+
41+
```
42+
terraform show -json ./tfplan > ./tfplan.json
43+
ovm-cli change-from-tfplan --title "example change" --tfplan-json ./tfplan.json
44+
```
45+

cmd/auth_client.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,27 @@ import (
1111
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
1212
)
1313

14-
// AuthenticatedChangesClient Returns a bookmark client that uses the auth
14+
// AuthenticatedApiKeyClient Returns an apikey client that uses the auth
1515
// embedded in the context and otel instrumentation
16-
func AuthenticatedChangesClient(ctx context.Context) sdpconnect.ChangesServiceClient {
16+
func AuthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceClient {
1717
httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient)
18-
url := viper.GetString("changes-url")
18+
url := viper.GetString("apikey-url")
1919
if url == "" {
2020
url = viper.GetString("url")
21-
viper.Set("changes-url", url)
21+
viper.Set("apikey-url", url)
2222
}
23-
return sdpconnect.NewChangesServiceClient(httpClient, url)
23+
return sdpconnect.NewApiKeyServiceClient(httpClient, url)
24+
}
25+
26+
// UnauthenticatedApiKeyClient Returns an apikey client with otel instrumentation
27+
// but no authentication. Can only be used for ExchangeKeyForToken
28+
func UnauthenticatedApiKeyClient(ctx context.Context) sdpconnect.ApiKeyServiceClient {
29+
url := viper.GetString("apikey-url")
30+
if url == "" {
31+
url = viper.GetString("url")
32+
viper.Set("apikey-url", url)
33+
}
34+
return sdpconnect.NewApiKeyServiceClient(otelhttp.DefaultClient, url)
2435
}
2536

2637
// AuthenticatedBookmarkClient Returns a bookmark client that uses the auth
@@ -35,6 +46,18 @@ func AuthenticatedBookmarkClient(ctx context.Context) sdpconnect.BookmarksServic
3546
return sdpconnect.NewBookmarksServiceClient(httpClient, url)
3647
}
3748

49+
// AuthenticatedChangesClient Returns a bookmark client that uses the auth
50+
// embedded in the context and otel instrumentation
51+
func AuthenticatedChangesClient(ctx context.Context) sdpconnect.ChangesServiceClient {
52+
httpClient := NewAuthenticatedClient(ctx, otelhttp.DefaultClient)
53+
url := viper.GetString("changes-url")
54+
if url == "" {
55+
url = viper.GetString("url")
56+
viper.Set("changes-url", url)
57+
}
58+
return sdpconnect.NewChangesServiceClient(httpClient, url)
59+
}
60+
3861
// AuthenticatedSnapshotsClient Returns a Snapshots client that uses the auth
3962
// embedded in the context and otel instrumentation
4063
func AuthenticatedSnapshotsClient(ctx context.Context) sdpconnect.SnapshotsServiceClient {

cmd/changefromtfplan.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var changeFromTfplanCmd = &cobra.Command{
3232
Short: "Creates a new Change from a given terraform plan file",
3333
PreRun: func(cmd *cobra.Command, args []string) {
3434
// Bind these to viper
35-
err := viper.BindPFlags(cmd.PersistentFlags())
35+
err := viper.BindPFlags(cmd.Flags())
3636
if err != nil {
3737
log.WithError(err).Fatal("could not bind `change-from-tfplan` flags")
3838
}
@@ -160,16 +160,22 @@ func ChangeFromTfplan(signals chan os.Signal, ready chan bool) int {
160160
))
161161
defer span.End()
162162

163-
// Connect to the websocket
164-
log.WithContext(ctx).Debugf("Connecting to overmind API: %v", viper.GetString("url"))
163+
gatewayUrl := viper.GetString("gateway-url")
164+
if gatewayUrl == "" {
165+
gatewayUrl = fmt.Sprintf("%v/api/gateway", viper.GetString("url"))
166+
viper.Set("gateway-url", gatewayUrl)
167+
}
165168

166169
lf := log.Fields{
167-
"url": viper.GetString("url"),
170+
"gateway_url": gatewayUrl,
168171
}
169172

173+
// Connect to the websocket
174+
log.WithContext(ctx).WithFields(lf).Debug("Connecting to overmind API")
175+
170176
ctx, err = ensureToken(ctx, signals)
171177
if err != nil {
172-
log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to authenticate")
178+
log.WithContext(ctx).WithFields(lf).WithField("apikey-url", viper.GetString("apikey-url")).WithError(err).Error("failed to authenticate")
173179
return 1
174180
}
175181

@@ -449,7 +455,7 @@ func init() {
449455
changeFromTfplanCmd.PersistentFlags().String("changes-url", "https://api.prod.overmind.tech", "The changes service API endpoint")
450456
changeFromTfplanCmd.PersistentFlags().String("frontend", "https://app.overmind.tech", "The frontend base URL")
451457

452-
changeFromTfplanCmd.PersistentFlags().String("tfplan-json", "./tfplan.json", "Parse changing items from this terraform plan JSON file. Generate this using `terraform show -json PLAN_FILE`")
458+
changeFromTfplanCmd.PersistentFlags().String("tfplan-json", "./tfplan.json", "Parse changing items from this terraform plan JSON file. Generate this using 'terraform show -json PLAN_FILE'")
453459

454460
changeFromTfplanCmd.PersistentFlags().String("title", "", "Short title for this change.")
455461
changeFromTfplanCmd.PersistentFlags().String("description", "", "Quick description of the change.")

cmd/getaffectedbookmarks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var getAffectedBookmarksCmd = &cobra.Command{
2525
Short: "Calculates the bookmarks that would be overlapping with a snapshot.",
2626
PreRun: func(cmd *cobra.Command, args []string) {
2727
// Bind these to viper
28-
err := viper.BindPFlags(cmd.PersistentFlags())
28+
err := viper.BindPFlags(cmd.Flags())
2929
if err != nil {
3030
log.WithError(err).Fatal("could not bind `get-affected-bookmarks` flags")
3131
}

cmd/getbookmark.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var getBookmarkCmd = &cobra.Command{
2525
Short: "Displays the contents of a bookmark.",
2626
PreRun: func(cmd *cobra.Command, args []string) {
2727
// Bind these to viper
28-
err := viper.BindPFlags(cmd.PersistentFlags())
28+
err := viper.BindPFlags(cmd.Flags())
2929
if err != nil {
3030
log.WithError(err).Fatal("could not bind `get-bookmark` flags")
3131
}

cmd/getsnapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var getSnapshotCmd = &cobra.Command{
2525
Short: "Displays the contents of a snapshot.",
2626
PreRun: func(cmd *cobra.Command, args []string) {
2727
// Bind these to viper
28-
err := viper.BindPFlags(cmd.PersistentFlags())
28+
err := viper.BindPFlags(cmd.Flags())
2929
if err != nil {
3030
log.WithError(err).Fatal("could not bind `get-snapshot` flags")
3131
}

cmd/request.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ var requestCmd = &cobra.Command{
3131
Short: "Runs a request against the overmind API",
3232
PreRun: func(cmd *cobra.Command, args []string) {
3333
// Bind these to viper
34-
err := viper.BindPFlags(cmd.PersistentFlags())
34+
err := viper.BindPFlags(cmd.Flags())
3535
if err != nil {
3636
log.WithError(err).Fatal("could not bind `request` flags")
3737
}
@@ -66,16 +66,23 @@ func Request(signals chan os.Signal, ready chan bool) int {
6666
return 1
6767
}
6868

69+
gatewayUrl := viper.GetString("gateway-url")
70+
if gatewayUrl == "" {
71+
gatewayUrl = fmt.Sprintf("%v/api/gateway", viper.GetString("url"))
72+
viper.Set("gateway-url", gatewayUrl)
73+
}
74+
6975
lf := log.Fields{
70-
"url": viper.GetString("url"),
76+
"gateway_url": gatewayUrl,
77+
"url": viper.GetString("url"),
7178
}
7279

7380
// Connect to the websocket
74-
log.WithContext(ctx).Debugf("Connecting to overmind API: %v", viper.GetString("url"))
81+
log.WithContext(ctx).WithFields(lf).Debug("Connecting to overmind API")
7582

7683
ctx, err = ensureToken(ctx, signals)
7784
if err != nil {
78-
log.WithContext(ctx).WithFields(lf).WithError(err).Error("failed to authenticate")
85+
log.WithContext(ctx).WithFields(lf).WithField("apikey-url", viper.GetString("apikey-url")).WithError(err).Error("failed to authenticate")
7986
return 1
8087
}
8188

@@ -87,7 +94,7 @@ func Request(signals chan os.Signal, ready chan bool) int {
8794
HTTPClient: NewAuthenticatedClient(ctx, otelhttp.DefaultClient),
8895
}
8996

90-
c, _, err := websocket.Dial(ctx, viper.GetString("url"), options)
97+
c, _, err := websocket.Dial(ctx, gatewayUrl, options)
9198
if err != nil {
9299
log.WithContext(ctx).WithFields(lf).WithError(err).Error("Failed to connect to overmind API")
93100
return 1

cmd/root.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/bufbuild/connect-go"
1314
"github.com/google/uuid"
1415
"github.com/overmindtech/ovm-cli/tracing"
1516
"github.com/overmindtech/sdp-go"
@@ -33,7 +34,7 @@ var rootCmd = &cobra.Command{
3334
Long: `The ovm-cli allows direct access to the overmind API`,
3435
PreRun: func(cmd *cobra.Command, args []string) {
3536
// Bind these to viper
36-
err := viper.BindPFlags(cmd.PersistentFlags())
37+
err := viper.BindPFlags(cmd.Flags())
3738
if err != nil {
3839
log.WithError(err).Fatal("could not bind `root` flags")
3940
}
@@ -53,7 +54,23 @@ func Execute() {
5354
func ensureToken(ctx context.Context, signals chan os.Signal) (context.Context, error) {
5455
// shortcut if we already have a token set
5556
if viper.GetString("token") != "" {
56-
return context.WithValue(ctx, sdp.UserTokenContextKey{}, viper.GetString("token")), nil
57+
token := viper.GetString("token")
58+
if strings.HasPrefix(token, "ovm_api_") {
59+
// exchange api token for JWT
60+
client := UnauthenticatedApiKeyClient(ctx)
61+
resp, err := client.ExchangeKeyForToken(ctx, &connect.Request[sdp.ExchangeKeyForTokenRequest]{
62+
Msg: &sdp.ExchangeKeyForTokenRequest{
63+
ApiKey: token,
64+
},
65+
})
66+
if err != nil {
67+
return ctx, fmt.Errorf("error authenticating the API token: %w", err)
68+
}
69+
token = resp.Msg.AccessToken
70+
} else {
71+
return ctx, errors.New("token does not match pattern 'ovm_api_*'")
72+
}
73+
return context.WithValue(ctx, sdp.UserTokenContextKey{}, token), nil
5774
}
5875

5976
// Check to see if the URL is secure
@@ -162,16 +179,18 @@ func init() {
162179
rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace")
163180

164181
// api endpoint
165-
rootCmd.PersistentFlags().String("url", "https://api.prod.overmind.tech/api/gateway", "The overmind API endpoint")
182+
rootCmd.PersistentFlags().String("url", "https://api.prod.overmind.tech", "The overmind API endpoint")
183+
rootCmd.PersistentFlags().String("gateway-url", "", "The overmind Gateway endpoint (defaults to /api/gateway on --url)")
166184

167185
// authorization
168-
rootCmd.PersistentFlags().String("auth0-client-id", "j3LylZtIosVPZtouKI8WuVHmE6Lluva1", "OAuth Client ID to use when connecting with auth")
169-
rootCmd.PersistentFlags().String("auth0-domain", "om-prod.eu.auth0.com", "Auth0 domain to connect to")
170-
rootCmd.PersistentFlags().String("token", "", "The token to use for authentication")
186+
rootCmd.PersistentFlags().String("token", "", "The API token to use for authentication, also read from OVM_TOKEN environment variable")
171187
err := viper.BindEnv("token", "OVM_TOKEN", "TOKEN")
172188
if err != nil {
173189
log.WithError(err).Fatal("could not bind token")
174190
}
191+
rootCmd.PersistentFlags().String("apikey-url", "", "The overmind API Keys endpoint (defaults to --url)")
192+
rootCmd.PersistentFlags().String("auth0-client-id", "j3LylZtIosVPZtouKI8WuVHmE6Lluva1", "OAuth Client ID to use when connecting with auth")
193+
rootCmd.PersistentFlags().String("auth0-domain", "om-prod.eu.auth0.com", "Auth0 domain to connect to")
175194

176195
// tracing
177196
rootCmd.PersistentFlags().String("honeycomb-api-key", "", "If specified, configures opentelemetry libraries to submit traces to honeycomb")

0 commit comments

Comments
 (0)