@@ -146,31 +146,54 @@ func readLocalToken(homeDir string, expectedScopes []string) (string, []string,
146146 return token .AccessToken , currentScopes , nil
147147}
148148
149- // ensureToken
150- func ensureToken (ctx context.Context , requiredScopes []string ) (context.Context , error ) {
151- // get a token from the api key if present
152- if viper .GetString ("api-key" ) != "" {
153- log .WithContext (ctx ).Debug ("using provided token for authentication" )
154- apiKey := viper .GetString ("api-key" )
155- if strings .HasPrefix (apiKey , "ovm_api_" ) {
156- // exchange api token for JWT
157- client := UnauthenticatedApiKeyClient (ctx )
158- resp , err := client .ExchangeKeyForToken (ctx , & connect.Request [sdp.ExchangeKeyForTokenRequest ]{
159- Msg : & sdp.ExchangeKeyForTokenRequest {
160- ApiKey : apiKey ,
161- },
162- })
163- if err != nil {
164- return ctx , fmt .Errorf ("error authenticating the API token: %w" , err )
165- }
166- log .WithContext (ctx ).Debug ("successfully authenticated" )
167- apiKey = resp .Msg .GetAccessToken ()
168- } else {
169- return ctx , errors .New ("OVM_API_KEY does not match pattern 'ovm_api_*'" )
149+ // Check whether or not a token has all of the required scopes. Returns a
150+ // boolean and an error which will be populated if we couldn't read the token
151+ func tokenHasAllScopes (token string , requiredScopes []string ) (bool , error ) {
152+ claims , err := extractClaims (token )
153+
154+ if err != nil {
155+ return false , fmt .Errorf ("error extracting claims from token: %w" , err )
156+ }
157+
158+ // Check that the token has the right scopes
159+ for _ , scope := range requiredScopes {
160+ if ! claims .HasScope (scope ) {
161+ return false , nil
162+ }
163+ }
164+
165+ return true , nil
166+ }
167+
168+ // Gets a token using an API key
169+ func getAPIKeyToken (ctx context.Context , apiKey string ) (string , error ) {
170+ log .WithContext (ctx ).Debug ("using provided token for authentication" )
171+
172+ var accessToken string
173+
174+ if strings .HasPrefix (apiKey , "ovm_api_" ) {
175+ // exchange api token for JWT
176+ client := UnauthenticatedApiKeyClient (ctx )
177+ resp , err := client .ExchangeKeyForToken (ctx , & connect.Request [sdp.ExchangeKeyForTokenRequest ]{
178+ Msg : & sdp.ExchangeKeyForTokenRequest {
179+ ApiKey : apiKey ,
180+ },
181+ })
182+ if err != nil {
183+ return "" , fmt .Errorf ("error authenticating the API token: %w" , err )
170184 }
171- return context .WithValue (ctx , sdp.UserTokenContextKey {}, apiKey ), nil
185+ log .WithContext (ctx ).Debug ("successfully authenticated" )
186+ accessToken = resp .Msg .GetAccessToken ()
187+ } else {
188+ return "" , errors .New ("OVM_API_KEY does not match pattern 'ovm_api_*'" )
172189 }
173190
191+ return accessToken , nil
192+ }
193+
194+ // Gets a token from Oauth with the required scopes. This method will also cache
195+ // that token locally for use later, and will use the cached token if possible
196+ func getOauthToken (ctx context.Context , requiredScopes []string ) (string , error ) {
174197 var localScopes []string
175198
176199 // Check for a locally saved token in ~/.overmind
@@ -182,92 +205,116 @@ func ensureToken(ctx context.Context, requiredScopes []string) (context.Context,
182205 if err != nil {
183206 log .WithContext (ctx ).Debugf ("Error reading local token, ignoring: %v" , err )
184207 } else {
185- return context .WithValue (ctx , sdp.UserTokenContextKey {}, localToken ), nil
208+ // If we already have the right scopes, return the token
209+ return localToken , nil
186210 }
187211 }
188212
213+ // If we need to get a new token, request the required scopes on top of
214+ // whatever ones the current local, valid token has so that we don't
215+ // keep replacing it
216+
189217 // Check to see if the URL is secure
190218 appurl := viper .GetString ("url" )
191219 parsed , err := url .Parse (appurl )
192220 if err != nil {
193221 log .WithContext (ctx ).WithError (err ).Error ("Failed to parse --url" )
194- return ctx , fmt .Errorf ("error parsing --url: %w" , err )
195- }
196-
197- if parsed .Scheme == "wss" || parsed .Scheme == "https" || parsed .Hostname () == "localhost" {
198- // If we need to get a new token, request the required scopes on top of
199- // whatever ones the current local, valid token has so that we don't
200- // keep replacing it
201- requestScopes := append (requiredScopes , localScopes ... )
202-
203- // Authenticate using the oauth device authorization flow
204- config := oauth2.Config {
205- ClientID : viper .GetString ("cli-auth0-client-id" ),
206- Endpoint : oauth2.Endpoint {
207- AuthURL : fmt .Sprintf ("https://%v/authorize" , viper .GetString ("cli-auth0-domain" )),
208- TokenURL : fmt .Sprintf ("https://%v/oauth/token" , viper .GetString ("cli-auth0-domain" )),
209- DeviceAuthURL : fmt .Sprintf ("https://%v/oauth/device/code" , viper .GetString ("cli-auth0-domain" )),
210- },
211- Scopes : requestScopes ,
212- }
222+ return "" , fmt .Errorf ("error parsing --url: %w" , err )
223+ }
213224
214- deviceCode , err := config .DeviceAuth (ctx , oauth2 .SetAuthURLParam ("audience" , "https://api.overmind.tech" ))
215- if err != nil {
216- return ctx , fmt .Errorf ("error getting device code: %w" , err )
217- }
225+ if ! (parsed .Scheme == "wss" || parsed .Scheme == "https" || parsed .Hostname () == "localhost" ) {
226+ return "" , fmt .Errorf ("target URL (%v) is insecure" , parsed )
227+ }
228+ // If we need to get a new token, request the required scopes on top of
229+ // whatever ones the current local, valid token has so that we don't
230+ // keep replacing it
231+ requestScopes := append (requiredScopes , localScopes ... )
232+
233+ // Authenticate using the oauth device authorization flow
234+ config := oauth2.Config {
235+ ClientID : viper .GetString ("cli-auth0-client-id" ),
236+ Endpoint : oauth2.Endpoint {
237+ AuthURL : fmt .Sprintf ("https://%v/authorize" , viper .GetString ("cli-auth0-domain" )),
238+ TokenURL : fmt .Sprintf ("https://%v/oauth/token" , viper .GetString ("cli-auth0-domain" )),
239+ DeviceAuthURL : fmt .Sprintf ("https://%v/oauth/device/code" , viper .GetString ("cli-auth0-domain" )),
240+ },
241+ Scopes : requestScopes ,
242+ }
243+
244+ deviceCode , err := config .DeviceAuth (ctx , oauth2 .SetAuthURLParam ("audience" , "https://api.overmind.tech" ))
245+ if err != nil {
246+ return "" , fmt .Errorf ("error getting device code: %w" , err )
247+ }
248+
249+ fmt .Printf ("Go to %v and verify this code: %v\n " , deviceCode .VerificationURIComplete , deviceCode .UserCode )
250+
251+ token , err := config .DeviceAccessToken (ctx , deviceCode )
252+ if err != nil {
253+ fmt .Printf (": %v\n " , err )
254+ return "" , fmt .Errorf ("Error exchanging Device Code for for access token: %w" , err )
255+ }
218256
219- fmt . Printf ( "Go to %v and verify this code: %v \n " , deviceCode . VerificationURIComplete , deviceCode . UserCode )
257+ log . WithContext ( ctx ). Info ( "Authenticated successfully ✅" )
220258
221- token , err := config .DeviceAccessToken (ctx , deviceCode )
259+ // Save the token locally
260+ if home , err := os .UserHomeDir (); err == nil {
261+ // Create the directory if it doesn't exist
262+ err = os .MkdirAll (filepath .Join (home , ".overmind" ), 0700 )
222263 if err != nil {
223- fmt .Printf (": %v\n " , err )
224- return ctx , fmt .Errorf ("Error exchanging Device Code for for access token: %w" , err )
264+ log .WithContext (ctx ).WithError (err ).Error ("Failed to create ~/.overmind directory" )
225265 }
226266
227- // Check that we actually got the claims we asked for. If you don't have
228- // permission auth0 will just not assign those scopes rather than fail
229- claims , err := extractClaims (token .AccessToken )
230-
267+ // Write the token to a file
268+ path := filepath .Join (home , ".overmind" , "token.json" )
269+ file , err := os .Create (path )
231270 if err != nil {
232- return ctx , fmt . Errorf ("error extracting claims from token: %w " , err )
271+ log . WithContext ( ctx ). WithError ( err ). Errorf ("Failed to create token file at %v " , path )
233272 }
234273
235- for _ , scope := range requiredScopes {
236- if ! claims . HasScope ( scope ) {
237- return ctx , fmt . Errorf ( "authenticated successfully, but you don't have the required permission: '%v'" , scope )
238- }
274+ // Encode the token
275+ err = json . NewEncoder ( file ). Encode ( token )
276+ if err != nil {
277+ log . WithContext ( ctx ). WithError ( err ). Errorf ( "Failed to encode token file at %v" , path )
239278 }
240279
241- log .WithContext (ctx ).Info ("Authenticated successfully ✅" )
280+ log .WithContext (ctx ).Debugf ("Saved token to %v" , path )
281+ }
242282
243- // Save the token locally
244- if home , err := os .UserHomeDir (); err == nil {
245- // Create the directory if it doesn't exist
246- err = os .MkdirAll (filepath .Join (home , ".overmind" ), 0700 )
247- if err != nil {
248- log .WithContext (ctx ).WithError (err ).Error ("Failed to create ~/.overmind directory" )
249- }
283+ return token .AccessToken , nil
284+ }
250285
251- // Write the token to a file
252- path := filepath .Join (home , ".overmind" , "token.json" )
253- file , err := os .Create (path )
254- if err != nil {
255- log .WithContext (ctx ).WithError (err ).Errorf ("Failed to create token file at %v" , path )
256- }
286+ // ensureToken
287+ func ensureToken (ctx context.Context , requiredScopes []string ) (context.Context , error ) {
288+ var accessToken string
289+ var err error
257290
258- // Encode the token
259- err = json .NewEncoder (file ).Encode (token )
260- if err != nil {
261- log .WithContext (ctx ).WithError (err ).Errorf ("Failed to encode token file at %v" , path )
262- }
291+ // get a token from the api key if present
292+ if apiKey := viper .GetString ("api-key" ); apiKey != "" {
293+ accessToken , err = getAPIKeyToken (ctx , apiKey )
294+ } else {
295+ accessToken , err = getOauthToken (ctx , requiredScopes )
296+ }
263297
264- log .WithContext (ctx ).Debugf ("Saved token to %v" , path )
265- }
298+ if err != nil {
299+ return ctx , fmt .Errorf ("error getting token: %w" , err )
300+ }
301+
302+ // Check that we actually got the claims we asked for. If you don't have
303+ // permission auth0 will just not assign those scopes rather than fail
304+ claims , err := extractClaims (accessToken )
266305
267- // Set the token
268- return context .WithValue (ctx , sdp.UserTokenContextKey {}, token .AccessToken ), nil
306+ if err != nil {
307+ return ctx , fmt .Errorf ("error extracting claims from token: %w" , err )
308+ }
309+
310+ for _ , scope := range requiredScopes {
311+ if ! claims .HasScope (scope ) {
312+ return ctx , fmt .Errorf ("authenticated successfully, but you don't have the required permission: '%v'" , scope )
313+ }
269314 }
270- return ctx , fmt .Errorf ("no OVM_API_KEY configured and target URL (%v) is insecure" , parsed )
315+
316+ // Add the token to the context
317+ return context .WithValue (ctx , sdp.UserTokenContextKey {}, accessToken ), nil
271318}
272319
273320// getChangeUuid returns the UUID of a change, as selected by --uuid or --change, or a state with the specified status and having --ticket-link
@@ -302,16 +349,16 @@ func getChangeUuid(ctx context.Context, expectedStatus sdp.ChangeStatus, errNotF
302349 // Finally look through all open changes to find one with a matching ticket link
303350 client := AuthenticatedChangesClient (ctx )
304351
305- var maybeChangeUuid * uuid.UUID
306352 changesList , err := client .ListChangesByStatus (ctx , & connect.Request [sdp.ListChangesByStatusRequest ]{
307353 Msg : & sdp.ListChangesByStatusRequest {
308354 Status : expectedStatus ,
309355 },
310356 })
311357 if err != nil {
312- return uuid .Nil , fmt .Errorf ("failed to searching for existing changes: %w" , err )
358+ return uuid .Nil , fmt .Errorf ("failed to search for existing changes: %w" , err )
313359 }
314360
361+ var maybeChangeUuid * uuid.UUID
315362 for _ , c := range changesList .Msg .GetChanges () {
316363 if c .GetProperties ().GetTicketLink () == ticketLink {
317364 maybeChangeUuid = c .GetMetadata ().GetUUIDParsed ()
0 commit comments