251756899f7517472177c6f5083a616e47ed7322 — Louis Solofrizzo a month ago 1e080e3
api, sdk, csc: Add SSH commands, helpers and routes for SSH key management

Signed-off-by: Louis Solofrizzo <lsolofrizzo@online.net>
M api/CMakeLists.txt => api/CMakeLists.txt +1 -0
@@ 8,4 8,5 @@ token.go
      quotas.go
      admin.go
+     ssh.go
  )

M api/admin.go => api/admin.go +3 -2
@@ 188,8 188,9 @@   	for _, key := range keys {
  		result.Keys = append(result.Keys, cisco.SSHKey{
- 			Key:     key.Key,
- 			Created: key.CreatedAt,
+ 			Key:         key.Key,
+ 			Created:     key.CreatedAt,
+ 			Fingerprint: key.Fingerprint,
  		})
  	}
  

M api/config.go => api/config.go +1 -1
@@ 34,5 34,5 @@ panic("Cannot read configuration file: " + err.Error())
  	}
  
- 	Config.HTTPHost = "http://" + Config.Host
+ 	Config.HTTPHost = Config.Host
  }

M api/database.go => api/database.go +3 -2
@@ 23,8 23,9 @@   type DbSSHKey struct {
  	gorm.Model
- 	Key  string
- 	User uint
+ 	Key         string
+ 	Fingerprint string
+ 	User        uint
  }
  
  type DbQuotas struct {

M api/main.go => api/main.go +1 -0
@@ 145,6 145,7 @@ InitInviteRoutes(app)
  	InitTokenRoutes(app)
  	InitAdminRoutes(app)
+ 	InitSSHRoutes(app)
  
  	app.Run(iris.Addr(Config.Address))
  }

A api/ssh.go => api/ssh.go +106 -0
@@ 0,0 1,106 @@
+ package main
+ 
+ import (
+ 	"bytes"
+ 	"cisco/sdk"
+ 	"crypto/md5"
+ 	"encoding/base64"
+ 	"encoding/hex"
+ 	"github.com/kataras/iris"
+ 	"strings"
+ )
+ 
+ func insertNth(s string, n int) string {
+ 	var buffer bytes.Buffer
+ 	var n_1 = n - 1
+ 	var l_1 = len(s) - 1
+ 
+ 	for i, rune := range s {
+ 		buffer.WriteRune(rune)
+ 		if i%n == n_1 && i != l_1 {
+ 			buffer.WriteRune(':')
+ 		}
+ 	}
+ 
+ 	return buffer.String()
+ }
+ 
+ func ssh_add(ctx iris.Context) {
+ 	var key cisco.SSHKeyAdd
+ 	var entry DbSSHKey
+ 
+ 	ctx.ReadJSON(&key)
+ 	if key.Key == "" {
+ 		APIErrorField(ctx, "key")
+ 		return
+ 	}
+ 
+ 	parts := strings.Fields(string(key.Key))
+ 	if len(parts) < 2 {
+ 		APIError(ctx, 400, "The provided SSH key is not valid")
+ 		return
+ 	}
+ 
+ 	k, err := base64.StdEncoding.DecodeString(parts[1])
+ 	if err != nil {
+ 		APIError(ctx, 400, "The provided SSH key is not valid: Cannot decode base64")
+ 		return
+ 	}
+ 
+ 	fp := md5.Sum([]byte(k))
+ 	fingerprint := strings.ToLower(hex.EncodeToString(fp[:]))
+ 	fingerprint = insertNth(fingerprint, 2)
+ 
+ 	DB.First(&entry, "fingerprint = ?", fingerprint)
+ 	if entry.ID != 0 {
+ 		APIError(ctx, 409, "We already have an entry for this fingerprint")
+ 		return
+ 	}
+ 
+ 	entry.Fingerprint = fingerprint
+ 	entry.Key = key.Key
+ 	entry.User, _ = ctx.Values().GetUint("User")
+ 
+ 	DB.Create(&entry)
+ 	ctx.StatusCode(iris.StatusCreated)
+ }
+ 
+ func ssh_list(ctx iris.Context) {
+ 	var entries []DbSSHKey
+ 	var result []cisco.SSHKey
+ 
+ 	user := ctx.Values().Get("User")
+ 	DB.Find(&entries, "user = ?", user)
+ 
+ 	for _, key := range entries {
+ 		result = append(result, cisco.SSHKey{
+ 			Key:         key.Key,
+ 			Fingerprint: key.Fingerprint,
+ 			Created:     key.CreatedAt,
+ 		})
+ 	}
+ 
+ 	ctx.JSON(&result)
+ }
+ 
+ func ssh_delete(ctx iris.Context) {
+ 	var key DbSSHKey
+ 
+ 	fingerprint := ctx.Params().Get("fingerprint")
+ 	user := ctx.Values().Get("User")
+ 
+ 	DB.First(&key, "fingerprint = ? AND user = ?", fingerprint, user)
+ 
+ 	if key.ID == 0 {
+ 		APIError(ctx, 404, "Specified key has not been found")
+ 		return
+ 	}
+ 
+ 	DB.Delete(&key)
+ }
+ 
+ func InitSSHRoutes(app *iris.Application) {
+ 	app.Post("/ssh", APIAuth, ssh_add)
+ 	app.Get("/ssh", APIAuth, ssh_list)
+ 	app.Delete("/ssh/{fingerprint:string}", APIAuth, ssh_delete)
+ }

M csc/cmd/admin/user/get.go => csc/cmd/admin/user/get.go +3 -2
@@ 82,8 82,9 @@   		fmt.Printf("SSH Keys (%d):\n", len(userinfo.Keys))
  		for _, key := range userinfo.Keys {
- 			fmt.Printf("    Key added at %s:\n", key.Created)
- 			fmt.Printf("        Key: %s\n", key.Key)
+ 			fmt.Printf("    Key %s:\n", key.Fingerprint)
+ 			fmt.Printf("        Created : %s\n", key.Created)
+ 			fmt.Printf("        Key     : %s\n", key.Key)
  		}
  
  		fmt.Printf("IPv4s (%d):\n", len(userinfo.Ipv4s))

A csc/cmd/ssh.go => csc/cmd/ssh.go +14 -0
@@ 0,0 1,14 @@
+ package cmd
+ 
+ import (
+ 	"github.com/spf13/cobra"
+ )
+ 
+ var SSHRootCmd = &cobra.Command{
+ 	Use:   "ssh",
+ 	Short: "Utilities for ssh",
+ }
+ 
+ func init() {
+ 	RootCmd.AddCommand(SSHRootCmd)
+ }

A csc/cmd/ssh/add.go => csc/cmd/ssh/add.go +40 -0
@@ 0,0 1,40 @@
+ package ssh
+ 
+ import (
+ 	"cisco/csc/cmd"
+ 	"cisco/sdk"
+ 	"fmt"
+ 	"github.com/spf13/cobra"
+ 	"io/ioutil"
+ 	"os"
+ )
+ 
+ var sshAdd = &cobra.Command{
+ 	Use:   "add",
+ 	Short: "Add a new SSH key",
+ 	Args:  cobra.ExactArgs(1),
+ 	Run: func(cmd *cobra.Command, args []string) {
+ 		file, err := os.Open(args[0])
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 			return
+ 		}
+ 		defer file.Close()
+ 
+ 		key, err := ioutil.ReadAll(file)
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 			return
+ 		}
+ 
+ 		err = cisco.AddSSHKey(string(key[:]))
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 			return
+ 		}
+ 	},
+ }
+ 
+ func init() {
+ 	cmd.SSHRootCmd.AddCommand(sshAdd)
+ }

A csc/cmd/ssh/delete.go => csc/cmd/ssh/delete.go +25 -0
@@ 0,0 1,25 @@
+ package ssh
+ 
+ import (
+ 	"cisco/csc/cmd"
+ 	"cisco/sdk"
+ 	"fmt"
+ 	"github.com/spf13/cobra"
+ )
+ 
+ var sshDelete = &cobra.Command{
+ 	Use:   "delete",
+ 	Short: "Delete an ssh key",
+ 	Args:  cobra.ExactArgs(1),
+ 	Run: func(cmd *cobra.Command, args []string) {
+ 		err := cisco.DeleteSSHKey(args[0])
+ 
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 		}
+ 	},
+ }
+ 
+ func init() {
+ 	cmd.SSHRootCmd.AddCommand(sshDelete)
+ }

A csc/cmd/ssh/list.go => csc/cmd/ssh/list.go +44 -0
@@ 0,0 1,44 @@
+ package ssh
+ 
+ import (
+ 	"cisco/csc/cmd"
+ 	"cisco/sdk"
+ 	"fmt"
+ 	"github.com/olekukonko/tablewriter"
+ 	"github.com/spf13/cobra"
+ 	"os"
+ 	"time"
+ )
+ 
+ var sshList = &cobra.Command{
+ 	Use:   "list",
+ 	Short: "List SSH keys",
+ 	Args:  cobra.ExactArgs(0),
+ 	Run: func(cmd *cobra.Command, args []string) {
+ 		withKeys, _ := cmd.Flags().GetBool("key-only")
+ 		keys, err := cisco.ListSSHKey()
+ 
+ 		if err != nil {
+ 			fmt.Printf("Error: %s\n", err.Error())
+ 			return
+ 		}
+ 
+ 		if withKeys {
+ 			for _, key := range keys {
+ 				fmt.Printf("%s", key.Key)
+ 			}
+ 		} else {
+ 			table := tablewriter.NewWriter(os.Stdout)
+ 			table.SetHeader([]string{"Fingerprint", "Created At"})
+ 			for _, key := range keys {
+ 				table.Append([]string{key.Fingerprint, key.Created.Format(time.UnixDate)})
+ 			}
+ 			table.Render()
+ 		}
+ 	},
+ }
+ 
+ func init() {
+ 	sshList.Flags().Bool("key-only", false, "Dump key content")
+ 	cmd.SSHRootCmd.AddCommand(sshList)
+ }

M csc/main.go => csc/main.go +1 -0
@@ 7,6 7,7 @@ _ "cisco/csc/cmd/admin/quotas/update"
  	_ "cisco/csc/cmd/admin/user"
  	_ "cisco/csc/cmd/invite"
+ 	_ "cisco/csc/cmd/ssh"
  	_ "cisco/csc/cmd/token"
  )
  

M sdk/request.go => sdk/request.go +18 -0
@@ 116,6 116,24 @@ Protected: true,
  }
  
+ var SSHKeyAddRoute Route = Route{
+ 	Path:      "/ssh",
+ 	Method:    "POST",
+ 	Protected: true,
+ }
+ 
+ var SSHKeyListRoute Route = Route{
+ 	Path:      "/ssh",
+ 	Method:    "GET",
+ 	Protected: true,
+ }
+ 
+ var SSHKeyDeleteRoute Route = Route{
+ 	Path:      "/ssh",
+ 	Method:    "DELETE",
+ 	Protected: true,
+ }
+ 
  type Request struct {
  	Arg  string
  	Data interface{}

M sdk/ssh.go => sdk/ssh.go +38 -2
@@ 1,10 1,46 @@ package cisco
  
  import (
+ 	"encoding/json"
  	"time"
  )
  
  type SSHKey struct {
- 	Key     string    `json:"key"`
- 	Created time.Time `json:"created"`
+ 	Key         string    `json:"key"`
+ 	Fingerprint string    `json:"fingerprint"`
+ 	Created     time.Time `json:"created"`
+ }
+ 
+ type SSHKeyAdd struct {
+ 	Key string `json:"key"`
+ }
+ 
+ func AddSSHKey(key string) error {
+ 	_, err := SendRequest(SSHKeyAddRoute, Request{
+ 		Data: &SSHKeyAdd{
+ 			Key: key,
+ 		},
+ 	})
+ 	return err
+ }
+ 
+ func ListSSHKey() ([]SSHKey, error) {
+ 	var result []SSHKey
+ 
+ 	data, err := SendRequest(SSHKeyListRoute, Request{})
+ 
+ 	if err != nil {
+ 		return nil, err
+ 	}
+ 
+ 	err = json.Unmarshal(data.([]byte), &result)
+ 	return result, err
+ }
+ 
+ func DeleteSSHKey(fingerprint string) error {
+ 	_, err := SendRequest(SSHKeyDeleteRoute, Request{
+ 		Arg: fingerprint,
+ 	})
+ 
+ 	return err
  }