Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Continuous integration for Gorjun

Add travis file for CI and libgorjun for tests.
  • Loading branch information
emli committed Feb 19, 2018
1 parent f47e27b commit f11073b
Show file tree
Hide file tree
Showing 13 changed files with 568 additions and 7 deletions.
26 changes: 19 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
211 changes: 211 additions & 0 deletions libgorjun/auth.go
Original file line number Diff line number Diff line change
@@ -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
}
117 changes: 117 additions & 0 deletions libgorjun/auth_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit f11073b

Please sign in to comment.