Skip to content

Commit

Permalink
Add mint whoami (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylekthompson authored Dec 14, 2023
1 parent 9048cf0 commit 26845e9
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 2 deletions.
1 change: 1 addition & 0 deletions cmd/mint/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ func init() {
rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(debugCmd)
rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(whoamiCmd)
}
34 changes: 34 additions & 0 deletions cmd/mint/whoami.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"os"

"github.com/rwx-research/mint-cli/internal/cli"

"github.com/spf13/cobra"
)

var (
WhoamiJson bool

whoamiCmd = &cobra.Command{
PreRunE: func(cmd *cobra.Command, args []string) error {
return requireAccessToken()
},
RunE: func(cmd *cobra.Command, args []string) error {
err := service.Whoami(cli.WhoamiConfig{Json: WhoamiJson, Stdout: os.Stdout})
if err != nil {
return err
}

return nil

},
Short: "Outputs details about the access token in use",
Use: "whoami [flags]",
}
)

func init() {
whoamiCmd.Flags().BoolVar(&WhoamiJson, "json", false, "output JSON instead of a textual representation")
}
33 changes: 31 additions & 2 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func (c Client) InitiateRun(cfg InitiateRunConfig) (*InitiateRunResult, error) {
}, nil
}

// InitiateRun sends a request to Mint for starting a new run
// ObtainAuthCode requests a new one-time-use code to login on a device
func (c Client) ObtainAuthCode(cfg ObtainAuthCodeConfig) (*ObtainAuthCodeResult, error) {
endpoint := "/api/auth/codes"

Expand Down Expand Up @@ -176,7 +176,7 @@ func (c Client) ObtainAuthCode(cfg ObtainAuthCodeConfig) (*ObtainAuthCodeResult,
return &respBody, nil
}

// InitiateRun sends a request to Mint for starting a new run
// AcquireToken consumes the one-time-use code once authorized
func (c Client) AcquireToken(tokenUrl string) (*AcquireTokenResult, error) {
req, err := http.NewRequest(http.MethodGet, tokenUrl, bytes.NewBuffer(make([]byte, 0)))
if err != nil {
Expand All @@ -202,6 +202,35 @@ func (c Client) AcquireToken(tokenUrl string) (*AcquireTokenResult, error) {
return &respBody, nil
}

// Whoami provides details about the authenticated token
func (c Client) Whoami() (*WhoamiResult, error) {
endpoint := "/api/auth/whoami"

req, err := http.NewRequest(http.MethodGet, endpoint, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, errors.Wrap(err, "unable to create new HTTP request")
}

req.Header.Set("Content-Type", "application/json")

resp, err := c.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "HTTP request failed")
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return nil, errors.New(fmt.Sprintf("Unable to call Mint API - %s", resp.Status))
}

respBody := WhoamiResult{}
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return nil, errors.Wrap(err, "unable to parse API response")
}

return &respBody, nil
}

// extractErrorMessage is a small helper function for parsing an API error message
func extractErrorMessage(reader io.Reader) string {
errorStruct := struct {
Expand Down
30 changes: 30 additions & 0 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,34 @@ var _ = Describe("API Client", func() {
Expect(err).To(BeNil())
})
})

Describe("Whoami", func() {
It("makes the request", func() {
email := "[email protected]"
body := struct {
OrganizationSlug string `json:"organization_slug"`
TokenKind string `json:"token_kind"`
UserEmail *string `json:"user_email,omitempty"`
}{
OrganizationSlug: "some-org",
TokenKind: "personal_access_token",
UserEmail: &email,
}
bodyBytes, _ := json.Marshal(body)

roundTrip := func(req *http.Request) (*http.Response, error) {
Expect(req.URL.Path).To(Equal("/api/auth/whoami"))
return &http.Response{
Status: "200 OK",
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(bodyBytes)),
}, nil
}

c := api.Client{roundTrip}

_, err := c.Whoami()
Expect(err).To(BeNil())
})
})
})
6 changes: 6 additions & 0 deletions internal/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,9 @@ type AcquireTokenResult struct {
State string `json:"state"` // consumed, expired, authorized, pending
Token string `json:"token,omitempty"`
}

type WhoamiResult struct {
OrganizationSlug string `json:"organization_slug"`
TokenKind string `json:"token_kind"` // organization_access_token, personal_access_token
UserEmail *string `json:"user_email,omitempty"`
}
9 changes: 9 additions & 0 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,12 @@ func (c LoginConfig) Validate() error {

return nil
}

type WhoamiConfig struct {
Json bool
Stdout io.Writer
}

func (c WhoamiConfig) Validate() error {
return nil
}
1 change: 1 addition & 0 deletions internal/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type APIClient interface {
InitiateRun(api.InitiateRunConfig) (*api.InitiateRunResult, error)
ObtainAuthCode(api.ObtainAuthCodeConfig) (*api.ObtainAuthCodeResult, error)
AcquireToken(tokenUrl string) (*api.AcquireTokenResult, error)
Whoami() (*api.WhoamiResult, error)
}

type SSHClient interface {
Expand Down
25 changes: 25 additions & 0 deletions internal/cli/service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"encoding/json"
"fmt"
"io"
"path/filepath"
Expand Down Expand Up @@ -208,6 +209,30 @@ func (s Service) Login(cfg LoginConfig) error {
}
}

func (s Service) Whoami(cfg WhoamiConfig) error {
result, err := s.APIClient.Whoami()
if err != nil {
return errors.Wrap(err, "unable to determine details about the access token")
}

if cfg.Json {
encoded, err := json.MarshalIndent(result, "", " ")
if err != nil {
return errors.Wrap(err, "unable to JSON encode the result")
}

fmt.Fprint(cfg.Stdout, string(encoded))
} else {
fmt.Fprintf(cfg.Stdout, "Token Kind: %v\n", strings.ReplaceAll(result.TokenKind, "_", " "))
fmt.Fprintf(cfg.Stdout, "Organization: %v\n", result.OrganizationSlug)
if result.UserEmail != nil {
fmt.Fprintf(cfg.Stdout, "User: %v\n", *result.UserEmail)
}
}

return nil
}

// taskDefinitionsFromPaths opens each file specified in `paths` and reads their content as a string.
// No validation takes place here.
func (s Service) taskDefinitionsFromPaths(paths []string) ([]api.TaskDefinition, error) {
Expand Down
148 changes: 148 additions & 0 deletions internal/cli/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,4 +630,152 @@ AAAEC6442PQKevgYgeT0SIu9zwlnEMl6MF59ZgM+i0ByMv4eLJPqG3xnZcEQmktHj/GY2i
})
})
})

Describe("whoami", func() {
var (
stdout strings.Builder
)

BeforeEach(func() {
stdout = strings.Builder{}
})

Context("when outputting json", func() {
Context("when the request fails", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
return nil, errors.New("uh oh can't figure out who you are")
}
})

It("returns an error", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: true,
Stdout: &stdout,
})

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unable to determine details about the access token"))
Expect(err.Error()).To(ContainSubstring("can't figure out who you are"))
})
})

Context("when there is an email", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
email := "[email protected]"
return &api.WhoamiResult{
TokenKind: "personal_access_token",
OrganizationSlug: "rwx",
UserEmail: &email,
}, nil
}
})

It("writes the token kind, organization, and user", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: true,
Stdout: &stdout,
})

Expect(err).NotTo(HaveOccurred())
Expect(stdout.String()).To(ContainSubstring(`"token_kind": "personal_access_token"`))
Expect(stdout.String()).To(ContainSubstring(`"organization_slug": "rwx"`))
Expect(stdout.String()).To(ContainSubstring(`"user_email": "[email protected]"`))
})
})

Context("when there is not an email", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
return &api.WhoamiResult{
TokenKind: "organization_access_token",
OrganizationSlug: "rwx",
}, nil
}
})

It("writes the token kind and organization", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: true,
Stdout: &stdout,
})

Expect(err).NotTo(HaveOccurred())
Expect(stdout.String()).To(ContainSubstring(`"token_kind": "organization_access_token"`))
Expect(stdout.String()).To(ContainSubstring(`"organization_slug": "rwx"`))
Expect(stdout.String()).NotTo(ContainSubstring(`"user_email"`))
})
})
})

Context("when outputting plaintext", func() {
Context("when the request fails", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
return nil, errors.New("uh oh can't figure out who you are")
}
})

It("returns an error", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: false,
Stdout: &stdout,
})

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("unable to determine details about the access token"))
Expect(err.Error()).To(ContainSubstring("can't figure out who you are"))
})
})

Context("when there is an email", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
email := "[email protected]"
return &api.WhoamiResult{
TokenKind: "personal_access_token",
OrganizationSlug: "rwx",
UserEmail: &email,
}, nil
}
})

It("writes the token kind, organization, and user", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: false,
Stdout: &stdout,
})

Expect(err).NotTo(HaveOccurred())
Expect(stdout.String()).To(ContainSubstring("Token Kind: personal access token"))
Expect(stdout.String()).To(ContainSubstring("Organization: rwx"))
Expect(stdout.String()).To(ContainSubstring("User: [email protected]"))
})
})

Context("when there is not an email", func() {
BeforeEach(func() {
mockAPI.MockWhoami = func() (*api.WhoamiResult, error) {
return &api.WhoamiResult{
TokenKind: "organization_access_token",
OrganizationSlug: "rwx",
}, nil
}
})

It("writes the token kind and organization", func() {
err := service.Whoami(cli.WhoamiConfig{
Json: false,
Stdout: &stdout,
})

Expect(err).NotTo(HaveOccurred())
Expect(stdout.String()).To(ContainSubstring("Token Kind: organization access token"))
Expect(stdout.String()).To(ContainSubstring("Organization: rwx"))
Expect(stdout.String()).NotTo(ContainSubstring("User:"))
})
})
})
})
})
9 changes: 9 additions & 0 deletions internal/mocks/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type API struct {
MockGetDebugConnectionInfo func(runID string) (api.DebugConnectionInfo, error)
MockObtainAuthCode func(api.ObtainAuthCodeConfig) (*api.ObtainAuthCodeResult, error)
MockAcquireToken func(tokenUrl string) (*api.AcquireTokenResult, error)
MockWhoami func() (*api.WhoamiResult, error)
}

func (c *API) InitiateRun(cfg api.InitiateRunConfig) (*api.InitiateRunResult, error) {
Expand Down Expand Up @@ -43,3 +44,11 @@ func (c *API) AcquireToken(tokenUrl string) (*api.AcquireTokenResult, error) {

return nil, errors.New("MockAcquireToken was not configured")
}

func (c *API) Whoami() (*api.WhoamiResult, error) {
if c.MockWhoami != nil {
return c.MockWhoami()
}

return nil, errors.New("MockWhoami was not configured")
}

0 comments on commit 26845e9

Please sign in to comment.