9f3db0e9f3c5103290e280f4d9f36cba6be601b7 — Louis Solofrizzo 11 months ago 43d1cc9
api: Add updated API

Signed-off-by: Louis Solofrizzo <lsolofrizzo@online.net>
15 files changed, 847 insertions(+), 1020 deletions(-)

M api/CMakeLists.txt
A api/admin.go
M api/config.go
A api/database.go
D api/http.go
D api/instance.go
A api/invite.go
A api/mail.go
M api/main.go
A api/quotas.go
D api/slaves.go
D api/ssh.go
A api/token.go
D api/user.go
A api/utils.go
M api/CMakeLists.txt => api/CMakeLists.txt +8 -6
@@ 1,9 1,11 @@-add_go_component(lf-cloud-api
+ add_go_component(cisco-api
      main.go
-     user.go
-     instance.go
+     database.go
      config.go
-     ssh.go
-     slaves.go
-     http.go
+     invite.go
+     utils.go
+     mail.go
+     token.go
+     quotas.go
+     admin.go
  )

A api/admin.go => api/admin.go +224 -0
@@ 0,0 1,224 @@
+ package main
+ 
+ import (
+ 	"cisco/sdk"
+ 	"github.com/kataras/iris"
+ )
+ 
+ func admin_user_list(ctx iris.Context) {
+ 	var result []cisco.User
+ 	var users []DbUser
+ 
+ 	DB.Find(&users)
+ 	for _, user := range users {
+ 		result = append(result, cisco.User{
+ 			Username: user.Username,
+ 			Email:    user.Email,
+ 			Role:     user.Role,
+ 		})
+ 	}
+ 
+ 	ctx.JSON(&result)
+ }
+ 
+ func admin_quota_list(ctx iris.Context) {
+ 	var result []cisco.Quota
+ 	var quotas []DbQuotas
+ 
+ 	DB.Find(&quotas)
+ 	for _, quota := range quotas {
+ 		result = append(result, cisco.Quota{
+ 			Role:      quota.Role,
+ 			Instances: quota.MaxInstances,
+ 			Ipv4:      quota.MaxIpv4,
+ 			Storage:   quota.MaxStorage,
+ 			Invites:   quota.MaxInvites,
+ 		})
+ 	}
+ 
+ 	ctx.JSON(&result)
+ }
+ 
+ func admin_quota_create(ctx iris.Context) {
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	DB.First(&quota, "role = ?", name)
+ 
+ 	if quota.ID != 0 {
+ 		APIError(ctx, 409, "A quota with this name already exists, please update it or delete it.")
+ 		return
+ 	}
+ 
+ 	quota.Role = name
+ 	DB.Create(&quota)
+ 	ctx.StatusCode(iris.StatusCreated)
+ }
+ 
+ func admin_quota_delete(ctx iris.Context) {
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	DB.First(&quota, "role = ?", name)
+ 
+ 	if quota.ID == 0 {
+ 		APIError(ctx, 404, "Cannot find the desired quota")
+ 		return
+ 	}
+ 
+ 	quota.Role = name
+ 	DB.Delete(&quota)
+ }
+ 
+ func admin_quota_instance(ctx iris.Context) {
+ 	var instance cisco.QuotaInstanceUpdate
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	ctx.ReadJSON(&instance)
+ 
+ 	DB.First(&quota, "role = ?", name)
+ 	if quota.ID == 0 {
+ 		APIError(ctx, 404, "Cannot find the desired quota")
+ 		return
+ 	}
+ 
+ 	DB.Model(&quota).Update("MaxInstances", instance.Instance)
+ }
+ 
+ func admin_quota_ipv4(ctx iris.Context) {
+ 	var ipv4 cisco.QuotaIpv4Update
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	ctx.ReadJSON(&ipv4)
+ 
+ 	DB.First(&quota, "role = ?", name)
+ 	if quota.ID == 0 {
+ 		APIError(ctx, 404, "Cannot find the desired quota")
+ 		return
+ 	}
+ 
+ 	DB.Model(&quota).Update("MaxIpv4", ipv4.Ipv4)
+ }
+ 
+ func admin_quota_storage(ctx iris.Context) {
+ 	var storage cisco.QuotaStorageUpdate
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	ctx.ReadJSON(&storage)
+ 
+ 	DB.First(&quota, "role = ?", name)
+ 	if quota.ID == 0 {
+ 		APIError(ctx, 404, "Cannot find the desired quota")
+ 		return
+ 	}
+ 
+ 	DB.Model(&quota).Update("MaxStorage", storage.Storage)
+ }
+ 
+ func admin_quota_invites(ctx iris.Context) {
+ 	var invites cisco.QuotaInvitesUpdate
+ 	var quota DbQuotas
+ 
+ 	name := ctx.Params().Get("name")
+ 	ctx.ReadJSON(&invites)
+ 
+ 	DB.First(&quota, "role = ?", name)
+ 	if quota.ID == 0 {
+ 		APIError(ctx, 404, "Cannot find the desired quota")
+ 		return
+ 	}
+ 
+ 	DB.Model(&quota).Update("MaxInvites", invites.Invites)
+ }
+ 
+ func admin_user_info(ctx iris.Context) {
+ 	var result cisco.UserInfo
+ 	var user DbUser
+ 	var tokens []DbToken
+ 	var keys []DbSSHKey
+ 	var instances []DbInstance
+ 	var ipv4s []DbIpv4
+ 	var invites []DbInvite
+ 
+ 	username := ctx.Params().Get("username")
+ 	DB.First(&user, "username = ?", username)
+ 	if user.ID == 0 {
+ 		APIError(ctx, 404, "Could not find the user")
+ 		return
+ 	}
+ 
+ 	DB.Find(&tokens, "user = ?", user.ID)
+ 	DB.Find(&keys, "user = ?", user.ID)
+ 	DB.Find(&instances, "user = ?", user.ID)
+ 	DB.Find(&ipv4s, "user = ?", user.ID)
+ 	DB.Find(&invites, "user = ?", user.ID)
+ 
+ 	for _, token := range tokens {
+ 		result.Tokens = append(result.Tokens, cisco.Token{
+ 			Key:         token.Key,
+ 			Secret:      token.Secret,
+ 			Description: token.Description,
+ 			Created:     token.CreatedAt,
+ 		})
+ 	}
+ 
+ 	for _, key := range keys {
+ 		result.Keys = append(result.Keys, cisco.SSHKey{
+ 			Key:     key.Key,
+ 			Created: key.CreatedAt,
+ 		})
+ 	}
+ 
+ 	for _, instance := range instances {
+ 		result.Instances = append(result.Instances, cisco.Instance{
+ 			Uuid:         instance.Uuid,
+ 			Name:         instance.Name,
+ 			Ipv4:         instance.Ipv4,
+ 			Size:         instance.Size,
+ 			OS:           instance.OS,
+ 			Architecture: instance.Architecture,
+ 			Type:         instance.Type,
+ 			Created:      instance.CreatedAt,
+ 		})
+ 	}
+ 
+ 	for _, ipv4 := range ipv4s {
+ 		result.Ipv4s = append(result.Ipv4s, cisco.Ipv4{
+ 			IP:       ipv4.IP,
+ 			Instance: ipv4.Instance,
+ 			Created:  ipv4.CreatedAt,
+ 		})
+ 	}
+ 
+ 	for _, invite := range invites {
+ 		result.Invites = append(result.Invites, cisco.Invite{
+ 			Email:   invite.Email,
+ 			Claimed: invite.Claimed,
+ 			Created: invite.CreatedAt,
+ 		})
+ 	}
+ 
+ 	result.User = cisco.User{
+ 		Username: user.Username,
+ 		Email:    user.Email,
+ 		Role:     user.Role,
+ 		Created:  user.CreatedAt,
+ 	}
+ 
+ 	ctx.JSON(result)
+ }
+ 
+ func InitAdminRoutes(app *iris.Application) {
+ 	app.Get("/admin/user/list", AdminAuth, admin_user_list)
+ 	app.Get("/admin/quota/list", AdminAuth, admin_quota_list)
+ 	app.Put("/admin/quota/{name:string}", AdminAuth, admin_quota_create)
+ 	app.Delete("/admin/quota/{name:string}", AdminAuth, admin_quota_delete)
+ 	app.Post("/admin/quota/instance/{name:string}", AdminAuth, admin_quota_instance)
+ 	app.Post("/admin/quota/ipv4/{name:string}", AdminAuth, admin_quota_ipv4)
+ 	app.Post("/admin/quota/storage/{name:string}", AdminAuth, admin_quota_storage)
+ 	app.Post("/admin/quota/invites/{name:string}", AdminAuth, admin_quota_invites)
+ 	app.Get("/admin/user/{username:string}", AdminAuth, admin_user_info)
+ }

M api/config.go => api/config.go +19 -34
@@ 1,53 1,38 @@ package main
  
  import (
- 	"flag"
- 	"github.com/go-xorm/xorm"
- 	"io/ioutil"
- 
- 	log "github.com/sirupsen/logrus"
- 
- 	_ "github.com/mattn/go-sqlite3"
  	"gopkg.in/yaml.v2"
+ 	"io/ioutil"
  )
  
  type CiscoConfig struct {
- 	Cert     string   `yaml:"cert" binding:"required"`
- 	Key      string   `yaml:"key" binding:"required"`
- 	Host     string   `yaml:"host" binding:"required"`
- 	Port     string   `yaml:"port" binding:"required"`
- 	CA       string   `yaml:"ca" binding:"required"`
- 	SSHProxy string   `yaml:"sshproxy" binding:"required"`
- 	SSHKey   string   `yaml:"sshkey" binding:"required"`
- 	DB       string   `yaml:"db" binding:"required"`
- 	Slaves   []string `yaml:"slaves" binding:"required"`
+ 	Driver       string `yaml:"driver"`
+ 	Database     string `yaml:"database"`
+ 	Address      string `yaml:"address"`
+ 	SMTPHost     string `yaml:"smtp_host"`
+ 	SMTPUser     string `yaml:"smtp_user"`
+ 	SMTPPassword string `yaml:"smtp_password"`
+ 	Host         string `yaml:"host"`
+ 	Salt         string `yaml:"salt"`
+ 	HTTPHost     string
+ }
+ 
+ type CiscoContext struct {
  }
  
  var Config CiscoConfig
- var Database *xorm.Engine
+ var Context CiscoContext
  
- func setConfig() error {
- 	filename := flag.String("config", "/etc/cisco.conf", "The yaml configuration file")
- 	flag.Parse()
+ func ReadConfig(filename string) {
  
- 	source, err := ioutil.ReadFile(*filename)
+ 	source, err := ioutil.ReadFile(filename)
  	if err != nil {
- 		return err
+ 		panic("Cannot open configuration file: " + err.Error())
  	}
  	err = yaml.Unmarshal(source, &Config)
  	if err != nil {
- 		return err
- 	}
- 
- 	Database, err = xorm.NewEngine("sqlite3", Config.DB)
- 	if err != nil {
- 		return err
- 	}
- 
- 	err = Database.Sync2(new(Instance))
- 	if err != nil {
- 		log.Fatal(err)
+ 		panic("Cannot read configuration file: " + err.Error())
  	}
  
- 	return nil
+ 	Config.HTTPHost = "http://" + Config.Host
  }

A api/database.go => api/database.go +89 -0
@@ 0,0 1,89 @@
+ package main
+ 
+ import (
+ 	"github.com/jinzhu/gorm"
+ 	_ "github.com/jinzhu/gorm/dialects/sqlite"
+ )
+ 
+ type DbUser struct {
+ 	gorm.Model
+ 	Email    string
+ 	Password string
+ 	Username string
+ 	Role     string
+ }
+ 
+ type DbToken struct {
+ 	gorm.Model
+ 	Key         string
+ 	Secret      string
+ 	Description string
+ 	User        uint
+ }
+ 
+ type DbSSHKey struct {
+ 	gorm.Model
+ 	Key  string
+ 	User uint
+ }
+ 
+ type DbQuotas struct {
+ 	gorm.Model
+ 	Role         string
+ 	MaxInstances int
+ 	MaxIpv4      int
+ 	MaxStorage   int
+ 	MaxInvites   int
+ }
+ 
+ type DbInstance struct {
+ 	gorm.Model
+ 	Uuid         string
+ 	Name         string
+ 	Ipv4         uint
+ 	Size         uint
+ 	OS           string
+ 	Architecture string
+ 	User         uint
+ 	Type         string
+ }
+ 
+ type DbIpv4 struct {
+ 	gorm.Model
+ 	IP       string
+ 	Ipv6In   string
+ 	Ipv6Out  string
+ 	User     uint
+ 	Instance string
+ }
+ 
+ type DbInvite struct {
+ 	gorm.Model
+ 	Email   string
+ 	Sum     string
+ 	Claimed bool
+ 	User    uint
+ }
+ 
+ var DB *gorm.DB
+ 
+ func InitDB() {
+ 	var err error
+ 
+ 	DB, err = gorm.Open(Config.Driver, Config.Database)
+ 	if err != nil {
+ 		panic("Failed to connect to database:" + err.Error())
+ 	}
+ 
+ 	DB.AutoMigrate(&DbUser{})
+ 	DB.AutoMigrate(&DbToken{})
+ 	DB.AutoMigrate(&DbSSHKey{})
+ 	DB.AutoMigrate(&DbQuotas{})
+ 	DB.AutoMigrate(&DbInstance{})
+ 	DB.AutoMigrate(&DbIpv4{})
+ 	DB.AutoMigrate(&DbInvite{})
+ }
+ 
+ func CloseDB() {
+ 	DB.Close()
+ }

D api/http.go => api/http.go +0 -79
@@ 1,79 0,0 @@-package main
- 
- import (
- 	log "github.com/sirupsen/logrus"
- 	"os"
- 	"os/exec"
- 	"text/template"
- )
- 
- type InstanceHTTP struct {
- 	Domain  string
- 	Address string
- 	Port    int
- }
- 
- func addInstanceToNginx(instance *Instance, domain string) error {
- 	tmpl, err := template.New("nginx-conf").Parse(`
- 	server {
- 		server_name {{.Domain}};
- 		set $upstream {{.Address}}:{{.Port}};
- 		location / {
- 			proxy_pass_header	Authorization;
- 			proxy_pass		http://$upstream;
- 			proxy_set_header	Host $host;
- 			proxy_set_header	X-Real-IP $remote_addr;
- 			proxy_set_header	X-Forwarded-For $proxy_add_x_forwarded_for;
- 			proxy_http_version	1.1;
- 			proxy_set_header	Connection "";
- 			proxy_buffering		off;
- 			client_max_body_size	0;
- 			proxy_read_timeout	36000s;
- 			proxy_redirect		off;
- 		}
- 	}
- 	`)
- 
- 	if err != nil {
- 		log.Errorf("Error in templating: %s", err)
- 		return err
- 	}
- 
- 	bind, err := instanceBindPort(instance, "80")
- 	if err != nil {
- 		log.Errorf("Cannot bind port on instance")
- 		return err
- 	}
- 
- 	template := InstanceHTTP{
- 		Domain:  domain,
- 		Address: bind.IP,
- 		Port:    bind.Port,
- 	}
- 
- 	file, err := os.Create("/etc/nginx/conf.d/" + domain + ".conf")
- 	if err != nil {
- 		log.Errorf("Cannot open nginx file")
- 		return err
- 	}
- 
- 	err = tmpl.Execute(file, template)
- 	if err != nil {
- 		log.Errorf("Cannot write to configuration file")
- 		return err
- 	}
- 
- 	run := exec.Command("/bin/bash", "-c", "certbot -d "+domain+" -n --nginx --redirect")
- 	run.Run()
- 	return nil
- }
- 
- func removeInstanceFromNginx(instance *Instance) error {
- 	os.Remove("/etc/nginx/conf.d/" + instance.Name + ".cloud.louifox.house.conf")
- 	for _, domain := range instance.Domains {
- 		os.Remove("/etc/nginx/conf.d/" + domain + ".cloud.louifox.house.conf")
- 	}
- 	run := exec.Command("/bin/bash", "-c", "systemctl reload nginx")
- 	run.Run()
- 	return nil
- }

D api/instance.go => api/instance.go +0 -274
@@ 1,274 0,0 @@-package main
- 
- import (
- 	"github.com/kataras/iris"
- 	log "github.com/sirupsen/logrus"
- 	"strconv"
- 	"time"
- )
- 
- type InstanceStatus int
- 
- const (
- 	InstanceCreated InstanceStatus = iota
- 	InstanceAllocated
- 	InstanceStarted
- 	InstanceInstalling
- 	InstanceStopped
- 	InstanceDeleting
- )
- 
- type Instance struct {
- 	ID           int64          `json:"id"`
- 	Name         string         `json:"name" xorm:"varchar(200)"`
- 	Architecture string         `json:"arch" xorm:"varchar(200)"`
- 	Release      string         `json:"release" xorm:"varchar(200)"`
- 	OS           string         `json:"os" xorm:"varchar(200)"`
- 	Owner        string         `json:"owner" xorm:"varchar(200)"`
- 	Status       InstanceStatus `json:"status" xorm:"int"`
- 	TextStatus   string         `json:"text_status"`
- 	IPv4         string         `json:"ipv4"`
- 	IPv6         string         `json:"ipv6"`
- 	Gateway      string         `json:"gateway"`
- 	Capacity     uint64         `json:"capacity"`
- 	PhysicalNode string         `json:"physical_node" xorm:"varchar(120)"`
- 	CreatedAt    time.Time      `json:"created" xorm:"created"`
- 	UpdatedAt    time.Time      `json:"updated" xorm:"updated"`
- 	Domains      []string
- }
- 
- func instance_list(ctx iris.Context) {
- 	var cert = get_certificate(ctx)
- 	var instances []Instance
- 
- 	err := Database.Where("owner = ?", cert.Subject.CommonName).Find(&instances)
- 	if err == nil {
- 		ctx.JSON(&instances)
- 	}
- }
- 
- func instance_add(ctx iris.Context) {
- 	var cert = get_certificate(ctx)
- 	var instance Instance
- 
- 	ctx.ReadJSON(&instance)
- 	instance.Owner = cert.Subject.CommonName
- 	instance.Status = InstanceCreated
- 
- 	Database.Insert(&instance)
- 	ctx.ContentType("application/json")
- 	ctx.Writef("{\"id\": %d}", instance.ID)
- 	go instance_create(&instance)
- }
- 
- func instance_exists(ctx iris.Context) {
- 	name := ctx.Params().Get("name")
- 
- 	instance := Instance{Name: name}
- 	ctx.ContentType("application/json")
- 	if ok, _ := Database.Get(&instance); ok {
- 		ctx.Writef("{\"status\":\"found\"}")
- 	} else {
- 		ctx.Writef("{\"status\":\"notfound\"}")
- 	}
- }
- 
- func instance_get(ctx iris.Context) {
- 	id, _ := ctx.Params().GetInt64("id")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	switch instance.Status {
- 	case InstanceCreated:
- 		instance.TextStatus = "created"
- 	case InstanceAllocated:
- 		instance.TextStatus = "allocated"
- 	case InstanceInstalling:
- 		instance.TextStatus = "installing"
- 	case InstanceStarted:
- 		instance.TextStatus = "running"
- 	case InstanceStopped:
- 		instance.TextStatus = "stopped"
- 	case InstanceDeleting:
- 		instance.TextStatus = "deleting"
- 	}
- 
- 	if ok {
- 		if instance.Status == InstanceStarted || instance.Status == InstanceStopped {
- 			err := instanceGet(&instance)
- 			if err != nil {
- 				ctx.StatusCode(iris.StatusInternalServerError)
- 				return
- 			}
- 		}
- 		ctx.JSON(&instance)
- 	} else {
- 		ctx.StatusCode(iris.StatusNotFound)
- 	}
- }
- 
- func instance_delete(ctx iris.Context) {
- 	id, _ := ctx.Params().GetInt64("id")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	if !ok {
- 		ctx.StatusCode(iris.StatusNotFound)
- 		return
- 	}
- 
- 	err := instanceDestroy(&instance)
- 	if err == nil {
- 		ssh_rm_host(instance.Name)
- 		removeInstanceFromNginx(&instance)
- 		Database.Delete(&instance)
- 	}
- }
- 
- func instance_network(ctx iris.Context) {
- 	id, _ := ctx.Params().GetInt64("id")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	if !ok {
- 		ctx.StatusCode(iris.StatusNotFound)
- 		return
- 	}
- 
- 	network, err := instanceGetNetwork(&instance)
- 	if err != nil {
- 		ctx.StatusCode(iris.StatusInternalServerError)
- 		return
- 	}
- 
- 	ctx.ContentType("application/json")
- 	ctx.Writef("%s", string(network))
- }
- 
- type InstanceBindPost struct {
- 	Source  string `json:"port"`
- 	IsLocal bool   `json:"local"`
- }
- 
- func instance_bind(ctx iris.Context) {
- 	var bind InstanceBindPost
- 	var err error
- 
- 	id, _ := ctx.Params().GetInt64("id")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	if !ok {
- 		ctx.StatusCode(iris.StatusNotFound)
- 		return
- 	}
- 
- 	if err = ctx.ReadJSON(&bind); err != nil {
- 		ctx.StatusCode(iris.StatusBadRequest)
- 		return
- 	}
- 
- 	if bind.IsLocal {
- 		_, err = instanceBindLocalPort(&instance, bind.Source)
- 	} else {
- 		_, err = instanceBindPort(&instance, bind.Source)
- 	}
- 
- 	if err != nil {
- 		ctx.StatusCode(iris.StatusInternalServerError)
- 	} else {
- 		ctx.StatusCode(iris.StatusAccepted)
- 	}
- }
- 
- func instance_resize(ctx iris.Context) {
- 	id, _ := ctx.Params().GetInt64("id")
- 	size := ctx.Params().Get("new_size")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	if !ok {
- 		ctx.StatusCode(iris.StatusNotFound)
- 		return
- 	}
- 
- 	err := instanceResize(&instance, size)
- 	if err != nil {
- 		ctx.StatusCode(iris.StatusInternalServerError)
- 	} else {
- 		ctx.StatusCode(iris.StatusAccepted)
- 	}
- }
- 
- func instance_domain(ctx iris.Context) {
- 	id, _ := ctx.Params().GetInt64("id")
- 	domain := ctx.Params().Get("domain")
- 
- 	instance := Instance{ID: id}
- 	ok, _ := Database.Get(&instance)
- 
- 	if !ok {
- 		ctx.StatusCode(iris.StatusNotFound)
- 		return
- 	}
- 
- 	err := addInstanceToNginx(&instance, domain)
- 	if err != nil {
- 		ctx.StatusCode(iris.StatusInternalServerError)
- 		return
- 	}
- 
- 	instance.Domains = append(instance.Domains, domain)
- 	Database.ID(instance.ID).Cols("domains").Update(&Instance{Domains: instance.Domains})
- 	ctx.StatusCode(iris.StatusAccepted)
- }
- 
- func instance_create(instance *Instance) {
- 	log.Infof("Choosing slave for instance %s", instance.Name)
- 	err := chooseSlave(instance)
- 	if err != nil {
- 		return
- 	}
- 
- 	err = createInstance(instance)
- 	if err != nil {
- 		return
- 	}
- 
- 	err = startInstance(instance)
- 	if err != nil {
- 		return
- 	}
- 
- 	/* Wait for DNS & Internet */
- 	time.Sleep(5 * time.Second)
- 
- 	err = instanceInstallAnsible(instance)
- 	if err != nil {
- 		log.Errorf("Could not install Ansible and init playbook")
- 		return
- 	}
- 
- 	bind, err := instanceBindPort(instance, "22")
- 	if err != nil {
- 		log.Errorf("Could not bind port")
- 		return
- 	}
- 
- 	err = ssh_add_host(instance.Name, instance.Owner, bind.IP+":"+strconv.Itoa(bind.Port))
- 	if err != nil {
- 		return
- 	}
- 
- 	err = addInstanceToNginx(instance, instance.Name+".cloud.louifox.house")
- 	if err != nil {
- 		return
- 	}
- 
- 	log.Infof("%v", bind)
- }

A api/invite.go => api/invite.go +137 -0
@@ 0,0 1,137 @@
+ package main
+ 
+ import (
+ 	"cisco/sdk"
+ 	"crypto/sha256"
+ 	"encoding/hex"
+ 	"fmt"
+ 	"github.com/kataras/iris"
+ 	"time"
+ )
+ 
+ var mailTemplateFmt string = `Hello!
+ 
+ You have been invited to use the %s cloud platform.
+ Here's your invitation code: %s.
+ 
+ You can either activate it via the webinterface[1], or via the command line tool:
+ 
+     csc invite claim %s --endpoint https://api.%s
+ 
+ Take Care,
+ 
+ [1] https://%s/invite/claim/%s
+ `
+ 
+ func invite_new(ctx iris.Context) {
+ 	var invite cisco.InviteNew
+ 	var entry DbInvite
+ 
+ 	ctx.ReadJSON(&invite)
+ 	if invite.Email == "" {
+ 		APIErrorField(ctx, "email")
+ 		return
+ 	}
+ 	if ValidateEmail(invite.Email) == false {
+ 		APIError(ctx, 400, "Bad Email format")
+ 		return
+ 	}
+ 
+ 	DB.First(&entry, "email = ?", invite.Email)
+ 	if entry.ID != 0 {
+ 		APIError(ctx, 409, "Specified email already have received an invite")
+ 		return
+ 	}
+ 
+ 	if CheckInviteQuotas(ctx) == false {
+ 		return
+ 	}
+ 
+ 	sum := sha256.Sum256([]byte(invite.Email + time.Now().String()))
+ 	sumHex := hex.EncodeToString(sum[:])
+ 
+ 	err := SendMail(invite.Email, "Your invitation to "+Config.Host,
+ 		fmt.Sprintf(mailTemplateFmt, Config.Host, sumHex, sumHex,
+ 			Config.Host, Config.Host, sumHex))
+ 
+ 	if err != nil {
+ 		APIError(ctx, 500, "Cannot send email: "+err.Error())
+ 		return
+ 	}
+ 
+ 	entry.Email = invite.Email
+ 	entry.Sum = sumHex
+ 	entry.Claimed = false
+ 	entry.User, _ = ctx.Values().GetUint("User")
+ 
+ 	DB.Create(&entry)
+ 	ctx.StatusCode(iris.StatusCreated)
+ }
+ 
+ func invite_get(ctx iris.Context) {
+ 	var ret cisco.InviteGet
+ 	var entry DbInvite
+ 
+ 	hash := ctx.Params().Get("hash")
+ 
+ 	DB.First(&entry, "sum = ?", hash)
+ 	if entry.ID == 0 {
+ 		APIError(ctx, 404, "Specified invite has not been found")
+ 		return
+ 	}
+ 
+ 	ret.Email = entry.Email
+ 	ret.Claimed = entry.Claimed
+ 	ctx.JSON(&ret)
+ }
+ 
+ func invite_claim(ctx iris.Context) {
+ 	var invite cisco.InviteClaim
+ 	var entry DbInvite
+ 	var user DbUser
+ 
+ 	hash := ctx.Params().Get("hash")
+ 	DB.First(&entry, "sum = ?", hash)
+ 	if entry.ID == 0 {
+ 		APIError(ctx, 404, "Specified invite has not been found")
+ 		return
+ 	} else if entry.Claimed == true {
+ 		APIError(ctx, 410, "This invite has already been claimed")
+ 		return
+ 	}
+ 
+ 	ctx.ReadJSON(&invite)
+ 	if invite.Password == "" {
+ 		APIErrorField(ctx, "password")
+ 		return
+ 	} else if invite.Username == "" {
+ 		APIErrorField(ctx, "username")
+ 		return
+ 	}
+ 
+ 	if ValidateSHA256(invite.Password) == false {
+ 		APIError(ctx, 400, "The provided password is not a valid SHA256 hash. Please _do not_ send password in plain text.")
+ 		return
+ 	}
+ 
+ 	DB.First(&user, "username = ?", invite.Username)
+ 	if user.ID != 0 {
+ 		APIError(ctx, 409, "This username is already taken, you cannot use it.")
+ 		return
+ 	}
+ 
+ 	user.Password = HashNSalt(invite.Password)
+ 	user.Username = invite.Username
+ 	user.Email = entry.Email
+ 	user.Role = "newbie"
+ 
+ 	DB.Create(&user)
+ 	DB.Model(&entry).Update("claimed", true)
+ 	ctx.StatusCode(iris.StatusCreated)
+ }
+ 
+ func InitInviteRoutes(app *iris.Application) {
+ 	app.Post("/invite", APIAuth, invite_new)
+ 	app.Get("/invite/{hash:string}", invite_get)
+ 	app.Post("/invite/claim/{hash:string}", invite_claim)
+ }

A api/mail.go => api/mail.go +23 -0
@@ 0,0 1,23 @@
+ package main
+ 
+ import (
+ 	"strings"
+ 
+ 	"github.com/emersion/go-sasl"
+ 	"github.com/emersion/go-smtp"
+ )
+ 
+ func SendMail(to string, subject string, msg string) error {
+ 	auth := sasl.NewPlainClient("", Config.SMTPUser, Config.SMTPPassword)
+ 
+ 	body := strings.NewReader(
+ 		"To: " + to + "\r\n" +
+ 			"From: " + Config.SMTPUser + "\r\n" +
+ 			"Subject: " + subject + "\r\n" +
+ 			"\r\n" +
+ 			msg + "\r\n" +
+ 			"---\r\n" +
+ 			"I'm just a dumb script, please don't respond!\r\n")
+ 
+ 	return smtp.SendMail(Config.SMTPHost, auth, Config.SMTPUser, []string{to}, body)
+ }

M api/main.go => api/main.go +134 -46
@@ 1,62 1,150 @@ package main
  
  import (
- 	"crypto/x509"
- 	"encoding/pem"
- 	"net/url"
- 
- 	log "github.com/sirupsen/logrus"
- 
+ 	"crypto/hmac"
+ 	"crypto/md5"
+ 	"crypto/rand"
+ 	"crypto/sha256"
+ 	"encoding/hex"
+ 	"flag"
+ 	"fmt"
  	"github.com/kataras/iris"
+ 	"github.com/satori/go.uuid"
+ 	"os"
  )
  
- func get_certificate(ctx iris.Context) *x509.Certificate {
- 	var rel, _ = url.QueryUnescape(ctx.GetHeader("X-Ssl-cert"))
- 	var block, _ = pem.Decode([]byte(rel))
- 	var cert, _ = x509.ParseCertificate(block.Bytes)
+ func S3Auth(ctx iris.Context) bool {
+ 	var token DbToken
+ 	var user DbUser
+ 	var toSign string
+ 
+ 	key := ctx.GetHeader("X-Csc-Key")
+ 	DB.First(&token, "key = ?", key)
+ 	if token.ID == 0 {
+ 		APIError(ctx, 403, "The request signature we calculated does not match the signature you provided. Check your key!")
+ 		return false
+ 	}
+ 
+ 	toSign = ctx.Method() + Config.HTTPHost + ctx.RequestPath(false) + ctx.GetHeader("Date") + key
+ 
+ 	rawData := RequestBody(ctx)
+ 	if rawData != nil && len(rawData) != 0 {
+ 		sum := md5.Sum(rawData)
+ 		if ctx.GetHeader("Content-MD5") != hex.EncodeToString(sum[:]) {
+ 			APIError(ctx, 400, "The provided content hash did not match the body content (Are you safe?)")
+ 			return false
+ 		}
+ 
+ 		toSign += string(rawData[:]) + ctx.GetHeader("Content-MD5")
+ 	}
+ 
+ 	secretHex, err := hex.DecodeString(token.Secret)
+ 	if err != nil {
+ 		APIError(ctx, 500, "We could not get your secret key")
+ 		return false
+ 	}
+ 
+ 	h := hmac.New(sha256.New, secretHex)
+ 	h.Write([]byte(toSign))
+ 	signature := hex.EncodeToString(h.Sum(nil))
+ 
+ 	if signature != ctx.GetHeader("X-Csc-Signature") {
+ 		APIError(ctx, 403, "The request signature we calculated does not match the signature you provided. Check your key!")
+ 		return false
+ 	}
+ 
+ 	DB.First(&user, "ID = ?", token.User)
  
- 	return cert
+ 	ctx.Values().Set("User", token.User)
+ 	ctx.Values().Set("Role", user.Role)
+ 	return true
  }
  
- func main() {
- 	app := iris.New()
- 	app.Use(func(ctx iris.Context) {
- 		log.WithFields(log.Fields{
- 			"ip":     ctx.RemoteAddr(),
- 			"path":   ctx.Path(),
- 			"method": ctx.Method(),
- 		}).Info("api request")
+ func APIAuth(ctx iris.Context) {
+ 	if S3Auth(ctx) {
  		ctx.Next()
- 	})
+ 	}
+ }
  
- 	err := setConfig()
- 	if err != nil {
- 		panic(err)
+ func AdminAuth(ctx iris.Context) {
+ 	if S3Auth(ctx) {
+ 		role := ctx.Values().Get("Role")
+ 		if role == "admin" {
+ 			ctx.Next()
+ 		} else {
+ 			APIError(ctx, 403, "This route is only for users with an admin privilege")
+ 		}
  	}
+ }
  
- 	createSlaves(Config.Slaves)
- 
- 	app.Delete("/api/user/key/{id:int32}", user_delete_key)
- 	app.Get("/api/user/key/{id:int32}", user_get_key)
- 	app.Get("/api/user/init", user_init)
- 	app.Get("/api/user/inspect", user_inspect)
- 	app.Post("/api/user/key", user_add_key)
- 	app.Get("/api/instance/list", instance_list)
- 	app.Get("/api/instance/exists/{name:string}", instance_exists)
- 	app.Post("/api/instance/add", instance_add)
- 	app.Get("/api/instance/{id:int32}", instance_get)
- 	app.Get("/api/instance/{id:int32}/network", instance_network)
- 	app.Post("/api/instance/{id:int32}/bind", instance_bind)
- 	app.Delete("/api/instance/{id:int32}", instance_delete)
- 	app.Put("/api/instance/{id:int32}/resize/{new_size:string}", instance_resize)
- 	app.Put("/api/instance/{id:int32}/domain/{domain:string}", instance_domain)
- 
- 	err = ssh_init()
- 	if err != nil {
- 		panic(err)
+ func parseFlags() {
+ 	admin := flag.String("admin", "", "Admin username")
+ 	password := flag.String("password", "", "Admin password (SHA256)")
+ 	email := flag.String("email", "", "Admin email")
+ 	filename := flag.String("config", "/etc/cisco-api.conf", "The yaml configuration file")
+ 
+ 	flag.Parse()
+ 
+ 	ReadConfig(*filename)
+ 	InitDB()
+ 
+ 	if *admin != "" && *password != "" && *email != "" {
+ 		var user DbUser
+ 		var entry DbToken
+ 
+ 		if ValidateSHA256(*password) == false {
+ 			panic("The provided admin password is not a valid SHA256 hash")
+ 		}
+ 
+ 		DB.First(&user, "username = ?", *admin)
+ 		if user.ID != 0 {
+ 			panic("This username already exists!")
+ 		}
+ 
+ 		fmt.Printf("Creating admin user %s\n", *admin)
+ 		user.Password = HashNSalt(*password)
+ 		user.Username = *admin
+ 		user.Email = *email
+ 		user.Role = "admin"
+ 		DB.Create(&user)
+ 
+ 		secret := make([]byte, 32)
+ 
+ 		_, err := rand.Read(secret)
+ 		if err != nil {
+ 			panic("Not enough entropy to generate the secret key")
+ 		}
+ 
+ 		entry.Key = uuid.NewV4().String()
+ 		entry.Description = "Initialization admin credentials"
+ 		entry.Secret = hex.EncodeToString(secret[:])
+ 		entry.User = user.ID
+ 
+ 		DB.Create(&entry)
+ 
+ 		fmt.Printf("Admin credentials (It will only print once, don't loose this!):\n")
+ 		fmt.Printf("key: %s\n", entry.Key)
+ 		fmt.Printf("secret: %s\n", entry.Secret)
+ 
+ 		os.Exit(0)
+ 	} else if *admin != "" || *password != "" || *email != "" {
+ 		panic("In order to create an admin user, you must provide _all_ three flags: -admin, -password, -email")
  	}
+ }
+ 
+ func main() {
+ 	parseFlags()
+ 	defer CloseDB()
+ 
+ 	app := iris.New()
+ 
+ 	app.OnErrorCode(404, func(ctx iris.Context) {
+ 		APIError(ctx, 404, "The route you asked could not be found. Is your client up to date?")
+ 	})
+ 
+ 	InitInviteRoutes(app)
+ 	InitTokenRoutes(app)
+ 	InitAdminRoutes(app)
  
- 	app.Run(iris.Addr(":8080"), iris.WithConfiguration(iris.Configuration{
- 		FireMethodNotAllowed: true,
- 	}))
+ 	app.Run(iris.Addr(Config.Address))
  }

A api/quotas.go => api/quotas.go +33 -0
@@ 0,0 1,33 @@
+ package main
+ 
+ import (
+ 	"fmt"
+ 	"github.com/kataras/iris"
+ )
+ 
+ func CheckInviteQuotas(ctx iris.Context) bool {
+ 	var quotas DbQuotas
+ 	var count int
+ 
+ 	role := ctx.Values().GetString("Role")
+ 	id := ctx.Values().GetString("User")
+ 
+ 	DB.First(&quotas, "role = ?", role)
+ 	if quotas.ID == 0 {
+ 		APIError(ctx, 500, "Could not find quotas for your role")
+ 		return false
+ 	}
+ 
+ 	if quotas.MaxInvites == -1 {
+ 		return true
+ 	}
+ 
+ 	DB.Model(&DbInvite{}).Where("user = ?", id).Count(&count)
+ 	if count >= quotas.MaxInvites {
+ 		quota := fmt.Sprintf("%d/%d", count, quotas.MaxInvites)
+ 		APIError(ctx, 403, "Your quotas for invites have been reached ("+quota+"), you cannot create another one. If you want an upgrade, please contact one of the admins")
+ 		return false
+ 	}
+ 
+ 	return true
+ }

D api/slaves.go => api/slaves.go +0 -331
@@ 1,331 0,0 @@-package main
- 
- import (
- 	"bytes"
- 	"encoding/json"
- 	"errors"
- 	log "github.com/sirupsen/logrus"
- 	"hash/fnv"
- 	"io/ioutil"
- 	"net/http"
- )
- 
- type Slave struct {
- 	Address string
- 	Name    string
- 	Arch    string
- }
- 
- var Slaves []*Slave
- 
- func httpRequest(verb string, slave *Slave, route string, body []byte) ([]byte, error) {
- 	url := "http://" + slave.Address + "/" + route
- 	req, err := http.NewRequest(verb, url, bytes.NewBuffer(body))
- 	if err != nil {
- 		return nil, err
- 	}
- 	res, err := http.DefaultClient.Do(req)
- 	if err != nil {
- 		return nil, err
- 	}
- 	defer res.Body.Close()
- 	resp, err := ioutil.ReadAll(res.Body)
- 	if err != nil {
- 		return nil, err
- 	}
- 	return resp, nil
- }
- 
- type SlaveInfo struct {
- 	Name string `json:"name"`
- 	Arch string `json:"arch"`
- }
- 
- func slaveGetInfo(slave *Slave) {
- 	var info SlaveInfo
- 
- 	resp, err := httpRequest("GET", slave, "api/info", nil)
- 	if err != nil {
- 		log.Errorf("Cannot get info from %s", slave.Address)
- 		return
- 	}
- 
- 	json.Unmarshal(resp, &info)
- 	slave.Name = info.Name
- 	slave.Arch = info.Arch
- 	if slave.Arch == "arm" {
- 		slave.Arch = "armhf"
- 	}
- }
- 
- func getSlaveByName(name string) *Slave {
- 	for _, slave := range Slaves {
- 		if slave.Name == name {
- 			return slave
- 		}
- 	}
- 
- 	return nil
- }
- 
- func chooseSlave(instance *Instance) error {
- 	var archSlaves []*Slave
- 
- 	for _, slave := range Slaves {
- 		if slave.Arch != instance.Architecture {
- 			continue
- 		}
- 
- 		archSlaves = append(archSlaves, slave)
- 	}
- 
- 	if len(archSlaves) == 0 {
- 		log.Errorf("Could not find any slave for %s", instance.Name)
- 		return errors.New("Could not find any slave")
- 	}
- 
- 	log.Infof("Found %d slaves for instance %s", len(archSlaves), instance.Name)
- 
- 	h := fnv.New32a()
- 	h.Write([]byte(instance.Name))
- 	slaveId := h.Sum32() % uint32(len(archSlaves))
- 	log.Infof("Choosed slave %s for instance %s", archSlaves[slaveId], instance.Name)
- 
- 	instance.PhysicalNode = archSlaves[slaveId].Name
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceAllocated})
- 	Database.ID(instance.ID).Cols("physical_node").Update(&Instance{PhysicalNode: archSlaves[slaveId].Name})
- 	return nil
- }
- 
- type InstanceAdd struct {
- 	Distro  string `json:"distro"`
- 	Release string `json:"release"`
- 	Name    string `json:"name"`
- 	Arch    string `json:"arch"`
- }
- 
- func createInstance(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	req := &InstanceAdd{
- 		Distro:  instance.OS,
- 		Release: instance.Release,
- 		Name:    instance.Name,
- 		Arch:    instance.Architecture,
- 	}
- 
- 	body, _ := json.Marshal(req)
- 	_, err := httpRequest("POST", slave, "api/instance", body)
- 
- 	if err != nil {
- 		log.Errorf("Could not create instance")
- 		return err
- 	}
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceStopped})
- 	return nil
- }
- 
- func startInstance(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 
- 	_, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/start", nil)
- 	if err != nil {
- 		log.Errorf("Could not start instance")
- 		return err
- 	}
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceStarted})
- 	return nil
- }
- 
- func stopInstance(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 
- 	_, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/stop", nil)
- 	if err != nil {
- 		log.Errorf("Could not start instance")
- 		return err
- 	}
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceStopped})
- 	return nil
- }
- 
- func rebootInstance(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 
- 	instance.Status = InstanceStopped
- 	Database.Update(&instance)
- 
- 	_, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/reboot", nil)
- 	if err != nil {
- 		log.Errorf("Could not start instance")
- 		return err
- 	}
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceStarted})
- 	return nil
- }
- 
- type InstanceGet struct {
- 	Name     string   `json:"name"`
- 	Status   int      `json:"status"`
- 	IPv4     []string `json:"IPv4"`
- 	IPv6     []string `json:"IPv6"`
- 	Gateway  string   `json:"gateway"`
- 	Capacity uint64   `json:"capacity"`
- }
- 
- func instanceGet(instance *Instance) error {
- 	var infos InstanceGet
- 
- 	slave := getSlaveByName(instance.PhysicalNode)
- 
- 	resp, err := httpRequest("GET", slave, "api/instance/"+instance.Name, nil)
- 	if err != nil {
- 		log.Errorf("Could not get instance")
- 		return err
- 	}
- 	json.Unmarshal(resp, &infos)
- 
- 	if len(infos.IPv4) == 0 {
- 		instance.IPv4 = "Unknown"
- 	} else {
- 		instance.IPv4 = infos.IPv4[0]
- 	}
- 
- 	if len(infos.IPv6) == 0 {
- 		instance.IPv6 = "Unknown"
- 	} else {
- 		instance.IPv6 = infos.IPv6[0]
- 	}
- 	instance.Gateway = infos.Gateway
- 	instance.Capacity = infos.Capacity
- 	return nil
- }
- 
- func instanceDestroy(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 
- 	_, err := httpRequest("DELETE", slave, "api/instance/"+instance.Name, nil)
- 	if err != nil {
- 		log.Errorf("Could not delete instance")
- 		return err
- 	}
- 	return nil
- }
- 
- type InstanceExec struct {
- 	Cmd []string `json:"cmd"`
- }
- 
- func instanceInstallAnsible(instance *Instance) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	var exec InstanceExec
- 
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceInstalling})
- 	if instance.OS == "ubuntu" || instance.OS == "ubuntu-core" {
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"apt update -yy",
- 				"apt install ansible git -yy",
- 			},
- 		}
- 	} else if instance.OS == "debian" {
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"echo 'deb http://ftp.de.debian.org/debian stretch-backports main' >> /etc/apt/sources.list",
- 				"apt update -yy",
- 				"apt install -t stretch-backports ansible git -yy",
- 			},
- 		}
- 	} else if instance.OS == "archlinux" {
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"pacman -Syu ansible git --noconfirm",
- 			},
- 		}
- 	} else if instance.OS == "fedora" || instance.OS == "centos" {
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"dnf install -y ansible git",
- 				"ln -sf /usr/bin/python3 /usr/bin/python",
- 			},
- 		}
- 	} else if instance.OS == "alpinelinux" {
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"apk add -y ansible git",
- 			},
- 		}
- 	} else if instance.OS == "gentoo" {
- 
- 		exec = InstanceExec{
- 			Cmd: []string{
- 				"USE='-ssl sqlite -cli' emerge app-admin/ansible dev-vcs/git app-portage/gentoolkit",
- 			},
- 		}
- 	}
- 
- 	body, _ := json.Marshal(exec)
- 	_, err := httpRequest("POST", slave, "api/instance/"+instance.Name+"/exec", body)
- 	if err != nil {
- 		return err
- 	}
- 
- 	exec = InstanceExec{
- 		Cmd: []string{
- 			"ansible-pull -U git://git.ne02ptzero.me/cisco-ansible init.yml",
- 		},
- 	}
- 	body, _ = json.Marshal(exec)
- 	_, err = httpRequest("POST", slave, "api/instance/"+instance.Name+"/exec", body)
- 	Database.ID(instance.ID).Cols("status").Update(&Instance{Status: InstanceStarted})
- 	return err
- }
- 
- type InstanceBind struct {
- 	IP   string `json:"ip"`
- 	Port int    `json:"port"`
- }
- 
- func instanceBindPort(instance *Instance, port string) (InstanceBind, error) {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	var instanceBind InstanceBind
- 
- 	body, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/bind/"+port, nil)
- 	err = json.Unmarshal(body, &instanceBind)
- 	return instanceBind, err
- }
- 
- func instanceBindLocalPort(instance *Instance, port string) (InstanceBind, error) {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	var instanceBind InstanceBind
- 
- 	body, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/bind_local/"+port, nil)
- 	err = json.Unmarshal(body, &instanceBind)
- 	return instanceBind, err
- }
- 
- func instanceGetNetwork(instance *Instance) ([]byte, error) {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	body, err := httpRequest("GET", slave, "api/instance/"+instance.Name+"/network", nil)
- 	return body, err
- }
- 
- func instanceResize(instance *Instance, size string) error {
- 	slave := getSlaveByName(instance.PhysicalNode)
- 	_, err := httpRequest("PUT", slave, "api/instance/"+instance.Name+"/resize/"+size, nil)
- 	return err
- }
- 
- func createSlaves(slaves []string) {
- 	for _, slave := range slaves {
- 		s := &Slave{
- 			Address: slave,
- 		}
- 		Slaves = append(Slaves, s)
- 		slaveGetInfo(s)
- 	}
- }

D api/ssh.go => api/ssh.go +0 -169
@@ 1,169 0,0 @@-package main
- 
- import (
- 	"io/ioutil"
- 
- 	"encoding/json"
- 	log "github.com/sirupsen/logrus"
- 	"golang.org/x/crypto/ssh"
- 	"io"
- 	"strconv"
- )
- 
- type Key struct {
- 	ID      int32  `json:"ID"`
- 	Key     string `json:"Key"`
- 	UserID  int    `json:"UserID"`
- 	Comment string `json:"Comment"`
- }
- 
- type User struct {
- 	ID    int    `json:"ID"`
- 	Email string `json:"Email"`
- 	Name  string `json:"Name"`
- 	Keys  []Key  `json:"Keys"`
- }
- 
- var client *ssh.Client
- 
- func ssh_exec_command(cmd string) (string, error) {
- 	session, err := client.NewSession()
- 	if err != nil {
- 		log.Fatal(err)
- 		return "", err
- 	}
- 
- 	defer session.Close()
- 
- 	log.Infof("Executing command '%s' on sshportal", cmd)
- 	out, err := session.CombinedOutput(cmd)
- 	log.Infof("Output is '%s'", string(out))
- 	if err != nil {
- 		log.Fatal(err)
- 		return "", err
- 	}
- 
- 	return string(out), nil
- }
- 
- func ssh_user_list() (string, error) {
- 	return ssh_exec_command("user ls")
- }
- 
- func ssh_user_inspect(name string) (string, error) {
- 	return ssh_exec_command("user inspect " + name)
- }
- 
- func ssh_get_key_user(name string, id int32) (*Key, error) {
- 	var users []User
- 
- 	result, err := ssh_user_inspect(name)
- 	if err != nil {
- 		return nil, err
- 	}
- 
- 	err = json.Unmarshal([]byte(result), &users)
- 	if err != nil {
- 		return nil, err
- 	}
- 
- 	for i := 0; i < len(users[0].Keys); i++ {
- 		if id == users[0].Keys[i].ID {
- 			return &users[0].Keys[i], nil
- 		}
- 	}
- 
- 	return nil, nil
- }
- 
- func ssh_user_key_del(id int32) error {
- 	_, err := ssh_exec_command("userkey rm " + strconv.FormatInt(int64(id), 10))
- 	return err
- }
- 
- func ssh_add_user_key(user string, key string) (string, error) {
- 	session, err := client.NewSession()
- 	if err != nil {
- 		log.Fatal(err)
- 		return "", err
- 	}
- 
- 	defer session.Close()
- 
- 	log.Infof("Key: %s", key)
- 	stdin, err := session.StdinPipe()
- 	io.WriteString(stdin, key)
- 
- 	log.Infof("Executing command '%s' on sshportal", "userkey create "+user)
- 	out, err := session.CombinedOutput("userkey create " + user)
- 	log.Infof("Output is '%s'", string(out))
- 
- 	if err != nil {
- 		log.Fatal(err)
- 		return "", err
- 	}
- 	return string(out), nil
- }
- 
- func ssh_add_host(name string, user string, address string) error {
- 	_, err := ssh_exec_command("hostgroup create --name " + name)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("host create --name " + name + " --group " + name + " ssh://root@" + address)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("usergroup create --name " + name)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("user update " + user + " -g " + name)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("acl create --hg " + name + " --ug " + name)
- 	if err != nil {
- 		return err
- 	}
- 	return nil
- }
- 
- func ssh_rm_host(name string) error {
- 	_, err := ssh_exec_command("hostgroup rm " + name)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("host rm " + name)
- 	if err != nil {
- 		return err
- 	}
- 	_, err = ssh_exec_command("usergroup rm " + name)
- 	if err != nil {
- 		return err
- 	}
- 	return nil
- }
- 
- func ssh_init() error {
- 	key, err := ioutil.ReadFile(Config.SSHKey)
- 	if err != nil {
- 		log.Fatalf("unable to read private key: %v", err)
- 	}
- 
- 	signer, err := ssh.ParsePrivateKey(key)
- 	if err != nil {
- 		log.Fatalf("unable to parse private key: %v", err)
- 	}
- 
- 	config := &ssh.ClientConfig{
- 		User: "admin",
- 		Auth: []ssh.AuthMethod{
- 			ssh.PublicKeys(signer),
- 		},
- 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- 	}
- 
- 	client, err = ssh.Dial("tcp", Config.SSHProxy+":22", config)
- 	return err
- }

A api/token.go => api/token.go +102 -0
@@ 0,0 1,102 @@
+ package main
+ 
+ import (
+ 	"cisco/sdk"
+ 	"crypto/rand"
+ 	"encoding/hex"
+ 	"github.com/kataras/iris"
+ 	"github.com/satori/go.uuid"
+ )
+ 
+ func token_new(ctx iris.Context) {
+ 	var newToken cisco.TokenNew
+ 	var entry DbToken
+ 	var user DbUser
+ 	var ret cisco.Token
+ 
+ 	ctx.ReadJSON(&newToken)
+ 	if newToken.Username == "" {
+ 		APIErrorField(ctx, "username")
+ 		return
+ 	}
+ 
+ 	if newToken.Password == "" {
+ 		APIErrorField(ctx, "password")
+ 		return
+ 	}
+ 
+ 	if ValidateSHA256(newToken.Password) == false {
+ 		APIError(ctx, 400, "The provided password is not a valid SHA256 hash. Please _do not_ send password in plain text.")
+ 		return
+ 	}
+ 
+ 	DB.First(&user, "username = ?", newToken.Username)
+ 	if user.ID == 0 {
+ 		APIError(ctx, 403, "Invalid credentials")
+ 		return
+ 	}
+ 
+ 	if user.Password != HashNSalt(newToken.Password) {
+ 		APIError(ctx, 403, "Invalid credentials")
+ 		return
+ 	}
+ 
+ 	secret := make([]byte, 32)
+ 
+ 	_, err := rand.Read(secret)
+ 	if err != nil {
+ 		APIError(ctx, 500, "Not enough entropy to generate the secret key")
+ 	}
+ 
+ 	entry.Key = uuid.NewV4().String()
+ 	entry.Description = newToken.Description
+ 	entry.Secret = hex.EncodeToString(secret[:])
+ 	entry.User = user.ID
+ 
+ 	DB.Create(&entry)
+ 
+ 	ret.Key = entry.Key
+ 	ret.Description = entry.Description
+ 	ret.Secret = entry.Secret
+ 
+ 	ctx.JSON(&ret)
+ 	ctx.StatusCode(iris.StatusCreated)
+ }
+ 
+ func token_list(ctx iris.Context) {
+ 	var entries []DbToken
+ 	var result []cisco.Token
+ 
+ 	user := ctx.Values().Get("User")
+ 	DB.Find(&entries, "user = ?", user)
+ 
+ 	for _, token := range entries {
+ 		result = append(result, cisco.Token{
+ 			Key:         token.Key,
+ 			Description: token.Description,
+ 			Created:     token.CreatedAt,
+ 		})
+ 	}
+ 	ctx.JSON(&result)
+ }
+ 
+ func token_revoke(ctx iris.Context) {
+ 	var token DbToken
+ 
+ 	key := ctx.Params().Get("key")
+ 	user := ctx.Values().Get("User")
+ 
+ 	DB.First(&token, "key = ? AND user = ?", key, user)
+ 	if token.ID == 0 {
+ 		APIError(ctx, 404, "Specified token has not been found")
+ 		return
+ 	}
+ 
+ 	DB.Delete(&token)
+ }
+ 
+ func InitTokenRoutes(app *iris.Application) {
+ 	app.Post("/token/new", token_new)
+ 	app.Get("/token/list", APIAuth, token_list)
+ 	app.Delete("/token/revoke/{key:string}", APIAuth, token_revoke)
+ }

D api/user.go => api/user.go +0 -81
@@ 1,81 0,0 @@-package main
- 
- import (
- 	"github.com/kataras/iris"
- 	"strings"
- )
- 
- func user_get_key(ctx iris.Context) {
- 	var cert = get_certificate(ctx)
- 	id, err := ctx.Params().GetInt32("id")
- 
- 	result, err := ssh_get_key_user(cert.Subject.CommonName, id)
- 	if err != nil {
- 		return
- 	}
- 
- 	if result == nil {
- 		ctx.StatusCode(iris.StatusUnauthorized)
- 		return
- 	}
- 
- 	ctx.JSON(result)
- }
- 
- func user_delete_key(ctx iris.Context) {
- 	var cert = get_certificate(ctx)
- 	id, err := ctx.Params().GetInt32("id")
- 
- 	result, err := ssh_get_key_user(cert.Subject.CommonName, id)
- 	if err != nil {
- 		return
- 	}
- 
- 	if result == nil {
- 		ctx.StatusCode(iris.StatusUnauthorized)
- 		return
- 	}
- 
- 	err = ssh_user_key_del(id)
- 	ctx.ContentType("application/json")
- 	if err != nil {
- 		ctx.Writef("{status: \"OK\"}")
- 	} else {
- 		ctx.Writef("{status: \"KO\"}")
- 	}
- }
- 
- func user_init(ctx iris.Context) {
- }
- 
- type KeyPost struct {
- 	Key string `json:"key"`
- }
- 
- func user_add_key(ctx iris.Context) {
- 	var key Key
- 	var cert = get_certificate(ctx)
- 
- 	if err := ctx.ReadJSON(&key); err != nil {
- 		ctx.StatusCode(iris.StatusBadRequest)
- 		return
- 	}
- 
- 	out, err := ssh_add_user_key(cert.Subject.CommonName, key.Key)
- 	if err != nil {
- 		ctx.StatusCode(iris.StatusInternalServerError)
- 	}
- 
- 	ctx.ContentType("application/json")
- 	ctx.Writef("{\"status\": \"OK\", \"id\": %s}", strings.Split(out, "\n")[1])
- }
- 
- func user_inspect(ctx iris.Context) {
- 	var cert = get_certificate(ctx)
- 
- 	result, err := ssh_user_inspect(cert.Subject.CommonName)
- 	if err == nil {
- 		ctx.ContentType("application/json")
- 		ctx.Writef(result)
- 	}
- }

A api/utils.go => api/utils.go +78 -0
@@ 0,0 1,78 @@
+ package main
+ 
+ import (
+ 	"bytes"
+ 	"cisco/sdk"
+ 	"crypto/sha256"
+ 	"encoding/hex"
+ 	"github.com/kataras/iris"
+ 	"io/ioutil"
+ 	"regexp"
+ )
+ 
+ func RequestBody(ctx iris.Context) []byte {
+ 	var requestBody []byte
+ 	data, err := ioutil.ReadAll(ctx.Request().Body)
+ 	if err == nil {
+ 		requestBody = data
+ 		ctx.Request().Body = ioutil.NopCloser(bytes.NewBuffer(data))
+ 	}
+ 	return requestBody
+ }
+ 
+ func ValidateEmail(email string) bool {
+ 	re := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
+ 	return re.MatchString(email)
+ }
+ 
+ func ValidateSHA256(hash string) bool {
+ 	re := regexp.MustCompile("^[A-Fa-f0-9]{64}$")
+ 	return re.MatchString(hash)
+ }
+ 
+ func HashNSalt(password string) string {
+ 	before := password + Config.Salt
+ 	sum := sha256.Sum256([]byte(before))
+ 	return hex.EncodeToString(sum[:])
+ }
+ 
+ func GetErrorFromCode(Code int) string {
+ 	switch Code {
+ 	case 400:
+ 		return "Bad Request"
+ 	case 403:
+ 		return "Forbidden"
+ 	case 404:
+ 		return "Not Found"
+ 	case 409:
+ 		return "Conflict"
+ 	case 410:
+ 		return "Gone"
+ 	case 500:
+ 		return "Internal Error"
+ 	}
+ 
+ 	return "Unknown"
+ }
+ 
+ func APIError(ctx iris.Context, Code int, Error string) {
+ 	var doc string
+ 
+ 	if ctx.GetCurrentRoute() != nil {
+ 		doc = "https://docs.cloud.louifox.house/api" + ctx.GetCurrentRoute().StaticPath() + "/" + ctx.GetCurrentRoute().Method()
+ 	}
+ 
+ 	err := cisco.ApiError{
+ 		Details: Error,
+ 		Code:    Code,
+ 		Error:   GetErrorFromCode(Code),
+ 		Doc:     doc,
+ 	}
+ 
+ 	ctx.StatusCode(Code)
+ 	ctx.JSON(err)
+ }
+ 
+ func APIErrorField(ctx iris.Context, Field string) {
+ 	APIError(ctx, 400, "Missing or empty field '"+Field+"' in JSON payload")
+ }