Skip to content

Commit

Permalink
Add device flow for obtaining token and auth command
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
  • Loading branch information
alexellis committed Aug 15, 2023
1 parent 3b45b15 commit 4a7c7ee
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 31 deletions.
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,34 @@ Or, run this command in a shell before executing any of the CLI commands.

## Obtain a token from GitHub for your own account

Obtain a Personal Access Token (PAT) from [https://github.com/settings/tokens](https://github.com/settings/tokens)
You can perform this step using GitHub's Device Flow:

Save it as for example: `~/pat.txt`
```bash
actuated-cli auth
```

Or you can obtain a Personal Access Token (PAT) manually from [https://github.com/settings/tokens](https://github.com/settings/tokens)

In either case, saving the token to `$HOME/.actuated/PAT` will mean you can avoid having to pass in the `--token` flag to each command.

## View queued jobs

```bash
actuated-cli jobs \
--token ~/reader.txt \
--owner actuated-samples
```

## View runners for organization

```bash
actuated-cli runners \
--token ~/reader.txt \
--owner actuated-samples
```

## View SSH sessions available:

```bash
actuated-cli ssh \
--token ~/reader.txt \
ssh ls
```

Expand All @@ -61,31 +64,27 @@ Connect to the first available session from your account:

```bash
actuated-cli ssh \
--token ~/reader.txt \
ssh connect
```

Connected to the second session in the list:

```bash
actuated-cli ssh \
--token ~/reader.txt \
ssh connect 2
```

Connect to a specific session by hostname:

```bash
actuated-cli ssh \
--token ~/reader.txt \
ssh connect runner1
```

Connect to a specific session with a host prefix:

```bash
actuated-cli ssh \
--token ~/reader.txt \
ssh connect 6aafd
```

Expand All @@ -100,7 +99,6 @@ View the serial console and systemd output of the VMs launched on a specific ser

```bash
actuated-cli logs \
--token ~/reader.txt \
--host runner1 \
--owner actuated-samples \
--age 15m
Expand All @@ -112,7 +110,6 @@ You can also get the logs for a specific runner by using the `--id` flag.

```bash
actuated-cli logs \
--token ~/reader.txt \
--host runner1 \
--owner actuated-samples \
--id ea5c285282620927689d90af3cfa3be2d5e2d004
Expand All @@ -126,7 +123,6 @@ View VM launch times, etc.

```bash
actuated-cli agent-logs \
--token ~/reader.txt \
--host runner1 \
--owner actuated-samples \
--age 15m
Expand All @@ -142,7 +138,6 @@ Run with sparingly because it will launch one VM per job queued.

```bash
actuated-cli repair \
--token ~/reader.txt \
--owner actuated-samples
```

Expand All @@ -152,7 +147,6 @@ Restart the agent by sending a `kill -9` signal:

```bash
actuated-cli restart \
--token ~/reader.txt \
--owner actuated-samples \
--host runner1
```
Expand All @@ -163,7 +157,6 @@ Reboot the machine, if in an unrecoverable position:

```bash
actuated-cli restart \
--token ~/reader.txt \
--owner actuated-samples \
--host runner1 \
--reboot
Expand Down
1 change: 0 additions & 1 deletion cmd/agent-logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ func makeAgentLogs() *cobra.Command {

cmd.Flags().StringP("owner", "o", "", "Owner for the logs")
cmd.Flags().String("host", "", "Host or name of server as displayed in actuated")
cmd.Flags().BoolP("staff", "s", false, "List as a staff user")
cmd.Flags().DurationP("age", "a", time.Minute*15, "Age of logs to fetch")

return cmd
Expand Down
129 changes: 129 additions & 0 deletions cmd/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// MIT License
// Copyright (c) 2023 Alex Ellis, OpenFaaS Ltd

package cmd

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"time"

"github.com/spf13/cobra"
)

func makeAuth() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Authenticate to GitHub to obtain a token and save it to $HOME/.actuated/PAT",
}

cmd.RunE = runAuthE

return cmd
}

func runAuthE(cmd *cobra.Command, args []string) error {

token := ""

clientID := "8c5dc5d9750ff2a8396a"

dcParams := url.Values{}
dcParams.Set("client_id", clientID)
dcParams.Set("redirect_uri", "http://127.0.0.1:31111/oauth/callback")
dcParams.Set("scope", "read:user,read:org,user:email")

req, err := http.NewRequest(http.MethodPost, "https://github.com/login/device/code", bytes.NewBuffer([]byte(dcParams.Encode())))

if err != nil {
return err
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

body, _ := io.ReadAll(res.Body)

if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, string(body))
}

auth := DeviceAuth{}

if err := json.Unmarshal(body, &auth); err != nil {
return err
}

fmt.Printf("Please visit: %s\n", auth.VerificationURI)
fmt.Printf("and enter the code: %s\n", auth.UserCode)

for i := 0; i < 60; i++ {
urlv := url.Values{}
urlv.Set("client_id", clientID)
urlv.Set("device_code", auth.DeviceCode)

urlv.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

req, err := http.NewRequest(http.MethodPost, "https://github.com/login/oauth/access_token", bytes.NewBuffer([]byte(urlv.Encode())))
if err != nil {
return err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

body, _ := io.ReadAll(res.Body)
parts, err := url.ParseQuery(string(body))
if err != nil {
return err
}
if parts.Get("error") == "authorization_pending" {
fmt.Println("Waiting for authorization...")
time.Sleep(time.Second * 5)
continue
} else if parts.Get("access_token") != "" {
// fmt.Println(parts)
token = parts.Get("access_token")

break
} else {
return fmt.Errorf("something went wrong")
}
}

const basePath = "$HOME/.actuated"
os.Mkdir(os.ExpandEnv(basePath), 0755)

if err := os.WriteFile(os.ExpandEnv(path.Join(basePath, "PAT")), []byte(token), 0644); err != nil {
return err
}

fmt.Printf("Access token written to: %s\n", os.ExpandEnv(path.Join(basePath, "PAT")))

return nil
}

// DeviceAuth is the device auth response from GitHub and is
// used to exchange for a personal access token
type DeviceAuth struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
1 change: 0 additions & 1 deletion cmd/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ func makeJobs() *cobra.Command {
cmd.RunE = runJobsE

cmd.Flags().StringP("owner", "o", "", "List jobs owned by this user")
cmd.Flags().BoolP("staff", "s", false, "List as a staff user")
cmd.Flags().Bool("json", false, "Request output in JSON format")

return cmd
Expand Down
1 change: 0 additions & 1 deletion cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ actuated-cli logs --owner=OWNER --host=HOST --id=ID

cmd.Flags().StringP("owner", "o", "", "List logs owned by this user")
cmd.Flags().String("host", "", "Host or name of server as displayed in actuated")
cmd.Flags().BoolP("staff", "s", false, "List as a staff user")
cmd.Flags().String("id", "", "ID variable for a specific runner VM hostname")
cmd.Flags().DurationP("age", "a", time.Minute*15, "Age of logs to fetch")

Expand Down
46 changes: 33 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ https://github.com/self-actuated/actuated-cli

root.PersistentFlags().String("token-value", "", "Personal Access Token")
root.PersistentFlags().StringP("token", "t", "", "File to read for Personal Access Token")
root.PersistentFlags().Bool("staff", false, "Execute the command as an actuated staff member")

root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if _, ok := os.LookupEnv("ACTUATED_URL"); !ok {
Expand All @@ -46,34 +47,53 @@ https://github.com/self-actuated/actuated-cli
root.AddCommand(makeUpgrade())

root.AddCommand(makeSSH())

root.AddCommand(makeAuth())

}

func Execute() error {
return root.Execute()
}

func getPat(cmd *cobra.Command) (string, error) {
pat, err := cmd.Flags().GetString("token-value")
if err != nil {
return "", err
}
if len(pat) > 0 {
return pat, nil
}
var (
pat,
patFile string
)

patFile, err := cmd.Flags().GetString("token")
if err != nil {
return "", err
if cmd.Flags().Changed("token-value") {
v, err := cmd.Flags().GetString("token-value")
if err != nil {
return "", err
}
pat = v
} else if cmd.Flags().Changed("token") {
v, err := cmd.Flags().GetString("token")
if err != nil {
return "", err
}
patFile = v
} else {
patFile = os.ExpandEnv("$HOME/.actuated/PAT")
}

if len(patFile) > 0 {
patData, err := os.ReadFile(os.ExpandEnv(patFile))
v, err := readPatFile(patFile)
if err != nil {
return "", err
}

pat = strings.TrimSpace(string(patData))
pat = v
}

return pat, nil
}

func readPatFile(filePath string) (string, error) {
patData, err := os.ReadFile(os.ExpandEnv(filePath))
if err != nil {
return "", err
}

return strings.TrimSpace(string(patData)), nil
}

0 comments on commit 4a7c7ee

Please sign in to comment.