Skip to content

Commit

Permalink
add world cli login for deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
zulkhair committed May 16, 2024
1 parent 03aa9eb commit cfc675a
Show file tree
Hide file tree
Showing 14 changed files with 686 additions and 69 deletions.
2 changes: 1 addition & 1 deletion cmd/world/cardinal/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ var devCmd = &cobra.Command{
go func() {
err := execCmd.Wait()
if err != nil {
logger.Error(eris.Wrap(err, "Cardinal process stopped"))
logger.Warn(eris.Wrap(err, "Cardinal process stopped"))
}

// if process exited, send signal to StopChan
Expand Down
3 changes: 2 additions & 1 deletion cmd/world/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ func init() {
// Register base commands
doctorCmd := getDoctorCmd(os.Stdout)
createCmd := getCreateCmd(os.Stdout)
rootCmd.AddCommand(createCmd, doctorCmd, versionCmd)
loginCmd := getLoginCmd()

Check failure on line 61 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Go

undefined: getLoginCmd) (typecheck)

Check failure on line 61 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Go

undefined: getLoginCmd

Check failure on line 61 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Unit & Coverage

undefined: getLoginCmd

Check failure on line 61 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Go

undefined: getLoginCmd

Check failure on line 61 in cmd/world/root/root.go

View workflow job for this annotation

GitHub Actions / Unit & Coverage

undefined: getLoginCmd
rootCmd.AddCommand(createCmd, doctorCmd, versionCmd, loginCmd)

// Register subcommands
rootCmd.AddCommand(cardinal.BaseCmd)
Expand Down
124 changes: 124 additions & 0 deletions cmd/world/root/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ package root

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/user"
"strings"
"testing"
"time"

"github.com/spf13/cobra"
tassert "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"

"pkg.world.dev/world-cli/cmd/world/cardinal"
"pkg.world.dev/world-cli/common/login"
)

type healthResponse struct {
Expand Down Expand Up @@ -268,3 +273,122 @@ func TestCheckLatestVersion(t *testing.T) {
assert.Error(t, err, "error parsing current version: Malformed version: wrong format")
})
}

func TestGenerateTokenName(t *testing.T) {
// Attempt to generate a token name
name, err := generateTokenName()

Check failure on line 279 in cmd/world/root/root_test.go

View workflow job for this annotation

GitHub Actions / Go

undefined: generateTokenName

// Ensure no error was returned
tassert.NoError(t, err)

// Ensure the name follows the expected pattern
tassert.Contains(t, name, "cli_")

// Additional checks if user and hostname can be retrieved in the environment
currentUser, userErr := user.Current()
hostname, hostErr := os.Hostname()
if userErr == nil && hostErr == nil {
expectedPrefix := fmt.Sprintf("cli_%s@%s_", currentUser.Username, hostname)
tassert.Contains(t, name, expectedPrefix)
}
}

func TestGenerateTokenNameWithFallback_Success(t *testing.T) {
// Attempt to generate a token name
name := generateTokenNameWithFallback()

Check failure on line 298 in cmd/world/root/root_test.go

View workflow job for this annotation

GitHub Actions / Go

undefined: generateTokenNameWithFallback

// Ensure the name follows the expected pattern
tassert.Contains(t, name, "cli_")

// Additional checks if user and hostname can be retrieved in the environment
currentUser, userErr := user.Current()
hostname, hostErr := os.Hostname()
if userErr == nil && hostErr == nil {
expectedPrefix := fmt.Sprintf("cli_%s@%s_", currentUser.Username, hostname)
tassert.Contains(t, name, expectedPrefix)
}
}

func TestGenerateTokenNameWithFallback_Failure(t *testing.T) {
// Temporarily change the current user and hostname to induce errors
// Note: This is a conceptual approach and might not be applicable or safe in real testing scenarios

// Attempt to generate a token name with fallback
name := generateTokenNameWithFallback()

Check failure on line 317 in cmd/world/root/root_test.go

View workflow job for this annotation

GitHub Actions / Go

undefined: generateTokenNameWithFallback

// Ensure the name follows the fallback pattern if errors occur
tassert.Contains(t, name, "cli_")
}

func TestPollForAccessToken(t *testing.T) {
tests := []struct {
name string
statusCode int
retryAfterHeader string
responseBody string
expectError bool
expectedResponse login.AccessTokenResponse
}{
{
name: "Successful token retrieval",
statusCode: http.StatusOK,
responseBody: `{"access_token": "test_token", "pub_key": "test_pub_key", "nonce": "test_nonce"}`,
expectedResponse: login.AccessTokenResponse{
AccessToken: "test_token",
PublicKey: "test_pub_key",
Nonce: "test_nonce",
},
expectError: false,
},
{
name: "Retry on 404 with Retry-After header",
statusCode: http.StatusNotFound,
retryAfterHeader: "1",
expectError: true,
},
{
name: "Retry on 404 without Retry-After header",
statusCode: http.StatusNotFound,
retryAfterHeader: "",
expectError: true,
},
{
name: "Error on invalid JSON response",
statusCode: http.StatusOK,
responseBody: `invalid_json`,
expectError: true,
},
{
name: "Error on non-200/404 status",
statusCode: http.StatusInternalServerError,
expectError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if test.retryAfterHeader != "" {
w.Header().Set("Retry-After", test.retryAfterHeader)
}
w.WriteHeader(test.statusCode)
w.Write([]byte(test.responseBody))
})

server := httptest.NewServer(handler)
defer server.Close()

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

response, err := pollForAccessToken(ctx, server.URL)

Check failure on line 384 in cmd/world/root/root_test.go

View workflow job for this annotation

GitHub Actions / Go

undefined: pollForAccessToken (typecheck)

if test.expectError {
tassert.Error(t, err)
} else {
tassert.NoError(t, err)
tassert.Equal(t, test.expectedResponse, response)
}
})
}
}
57 changes: 56 additions & 1 deletion common/globalconfig/globalconfig.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package globalconfig

import (
"encoding/json"
"os"
"path/filepath"

"github.com/rotisserie/eris"

Check failure on line 8 in common/globalconfig/globalconfig.go

View workflow job for this annotation

GitHub Actions / Go

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(pkg.world.dev/world-cli) -s blank -s dot --custom-order (gci)
"pkg.world.dev/world-cli/common/logger"
)

const (
configDir = ".worldcli"
configDir = ".worldcli"
credentialFileName = "credential.json"

Check failure on line 14 in common/globalconfig/globalconfig.go

View workflow job for this annotation

GitHub Actions / Go

G101: Potential hardcoded credentials (gosec)
)

type Credential struct {
Token string `json:"token"`
Name string `json:"name"`
}

func GetConfigDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
Expand All @@ -18,6 +28,51 @@ func GetConfigDir() (string, error) {
return filepath.Join(homeDir, configDir), nil
}

func GetWorldForgeToken() (string, error) {
fullConfigDir, err := GetConfigDir()
if err != nil {
return "", err
}

tokenFile := filepath.Join(fullConfigDir, credentialFileName)

file, err := os.ReadFile(tokenFile)
if err != nil {
return "", err
}

// Unmarshal the token
var cred Credential
err = json.Unmarshal(file, &cred)
if err != nil {
logger.Error(eris.Wrap(err, "failed to unmarshal token"))
return "", err
}

return cred.Token, nil
}

func SetWorldForgeToken(name string, token string) error {
fullConfigDir, err := GetConfigDir()
if err != nil {
return eris.Wrap(err, "failed to get config dir")
}

tokenFile := filepath.Join(fullConfigDir, credentialFileName)

cred := Credential{
Token: token,
Name: name,
}

credJSON, err := json.Marshal(cred)
if err != nil {
return eris.Wrap(err, "failed to marshal token")
}

return os.WriteFile(tokenFile, credJSON, 0600)
}

func SetupConfigDir() error {
fullConfigDir, err := GetConfigDir()
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions common/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ func Error(args ...interface{}) {
log.Error().Timestamp().Msg(fmt.Sprint(args...))
}

// Error function
func ErrorE(err error) {
log.Error().Timestamp().Err(err)
}

// Errorln function
func Errorln(args ...interface{}) {
log.Error().Timestamp().Msg(fmt.Sprintln(args...))
Expand Down
102 changes: 102 additions & 0 deletions common/login/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package login

import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/rand"
"encoding/hex"
"fmt"

"github.com/rotisserie/eris"
)

const (
decryptionErrorMsg = "cannot decrypt access token"
)

type Encryption struct {
curve ecdh.Curve
privateKey *ecdh.PrivateKey
publicKey *ecdh.PublicKey
}

type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
PublicKey string `json:"pub_key"`
Nonce string `json:"nonce"`
}

// NewEncryption creates a new Encryption struct
func NewEncryption() (Encryption, error) {
enc := Encryption{}
err := enc.generateKeys()
if err != nil {
return enc, err
}
return enc, nil
}

// generateKeys generates a private and public key pair
func (enc *Encryption) generateKeys() error {
enc.curve = ecdh.P256()
privateKey, err := enc.curve.GenerateKey(rand.Reader)
if err != nil {
return eris.Wrap(err, "cannot generate keys")
}
enc.privateKey = privateKey
enc.publicKey = privateKey.PublicKey()
return nil
}

// encodedPublicKey returns the public key as a hex string
func (enc Encryption) EncodedPublicKey() string {
return hex.EncodeToString(enc.publicKey.Bytes())
}

// decryptAccessToken decrypts the access token using the private key and nonce
func (enc Encryption) DecryptAccessToken(accessToken string, publicKey string, nonce string) (string, error) {
decodedAccessToken, err := hex.DecodeString(accessToken)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

decodedNonce, err := hex.DecodeString(nonce)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

decodedPublicKey, err := hex.DecodeString(publicKey)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

fmt.Println("Decoded Pub Key : ", string(decodedPublicKey))

remotePublicKey, err := enc.curve.NewPublicKey(decodedPublicKey)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

secret, err := enc.privateKey.ECDH(remotePublicKey)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

block, err := aes.NewCipher(secret)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

decryptedAccessToken, err := aesgcm.Open(nil, decodedNonce, decodedAccessToken, nil)
if err != nil {
return "", eris.Wrap(err, decryptionErrorMsg)
}

return string(decryptedAccessToken), nil
}
13 changes: 13 additions & 0 deletions common/login/login_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build darwin

package login

import (
"context"
"os/exec"
)

func RunOpenCmd(ctx context.Context, input string) error {
cmd := exec.CommandContext(ctx, "open", input)
return cmd.Run()
}
17 changes: 17 additions & 0 deletions common/login/login_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !windows && !darwin

package login

import (
"bytes"
"context"
"os"
"os/exec"
)

func RunOpenCmd(ctx context.Context, input string) error {
if f, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil && bytes.Contains(f, []byte("WSL")) {
return exec.CommandContext(ctx, "wslview", input).Run()
}
return exec.CommandContext(ctx, "xdg-open", input).Run()
}
Loading

0 comments on commit cfc675a

Please sign in to comment.