Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Border0 FKA Mysocketio Commands + Docs #1768

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 154 additions & 22 deletions border0_api/border0.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,175 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"github.com/cenkalti/backoff"
"github.com/golang-jwt/jwt"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/srl-labs/containerlab/nodes"
"github.com/srl-labs/containerlab/utils"
"gopkg.in/yaml.v2"
)

const (
apiUrl = "https://api.border0.com/api/v1"
portalUrl = "https://portal.border0.com"
ENV_NAME_BORDER0_ADMIN_TOKEN = "BORDER0_ADMIN_TOKEN"
ENV_NAME_BORDER0_API = "BORDER0_API"
ENV_NAME_BORDER0_PORTAL = "BORDER0_PORTAL"
)

var supportedSockTypes = []string{"ssh", "tls", "http", "https"}

// to avoid multiple token lookups etc. we'll cache the token.
var tokenCache = ""

// Login performs a login to border0.com and stores the retrieved the access-token in the cwd.
func Login(ctx context.Context, email, password string) error {
// if password not set read from terminal
if password == "" {
type deviceAuthorization struct {
Token string `json:"token,omitempty"`
}

type deviceAuthorizationStatus struct {
Token string `json:"token,omitempty"`
State string `json:"state,omitempty"`
}

func createDeviceAuthorization(ctx context.Context) (string, error) {
deviceAuthResp := &deviceAuthorization{}
err := Request(ctx, http.MethodPost, "device_authorizations", deviceAuthResp, nil, false, "")
if err != nil {
return "", err
}

return deviceAuthResp.Token, nil
}

func getDeviceAuthorizationStatus(ctx context.Context, deviceAuthToken string) (*deviceAuthorizationStatus, error) {
deviceAuthStatusResp := &deviceAuthorizationStatus{}

err := Request(ctx, http.MethodGet, "device_authorizations", deviceAuthStatusResp, nil, false, deviceAuthToken)
if err != nil {
return nil, fmt.Errorf("failed to retrieve Border0 device authorization status: %v", err)
}

return deviceAuthStatusResp, nil
}

func handleDeviceAuthorization(ctx context.Context, deviceAuthToken string, disableBrowser bool) (string, error) {
deviceAuthJWT, _ := jwt.Parse(deviceAuthToken, nil)
if deviceAuthJWT == nil {
return "", fmt.Errorf("failed to decode Border0 device authorization token")
}
claims := deviceAuthJWT.Claims.(jwt.MapClaims)
deviceIdentifier := fmt.Sprint(claims["identifier"])

// Try opening the system's browser automatically. The error is ignored because the desired behavior of the
// handler is the same regardless of whether opening the browser fails or succeeds -- we still print the URL.
// This is desirable because in the event opening the browser succeeds, the customer may still accidentally
// close the new tab / browser session, or may want to authenticate in a different browser / session. In the
// event that opening the browser fails, the customer may still complete authenticating by navigating to the
// URL in a different device.

url := fmt.Sprintf("%s/login?device_identifier=%v", getPortalUrl(), url.QueryEscape(deviceIdentifier))

fmt.Printf("Please navigate to the URL below in order to complete the login process:\n%s\n", url)

// check if the disableBrowser flag is set
if !disableBrowser {
// check if we're on DARWIN and if we're running as sudo, if so, make sure we open the browser as the user
// this prevents folks from not having access to credentials , sessions, etc
sudoUsername := os.Getenv("SUDO_USER")
sudoAttempt := false
if runtime.GOOS == "darwin" && sudoUsername != "" {
err := exec.Command("sudo", "-u", sudoUsername, "open", url).Run()
if err == nil {
// If for some reason this failed, we'll try again to standard way
sudoAttempt = true
}
}
if !sudoAttempt {
_ = open.Run(url)
}
}

exponentialBackoff := backoff.NewExponentialBackOff()
exponentialBackoff.InitialInterval = 1 * time.Second
exponentialBackoff.MaxInterval = 5 * time.Second
exponentialBackoff.Multiplier = 1.3
exponentialBackoff.MaxElapsedTime = 3 * time.Minute

var token *deviceAuthorizationStatus

retryFn := func() error {
var err error
password, err = utils.ReadPasswordFromTerminal()
token, err = getDeviceAuthorizationStatus(ctx, deviceAuthToken)
if err != nil {
return err
}
if token.Token == "" || token.State == "not_authorized" {
return fmt.Errorf("device authorization code is not authorized")
}
return nil
}
// prepare a LoginRequest
loginReq := &LoginRequest{
Email: email,
Password: password,
}
// init a LoginResponse
loginResp := &LoginResponse{}

// execute the request
err := Request(ctx, http.MethodPost, "login", loginResp, loginReq, false)
err := backoff.Retry(retryFn, exponentialBackoff)
if err != nil {
return err
return "", fmt.Errorf("failed to log you in, make sure that you have authenticated using the link above: %v", err)
}

err = writeToken(loginResp.Token)
if err != nil {
fmt.Println("Login successful!\n")
return token.Token, nil
}

// Login performs a login to border0.com and stores the retrieved the access-token in the cwd.
func Login(ctx context.Context, email, password string, disableBrowser bool) error {
var token string

// if email is not set, we default to Border0's OAuth2 Device Authorization Flow.
if email == "" {
deviceAuthToken, err := createDeviceAuthorization(ctx)
if err != nil {
return fmt.Errorf("failed to initiate Border0 device authorization flow: %v", err)
}

token, err = handleDeviceAuthorization(ctx, deviceAuthToken, disableBrowser)
if err != nil {
return fmt.Errorf("failed to authenticate you against Border0: %v", err)
}
} else {
// if password not set read from terminal
if password == "" {
var err error
password, err = utils.ReadPasswordFromTerminal()
if err != nil {
return err
}
}
// prepare a LoginRequest
loginReq := &LoginRequest{
Email: email,
Password: password,
}
// init a LoginResponse
loginResp := &LoginResponse{}

// execute the request
err := Request(ctx, http.MethodPost, "login", loginResp, loginReq, false, "")
if err != nil {
return err
}

token = loginResp.Token
}

if err := writeToken(token); err != nil {
return err
}
return nil
Expand All @@ -72,6 +193,14 @@ func getApiUrl() string {
}
}

func getPortalUrl() string {
if os.Getenv(ENV_NAME_BORDER0_PORTAL) != "" {
return os.Getenv(ENV_NAME_BORDER0_PORTAL)
} else {
return portalUrl
}
}

// getToken retrieved the border0 access-token as a string.
func getToken() (string, error) {
// return the cached token
Expand Down Expand Up @@ -154,7 +283,7 @@ func tokenfile() (string, error) {
}
}
// no valid file found, return error
return "", fmt.Errorf("no access-token found, please login to border0.com first e.g use `containerlab tools border0 login --email <BORDER0-USER-MAIL-ADDRESS>`")
return "", fmt.Errorf("no access-token found, please login to border0.com first e.g use `containerlab tools border0 login`")
}

// cwdTokenFilePath get the abspath of the token file in the current working directory.
Expand All @@ -169,21 +298,24 @@ func cwdTokenFilePath() string {
// GetExistingPolicies retrieved the existing policies from border0.com.
func GetExistingPolicies(ctx context.Context) ([]Policy, error) {
var policies []Policy
err := Request(ctx, http.MethodGet, "policies", &policies, nil, true)
err := Request(ctx, http.MethodGet, "policies", &policies, nil, true, "")
if err != nil {
return nil, err
}
return policies, nil
}

// Request is the helper function that handels the http requests, as well as the marshalling of request structs and unmarshalling of responses.
func Request(ctx context.Context, method string, url string, targetStruct interface{}, data interface{}, requireAccessToken bool) error {
func Request(ctx context.Context, method string, url string, targetStruct interface{}, data interface{}, requireAccessToken bool, token string) error {
jv, _ := json.Marshal(data)
body := bytes.NewBuffer(jv)

req, _ := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s/%s", getApiUrl(), url), body)

token := ""
if token != "" {
req.Header.Add("x-access-token", strings.TrimSpace(token))
}

// try to find the token in the environment
if requireAccessToken {
var err error
Expand Down Expand Up @@ -243,7 +375,7 @@ func Request(ctx context.Context, method string, url string, targetStruct interf
func RefreshLogin(ctx context.Context) error {
t := &LoginRefreshResponse{}
log.Debug("Validating and refreshing border0.com token")
err := Request(ctx, http.MethodPost, "login/refresh", t, nil, true)
err := Request(ctx, http.MethodPost, "login/refresh", t, nil, true, "")
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion border0_api/border0_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestLogin(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO()
if err := Login(ctx, tt.args.email, tt.args.password); (err != nil) != tt.wantErr {
if err := Login(ctx, tt.args.email, tt.args.password, false); (err != nil) != tt.wantErr {
t.Errorf("Login() error = %v, wantErr %v", err, tt.wantErr)
}
})
Expand Down
17 changes: 15 additions & 2 deletions cmd/border0.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,28 @@ import (
var (
border0Email string
border0Password string

border0DisableBrowser bool
)

func init() {
toolsCmd.AddCommand(border0Cmd)

border0Cmd.AddCommand(border0LoginCmd)

border0LoginCmd.Flags().BoolVarP(&border0DisableBrowser, "disable-browser", "b", false, "Disable opening the browser")

// Programmatic user authentication for the Border0 service was deprecated on 11/2023,
// so we hide the email and password flags though we keep them around for backwards
// compatibility because some containerlabs users have been allowlisted for programmatic
// authentication. Note that as of 12/2023 no new allowlist requests are considered by
// Border0. Instead Border0 users who wish to integrate with containerlabs will need to
// use Border0 "admin tokens" i.e. service identities. For info on how to create tokens,
// see https://docs.border0.com/docs/creating-access-token
border0LoginCmd.Flags().StringVarP(&border0Email, "email", "e", "", "Email address")
border0LoginCmd.Flags().StringVarP(&border0Password, "password", "p", "", "Password")
_ = border0LoginCmd.MarkFlagRequired("email")
border0LoginCmd.Flags().MarkHidden("email")
border0LoginCmd.Flags().MarkHidden("password")
}

// border0Cmd represents the border0 command container.
Expand All @@ -40,6 +53,6 @@ var border0LoginCmd = &cobra.Command{
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

return border0_api.Login(ctx, border0Email, border0Password)
return border0_api.Login(ctx, border0Email, border0Password, border0DisableBrowser)
},
}
20 changes: 8 additions & 12 deletions docs/cmd/tools/border0/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@ The token is saved as `$PWD/.border0_token` file.

### Flags

#### email
With mandatory `--email | -e` flag user sets an email address used to register with border0.com service

#### password
The `--password | -p` sets the password for a user. If flag is not set, the prompt will appear on the terminal to allow for safe enter of the password.
#### disable-browser
The `--disable-browser | -b` prevents the command from attempting to open the browser in order to complete authentication with Border0. If the flag is set, the command will simply print the URL which you must navigate to, whether in the same device or a different device, in order to complete authentication.

### Examples

```bash
# Login with password entered from the prompt
containerlab tools border0.com login -e [email protected]
Password:
INFO[0000] Written border0.com token to a file /root/containerlab/.border0_token
containerlab tools border0 login

Please navigate to the URL below in order to complete the login process:
https://portal.border0.com/login?device_identifier=IjM1OTJkZGVmLTgzNTMtNDU4Yy04NjNkLTk1OTdhYjY0ZjFiOSI.ZW6BRw.Z9XlL0CtL7HkKTDX7GSp28d9mG0

Login successful

# Login with password passed as a flag
containerlab tools border0.com login -e [email protected] -p Pa$$word
Password:
INFO[0000] Written border0.com token to a file /root/containerlab/.border0_token
```

Expand Down
20 changes: 8 additions & 12 deletions docs/cmd/tools/mysocketio/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@ The token is saved as `$PWD/.mysocketio_token` file.

### Flags

#### email
With mandatory `--email | -e` flag user sets an email address used to register with mysocketio service

#### password
The `--password | -p` sets the password for a user. If flag is not set, the prompt will appear on the terminal to allow for safe enter of the password.
#### disable-browser
The `--disable-browser | -b` prevents the command from attempting to open the browser in order to complete authentication with Border0. If the flag is set, the command will simply print the URL which you must navigate to, whether in the same device or a different device, in order to complete authentication.

### Examples

```bash
# Login with password entered from the prompt
containerlab tools mysocketio login -e [email protected]
Password:
INFO[0000] Written mysocketio token to a file /root/containerlab/.mysocketio_token
containerlab tools mysocketio login

Please navigate to the URL below in order to complete the login process:
https://portal.border0.com/login?device_identifier=IjM1OTJkZGVmLTgzNTMtNDU4Yy04NjNkLTk1OTdhYjY0ZjFiOSI.ZW6BRw.Z9XlL0CtL7HkKTDX7GSp28d9mG0

Login successful

# Login with password passed as a flag
containerlab tools mysocketio login -e [email protected] -p Pa$$word
Password:
INFO[0000] Written mysocketio token to a file /root/containerlab/.mysocketio_token
```

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/a8m/envsubst v1.4.2
github.com/awalterschulze/gographviz v2.0.3+incompatible
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/containernetworking/plugins v1.3.0
github.com/containers/common v0.57.0
github.com/containers/podman/v4 v4.8.0
Expand All @@ -15,6 +16,7 @@ require (
github.com/docker/go-units v0.5.0
github.com/dustin/go-humanize v1.0.1
github.com/florianl/go-tc v0.4.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.4.0
Expand All @@ -33,6 +35,7 @@ require (
github.com/pmorjan/kmod v1.1.0
github.com/scrapli/scrapligo v1.2.0
github.com/sirupsen/logrus v1.9.3
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.8.0
github.com/steiler/acls v0.1.1
github.com/stretchr/testify v1.8.4
Expand Down
Loading