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

Add Namespace Management CLI Commands #14

Merged
merged 10 commits into from
Aug 1, 2023
116 changes: 116 additions & 0 deletions cmd/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

import (
"os"

"github.com/spf13/cobra"
log "github.com/sirupsen/logrus"
)

// These functions are just placeholders. You need to provide actual implementation.

var withIdentity bool
var prefix string
var host string
var jwks bool
var pubkeyPath string
var privkeyPath string

func registerANamespace(cmd *cobra.Command, args []string) {
endpoint := host + "/registry"
if prefix == "" {
log.Error("Error: prefix is required")
os.Exit(1)
}

if withIdentity {
err := namespace_register_with_identity(pubkeyPath, privkeyPath, endpoint, prefix)
if err != nil {
log.Error(err)
os.Exit(1)
}
} else {
err := namespace_register(pubkeyPath, privkeyPath, endpoint, "", prefix)
if err != nil {
log.Error(err)
os.Exit(1)
}
}
}

func deleteANamespace(cmd *cobra.Command, args []string) {
endpoint := host + "/" + prefix
err := delete_namespace(endpoint)
if err != nil {
log.Error(err)
os.Exit(1)
}
}

func listAllNamespaces(cmd *cobra.Command, args []string) {
endpoint := host
err := list_namespaces(endpoint)
if err != nil {
log.Error(err)
os.Exit(1)
}
}

func getNamespace(cmd *cobra.Command, args []string) {
if jwks {
endpoint := host + "/" + prefix + "/issuer.jwks"
err := get_namespace(endpoint)
if err != nil {
log.Error(err)
os.Exit(1)
}
} else {
log.Error("Error: get command requires --jwks flag")
os.Exit(1)
}
}

var namespaceCmd = &cobra.Command{
Use: "namespace",
Short: "Work with namespaces",
}

var registerCmd = &cobra.Command{
Use: "register",
Short: "Register a new namespace",
Run: registerANamespace,
}

var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a namespace",
Run: deleteANamespace,
}

var listCmd = &cobra.Command{
Use: "list",
Short: "List all namespaces",
Run: listAllNamespaces,
}

var getCmd = &cobra.Command{
Use: "get",
Short: "Get a specific namespace",
Run: getNamespace,
}

func init() {
registerCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for registering namespace")
registerCmd.Flags().BoolVar(&withIdentity, "with-identity", false, "Register a namespace with an identity")
getCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for get namespace")
getCmd.Flags().BoolVar(&jwks, "jwks", false, "Get the jwks of the namespace")
deleteCmd.Flags().StringVar(&prefix, "prefix", "", "prefix for delete namespace")

namespaceCmd.PersistentFlags().StringVar(&host, "host", "http://localhost:8443/cli-namespaces", "Host of the namespace registry")
namespaceCmd.PersistentFlags().StringVar(&pubkeyPath, "pubkey", "/usr/src/app/pelican/cmd/cert/.well-known/client.jwks", "Path to the public key")
namespaceCmd.PersistentFlags().StringVar(&privkeyPath, "privkey", "/usr/src/app/pelican/cmd/cert/client.key", "Path to the private key")
namespaceCmd.AddCommand(registerCmd)
namespaceCmd.AddCommand(deleteCmd)
namespaceCmd.AddCommand(listCmd)
namespaceCmd.AddCommand(getCmd)
}
206 changes: 206 additions & 0 deletions cmd/namespace_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package main

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"net/http"
"bytes"
"bufio"
"math/big"
"encoding/base64"

"github.com/pelicanplatform/pelican/config"
)


func signPayload(payload []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) {
hash := sha256.Sum256(payload)
signature, err := privateKey.Sign(rand.Reader, hash[:], crypto.SHA256) // Use crypto.SHA256 instead of the hash[:]
if err != nil {
return nil, err
}
return signature, nil
}

func generateNonce() (string, error) {
nonce := make([]byte, 32)
_, err := rand.Read(nonce)
if err != nil {
return "", err
}
return hex.EncodeToString(nonce), nil
}

func make_request(url string, method string, data map[string]interface{}) ([]byte, error) {
payload, _ := json.Marshal(data)

req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}

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

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil,err
}
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)
return body, nil
}

func resp_to_json(body []byte) (map[string]string) {
// Unmarshal the response body
var respData map[string]string
err := json.Unmarshal(body, &respData)
if err != nil {
panic(err)
}
return respData
}

func namespace_register_with_identity(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, prefix string) (error) {
data := map[string]interface{}{
"identity_required": "true",
}
resp, err := make_request(namespaceRegistryEndpoint, "POST", data)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
respData := resp_to_json(resp)

verification_url := respData["verification_url"]
device_code := respData["device_code"]
fmt.Printf("Verification URL: %s\n", verification_url)

done := false
for !done {
data = map[string]interface{}{
"identity_required": "true",
"device_code": device_code,
}
resp, err = make_request(namespaceRegistryEndpoint, "POST", data)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
respData = resp_to_json(resp)

if respData["status"] == "APPROVED" {
done = true
} else {
fmt.Printf("Waiting for approval...\n")
reader := bufio.NewReader(os.Stdin)
fmt.Print("Press Enter after verification")
_, _ = reader.ReadString('\n')
}
}
access_token := respData["access_token"]
fmt.Printf("Access token: %s\n", access_token)
return namespace_register(publicKeyPath, privateKeyPath, namespaceRegistryEndpoint, access_token, prefix)
}

func namespace_register(publicKeyPath string, privateKeyPath string, namespaceRegistryEndpoint string, access_token string, prefix string) (error) {
publicKey, err := config.LoadPublicKey(publicKeyPath, privateKeyPath)
if err != nil {
return fmt.Errorf("Failed to load public key: %v\n", err)
}

jwks, err := config.JWKSMap(publicKey)
if err != nil {
return fmt.Errorf("Failed to convert public key to JWKS: %v\n", err)
}

privateKey, err := config.LoadPrivateKey(privateKeyPath)
if err != nil {
return fmt.Errorf("Failed to load private key: %v\n", err)
}

client_nonce, err := generateNonce()
if err != nil {
return fmt.Errorf("Failed to generate client nonce: %v\n", err)
}

data := map[string]interface{}{
"client_nonce": client_nonce,
"pubkey": fmt.Sprintf("%x", jwks["x"]),
}

resp, err := make_request(namespaceRegistryEndpoint, "POST", data)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
respData := resp_to_json(resp)

// Create client payload by concatenating client_nonce and server_nonce
clientPayload := client_nonce + respData["server_nonce"]

// Sign the payload
signature, err := signPayload([]byte(clientPayload), privateKey)
if err != nil {
return fmt.Errorf("Failed to sign payload: %v\n", err)
}

// Create data for the second POST request
xBytes, _ := base64.RawURLEncoding.DecodeString(jwks["x"])
yBytes, _ := base64.RawURLEncoding.DecodeString(jwks["y"])

data2 := map[string]interface{}{
"client_nonce": client_nonce,
"server_nonce": respData["server_nonce"],
"pubkey": map[string]string{
bbockelm marked this conversation as resolved.
Show resolved Hide resolved
"x" : new(big.Int).SetBytes(xBytes).String(),
"y" : new(big.Int).SetBytes(yBytes).String(),
"curve": jwks["crv"],
},
"client_payload": clientPayload,
"client_signature": hex.EncodeToString(signature),
"server_payload": respData["server_payload"],
"server_signature": respData["server_signature"],
"prefix": prefix,
"access_token": access_token,
}

// Send the second POST request
_, err = make_request(namespaceRegistryEndpoint, "POST", data2)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
return nil
}

func list_namespaces(endpoint string) (error) {
respData, err := make_request(endpoint, "GET", nil)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
fmt.Println(string(respData))
return nil
}

func get_namespace(endpoint string) (error) {
respData, err := make_request(endpoint, "GET", nil)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
fmt.Println(string(respData))
return nil
}

func delete_namespace(endpoint string) (error) {
respData, err := make_request(endpoint, "DELETE", nil)
if err != nil {
return fmt.Errorf("Failed to make request: %v\n", err)
}
fmt.Println(string(respData))
return nil
}
3 changes: 1 addition & 2 deletions cmd/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ func init() {
originCmd.AddCommand(originConfigCmd)
originCmd.AddCommand(originServeCmd)
originServeCmd.Flags().StringP("volume", "v", "", "Setting the volue to /SRC:/DEST will export the contents of /SRC as /DEST in the Pelican federation")
err := viper.BindPFlag("ExportVolume", originServeCmd.Flags().Lookup("volume"))
if err != nil {
if err := viper.BindPFlag("ExportVolume", originServeCmd.Flags().Lookup("volume")); err != nil {
panic(err)
}
}
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func init() {
rootCmd.AddCommand(directorCmd)
objectCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.AddCommand(originCmd)
rootCmd.AddCommand(namespaceCmd)
rootCmd.AddCommand(rootConfigCmd)
rootCmd.AddCommand(rootPluginCmd)
preferredPrefix := config.GetPreferredPrefix()
Expand Down Expand Up @@ -88,4 +89,4 @@ func initConfig() {
if viper.GetBool("Debug") {
setLogging(log.DebugLevel)
}
}
}
bbockelm marked this conversation as resolved.
Show resolved Hide resolved
Loading