From f11073b2064b3715d083109b2804a2813387cf65 Mon Sep 17 00:00:00 2001 From: emli Date: Mon, 19 Feb 2018 11:06:01 +0600 Subject: [PATCH] #194 Continuous integration for Gorjun Add travis file for CI and libgorjun for tests. --- .travis.yml | 26 ++- libgorjun/auth.go | 211 ++++++++++++++++++ libgorjun/auth_test.go | 117 ++++++++++ ...apache-subutai-template_4.0.0_amd64.tar.gz | Bin 0 -> 637991 bytes ...nginx-subutai-template_0.1.10_amd64.tar.gz | Bin 0 -> 949689 bytes ...nginx-subutai-template_0.1.11_amd64.tar.gz | Bin 0 -> 949687 bytes .../nginx-subutai-template_0.1.6_amd64.tar.gz | Bin 0 -> 949689 bytes .../nginx-subutai-template_0.1.7_amd64.tar.gz | Bin 0 -> 949691 bytes .../nginx-subutai-template_0.1.9_amd64.tar.gz | Bin 0 -> 949690 bytes libgorjun/gorjun.go | 193 ++++++++++++++++ libgorjun/gorjun.service | 11 + libgorjun/gpg.txt | 10 + libgorjun/register.sh | 7 + 13 files changed, 568 insertions(+), 7 deletions(-) create mode 100644 libgorjun/auth.go create mode 100644 libgorjun/auth_test.go create mode 100644 libgorjun/data/abdysamat-apache-subutai-template_4.0.0_amd64.tar.gz create mode 100644 libgorjun/data/nginx-subutai-template_0.1.10_amd64.tar.gz create mode 100644 libgorjun/data/nginx-subutai-template_0.1.11_amd64.tar.gz create mode 100644 libgorjun/data/nginx-subutai-template_0.1.6_amd64.tar.gz create mode 100644 libgorjun/data/nginx-subutai-template_0.1.7_amd64.tar.gz create mode 100644 libgorjun/data/nginx-subutai-template_0.1.9_amd64.tar.gz create mode 100644 libgorjun/gorjun.go create mode 100644 libgorjun/gorjun.service create mode 100644 libgorjun/gpg.txt create mode 100755 libgorjun/register.sh diff --git a/.travis.yml b/.travis.yml index 9b637c1..cb494ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,20 @@ -language: perl -perl: - - "5.22" -before_install: - - git clone git://github.com/travis-perl/helpers ~/travis-perl-helpers - - source ~/travis-perl-helpers/init --auto +language: go + +sudo: enabled +go: + - 1.10 script: - - cd gorjun-perl-cli; prove -v t/test_cover.t + - go get + - make + - sudo apt-get install systemd + - sudo cp -f /home/travis/gopath/src/github.com/subutai-io/gorjun/libgorjun/gorjun.service /etc/systemd/system/gorjun.service + - sudo systemctl daemon-reload + - sudo systemctl start gorjun.service + - sudo systemctl status gorjun.service + - sudo apt install -y rng-tools + - sudo rngd -r /dev/urandom + - gpg --gen-key --batch /home/travis/gopath/src/github.com/subutai-io/gorjun/libgorjun/gpg.txt + - sudo chmod +x /home/travis/gopath/src/github.com/subutai-io/gorjun/libgorjun/register.sh + - cd /home/travis/gopath/src/github.com/emli/subutai-io/libgorjun/; ./register.sh + - cd /home/travis/gopath/src/github.com/emli/subutai-io/libgorjun; go get github.com/stretchr/testify/assert; + - go test -v \ No newline at end of file diff --git a/libgorjun/auth.go b/libgorjun/auth.go new file mode 100644 index 0000000..d415303 --- /dev/null +++ b/libgorjun/auth.go @@ -0,0 +1,211 @@ +package gorjun + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + "mime/multipart" +) + +func (g *GorjunServer) RegisterUser(username string, publicKey string) (string, error) { + var b bytes.Buffer + w := multipart.NewWriter(&b) + + fw, err := w.CreateFormField("name") + if err != nil { + return "", fmt.Errorf("Failed to create name form: %v", err) + } + if _, err = fw.Write([]byte(username)); err != nil { + return "", fmt.Errorf("Failed to write token: %v", err) + } + if fw, err = w.CreateFormField("key"); err != nil { + return "", fmt.Errorf("Failed to create key form field: %v", err) + } + if _, err = fw.Write([]byte(publicKey)); err != nil { + return "", fmt.Errorf("Failed to write token: %v", err) + } + + w.Close() + + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/kurjun/rest/auth/register", g.Hostname), &b) + if err != nil { + return "", fmt.Errorf("Failed to create HTTP request: %v", err) + } + req.Header.Set("Content-Type", w.FormDataContentType()) + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("Failed to execute HTTP request: %v", err) + } + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("Registration failed. Server returned %s error", res.Status) + } + response, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("Failed to read response body: %v", err) + } + return string(response), nil +} + +// AuthenticateUser will try to authenticate user by downloading his token code, signing it with GPG +// and sending it back to server to get user token +// If passphrase is not empty, PGP will try to decrypt the private key before signing the code +// if gpgdir is empty, the default ($HOME/.gnupg) will be used +func (g *GorjunServer) AuthenticateUser() error { + err := g.GetAuthTokenCode() + if err != nil { + return err + } + sign, err := g.SignToken(g.TokenCode) + if err != nil { + return err + } + err = g.GetActiveToken(sign) + if err != nil { + return err + } + return nil +} + +// GetAuthTokenCode is a first step of authentication - it requests a special code from the server. +// This code needs to be PGP-signed later +func (g *GorjunServer) GetAuthTokenCode() error { + resp, err := http.Get(fmt.Sprintf("http://%s/kurjun/rest/auth/token?user=%s", g.Hostname, g.Username)) + if err != nil { + return fmt.Errorf("Failed to retrieve unsigned token: %v", err) + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("Failed to read body from %s: %v", g.Hostname, err) + } + g.TokenCode = string(data) + return nil +} + +// GetActiveToken will send signed message to server and return active token +// that will be used for authneticated requests +func (g *GorjunServer) GetActiveToken(signed string) error { + signed = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" + g.TokenCode + "\n" + signed + "\n" + form := url.Values{ + "message": {signed}, + "user": {g.Username}, + } + body := bytes.NewBufferString(form.Encode()) + resp, err := http.Post(fmt.Sprintf("http://%s/kurjun/rest/auth/token", g.Hostname), "application/x-www-form-urlencoded", body) + if err != nil { + return fmt.Errorf("Failed to retrieve active token: %v", err) + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("Failed to read body from %s: %v", g.Hostname, err) + } + g.Token = string(data) + if len(g.Token) != 64 { + reason := g.Token + g.Token = "" + return fmt.Errorf("Failed to retrieve active token: %s", reason) + } + return nil +} + +func (g *GorjunServer) GetKeyByEmail(keyring openpgp.EntityList, email string) *openpgp.Entity { + for _, entity := range keyring { + for _, ident := range entity.Identities { + if ident.UserId.Email == email { + return entity + } + } + } + return nil +} + +// SignToken will sign with GnuPG provided token and return signed version +func (g *GorjunServer) SignToken(token string) (string, error) { + if g.GPGDirectory == "" { + return "", fmt.Errorf("GPG Directory was not specified") + } + // GPG may have two variants of key storage - in secring.gpg/pubring.gpg for older versions + // and for pubring.kbx and separate directory for private key in version of GnuPG 2.1+ + pubringPath := g.GPGDirectory + "/pubring.gpg" + if _, err := os.Stat(pubringPath); os.IsNotExist(err) { + pubringPath = g.GPGDirectory + "/pubring.kbx" + } + if _, err := os.Stat(pubringPath); os.IsNotExist(err) { + return "", fmt.Errorf("Can't find pubring.gpg nor pubring.kbx") + } + pukFile, err := os.Open(g.GPGDirectory + "/pubring.gpg") + defer pukFile.Close() + if err != nil { + return "", fmt.Errorf("Failed to open public keyring file: %v", err) + } + pubring, err := openpgp.ReadKeyRing(pukFile) + if err != nil { + return "", fmt.Errorf("Failed to read public keyring: %v", err) + } + publicKey := g.GetKeyByEmail(pubring, g.Email) + if publicKey == nil { + return "", fmt.Errorf("Public key for %s was not found", g.Email) + } + + priFile, err := os.Open(g.GPGDirectory + "/secring.gpg") + defer priFile.Close() + if err != nil { + return "", fmt.Errorf("Failed to open private keyring file: %v", err) + } + secring, err := openpgp.ReadKeyRing(priFile) + if err != nil { + return "", fmt.Errorf("Failed to read private keyring: %v", err) + } + privateKey := g.GetKeyByEmail(secring, g.Email) + if privateKey == nil { + return "", fmt.Errorf("Private key for %s was not found", g.Email) + } + if g.Passphrase != "" { + privateKey.PrivateKey.Decrypt([]byte(g.Passphrase)) + } + outBuf := new(bytes.Buffer) + err = openpgp.ArmoredDetachSign(outBuf, privateKey, strings.NewReader(token), nil) + if err != nil { + return "", fmt.Errorf("Failed to sign token: %s", err) + } + return outBuf.String(), nil +} + +func (g *GorjunServer) decodePrivateKey() (*packet.PrivateKey, error) { + in, err := os.Open(g.GPGDirectory + "/secring.gpg") + if err != nil { + in.Close() + return nil, err + } + + block, err := armor.Decode(in) + if err != nil { + return nil, fmt.Errorf("Failed to decode GPG Armor: %s", err) + } + + if block.Type != openpgp.PrivateKeyType { + return nil, fmt.Errorf("Invalid private key file") + } + + reader := packet.NewReader(block.Body) + pkt, err := reader.Next() + if err != nil { + return nil, fmt.Errorf("Error reading private key") + } + + key, success := pkt.(*packet.PrivateKey) + if !success { + return nil, fmt.Errorf("Error parsing private key") + } + return key, nil +} diff --git a/libgorjun/auth_test.go b/libgorjun/auth_test.go new file mode 100644 index 0000000..3521953 --- /dev/null +++ b/libgorjun/auth_test.go @@ -0,0 +1,117 @@ +package gorjun + +import ( + "fmt" + "testing" + "time" + "math/rand" + "net/http" + "io/ioutil" + "encoding/json" + "github.com/stretchr/testify/assert" +) + +var r *rand.Rand // Rand for this package. + +func init() { + r = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +func RandomString(strlen int) string { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789" + result := make([]byte, strlen) + for i := range result { + result[i] = chars[r.Intn(len(chars))] + } + return string(result) +} + +func TestGorjunServer_AuthenticateUser(t *testing.T) { + g := NewGorjunServer(); + _, err := g.RegisterUser("tester", "publickey") + if err != nil { + t.Errorf("Failed to register user: %v", err) + } +} + +func TestGorjunServer_RegisterUserWithMultipleKeys(t *testing.T) { + g := NewGorjunServer(); + randomUserName := RandomString(10) + for i:= 1; i <= 100; i++ { + randomKey := RandomString(100) + _, err := g.RegisterUser(randomUserName, randomKey) + if err != nil { + t.Errorf("Failed to register user: %v", err) + } + resp, err := http.Get(fmt.Sprintf("http://%s/kurjun/rest/auth/keys?user=%s", g.Hostname, randomUserName)) + if err != nil { + fmt.Errorf("Failed to retrieve user keys: %v", err) + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + fmt.Errorf("Failed to read body from %s: %v", g.Hostname, err) + } + var f []GorjunFile + err = json.Unmarshal(data, &f) + if err != nil { + fmt.Errorf("Failed to unmarshal contents from %s: %v", g.Hostname, err) + } + assert.Equal(t, i, len(f), "Numbers of key should be equal") + } +} + +func TestGorjunServer_GetKeysByOwner(t *testing.T) { + g := NewGorjunServer(); + artifactTypes := [3]string{"template", "raw", "apt"} + for i:= 0; i < len(artifactTypes); i++ { + resp, err := http.Get(fmt.Sprintf("http://%s/kurjun/rest/" + artifactTypes[i] + "/list", g.Hostname)) + if err != nil { + fmt.Errorf("Failed to retrieve list of %s s: %v", artifactTypes[i], err) + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + var artifacts []GorjunFile + err = json.Unmarshal(data, &artifacts) + for j:= 0; j < len(artifacts); j++ { + if len(artifacts[j].Owner) > 0 { + resp, _ := http.Get(fmt.Sprintf("http://%s/kurjun/rest/auth/keys?user=%s", g.Hostname, artifacts[j].Owner[0])) + data, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + var keys []Keys + err = json.Unmarshal(data, &keys) + assert.NotEqual(t, len(keys), 0, "Keys of existing user should be greater than zero") + } + } + } + +} +func TestGetAuthTokenCode(t *testing.T) { + g := NewGorjunServer(); + err := g.GetAuthTokenCode() + if err != nil { + t.Errorf("Failed to retrieve token: %v", err) + } + if len(g.TokenCode) != 32 { + t.Errorf("Token length doesn't equals 32 symbols: %d", len(g.TokenCode)) + } +} + +func TestGetActiveToken(t *testing.T) { + g := NewGorjunServer() + err := g.GetAuthTokenCode() + if err != nil { + t.Errorf("Failed to retrieve token: %v", err) + } + fmt.Printf("Token code: %s\n", g.TokenCode) + sign, err := g.SignToken(g.TokenCode) + if err != nil { + t.Errorf("Failed to sign token code: %v", err) + } + fmt.Printf("Signed token code: %s\n", sign) + err = g.GetActiveToken(sign) + if err != nil { + t.Errorf("Failed to get active token: %v", err) + } + fmt.Printf("Active token: %s, len: %d\n", g.Token, len(g.Token)) +} diff --git a/libgorjun/data/abdysamat-apache-subutai-template_4.0.0_amd64.tar.gz b/libgorjun/data/abdysamat-apache-subutai-template_4.0.0_amd64.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ce4cde0500a410d0d8cc5b693ba6d7cc876255a7 GIT binary patch literal 637991 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