integration_test.go 5.24 KB
Newer Older
ale's avatar
ale committed
1 2 3 4
package integrationtest

import (
	"bytes"
5
	"context"
ale's avatar
ale committed
6
	"encoding/json"
ale's avatar
ale committed
7
	"errors"
ale's avatar
ale committed
8 9 10 11 12 13 14 15 16
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

17
	as "git.autistici.org/ai3/accountserver"
ale's avatar
ale committed
18 19 20
	"git.autistici.org/ai3/accountserver/backend"
	"git.autistici.org/ai3/accountserver/ldaptest"
	"git.autistici.org/ai3/accountserver/server"
21 22
	"git.autistici.org/ai3/go-common/pwhash"
	"git.autistici.org/ai3/go-common/userenckey"
23
	"git.autistici.org/id/go-sso"
ale's avatar
ale committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
	"golang.org/x/crypto/ed25519"
)

const (
	testLDAPPort = 42872
	testLDAPAddr = "ldap://127.0.0.1:42872"

	testSSODomain  = "domain"
	testSSOService = "accountserver.domain/"
	testAdminUser  = "admin"
	testAdminGroup = "admins"
)

func withSSO(t testing.TB) (func(), sso.Signer, string) {
	tmpf, err := ioutil.TempFile("", "")
	if err != nil {
		t.Fatal(err)
	}

	pub, priv, err := ed25519.GenerateKey(nil)
	if err != nil {
		t.Fatal(err)
	}
	tmpf.Write(pub)
	tmpf.Close()

	signer, err := sso.NewSigner(priv)
	if err != nil {
		t.Fatal(err)
	}

	return func() {
		os.Remove(tmpf.Name())
	}, signer, tmpf.Name()
}

type testClient struct {
	srvURL string
	signer sso.Signer
}

65 66
func (c *testClient) ssoTicket(username string, groups ...string) string {
	if len(groups) == 0 && username == testAdminUser {
ale's avatar
ale committed
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
		groups = append(groups, testAdminGroup)
	}
	signed, err := c.signer.Sign(sso.NewTicket(username, testSSOService, testSSODomain, "", groups, 1*time.Hour))
	if err != nil {
		panic(err)
	}
	return signed
}

func (c *testClient) request(uri string, req, out interface{}) error {
	data, _ := json.Marshal(req)
	resp, err := http.Post(c.srvURL+uri, "application/json", bytes.NewReader(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
ale's avatar
ale committed
83 84 85 86 87 88
	data, _ = ioutil.ReadAll(resp.Body)

	if resp.StatusCode >= 400 && resp.StatusCode < 500 {
		log.Printf("request error: %s", string(data))
		return errors.New(string(data))
	}
ale's avatar
ale committed
89
	if resp.StatusCode != 200 {
ale's avatar
ale committed
90
		log.Printf("remote error: %s", string(data))
ale's avatar
ale committed
91 92 93 94 95 96 97 98 99 100 101 102
		return fmt.Errorf("http status code %d", resp.StatusCode)
	}
	if resp.Header.Get("Content-Type") != "application/json" {
		return fmt.Errorf("unexpected content-type %s", resp.Header.Get("Content-Type"))
	}

	if out == nil {
		return nil
	}
	return json.Unmarshal(data, out)
}

103
func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backend, *testClient) {
ale's avatar
ale committed
104
	stop := ldaptest.StartServer(t, &ldaptest.Config{
105 106 107 108 109 110 111
		Dir:  "../ldaptest",
		Port: testLDAPPort,
		Base: "dc=example,dc=com",
		LDIFs: []string{
			"testdata/base.ldif",
			"testdata/test1.ldif",
			"testdata/test2.ldif",
112
			"testdata/test3.ldif",
113
		},
ale's avatar
ale committed
114 115 116 117 118 119 120 121 122 123 124 125 126
	})

	be, err := backend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
	if err != nil {
		t.Fatal("NewLDAPBackend", err)
	}

	ssoStop, signer, ssoPubKeyFile := withSSO(t)

	svcConfig.SSO.PublicKeyFile = ssoPubKeyFile
	svcConfig.SSO.Domain = testSSODomain
	svcConfig.SSO.Service = testSSOService
	svcConfig.SSO.AdminGroup = testAdminGroup
ale's avatar
ale committed
127
	svcConfig.ForbiddenUsernames = []string{"forbidden"}
ale's avatar
ale committed
128
	svcConfig.AvailableDomains = map[string][]string{
129 130
		as.ResourceTypeEmail:       []string{"example.com"},
		as.ResourceTypeMailingList: []string{"example.com"},
ale's avatar
ale committed
131
	}
ale's avatar
ale committed
132
	shards := []string{"host1", "host2", "host3"}
ale's avatar
ale committed
133
	svcConfig.Shards.Available = map[string][]string{
134 135 136 137 138 139
		as.ResourceTypeEmail:       shards,
		as.ResourceTypeMailingList: shards,
		as.ResourceTypeWebsite:     shards,
		as.ResourceTypeDomain:      shards,
		as.ResourceTypeDAV:         shards,
		as.ResourceTypeDatabase:    shards,
ale's avatar
ale committed
140 141
	}
	svcConfig.Shards.Allowed = svcConfig.Shards.Available
ale's avatar
ale committed
142
	svcConfig.WebsiteRootDir = "/home/users/investici.org"
ale's avatar
ale committed
143

144
	service, err := as.NewAccountService(be, &svcConfig)
ale's avatar
ale committed
145 146 147 148 149 150
	if err != nil {
		stop()
		t.Fatal("NewAccountService", err)
	}

	as := server.New(service, be)
151
	srv := httptest.NewServer(as)
ale's avatar
ale committed
152 153 154 155 156 157 158 159 160 161

	c := &testClient{
		srvURL: srv.URL,
		signer: signer,
	}

	return func() {
		stop()
		srv.Close()
		ssoStop()
162
	}, be, c
ale's avatar
ale committed
163 164
}

165 166 167 168
func startService(t testing.TB) (func(), as.Backend, *testClient) {
	return startServiceWithConfig(t, as.Config{})
}

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
// Verify that some user authentication invariants are true. Returns
// the RawUser for further checks.
func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword string) *as.RawUser {
	tx, _ := be.NewTransaction()
	user, err := tx.GetUser(context.Background(), username)
	if err != nil {
		t.Fatalf("GetUser(%s): %v", username, err)
	}

	// Verify that the password is correct.
	if !pwhash.ComparePassword(user.Password, primaryPassword) {
		t.Fatalf("password for user %s is not %s", username, primaryPassword)
	}

	// Verify that we can successfully encrypt keys.
	if user.HasEncryptionKeys {
		if _, err := userenckey.Decrypt(keysToBytes(user.Keys), []byte(primaryPassword)); err != nil {
			t.Fatalf("password for user %s can't decrypt keys", username)
		}
	}

ale's avatar
ale committed
190 191 192 193 194 195 196 197 198
	// Verify that the user shard matches the email resource shard.
	email := user.GetSingleResourceByType(as.ResourceTypeEmail)
	if email == nil {
		t.Fatalf("no email resources for user %s", username)
	}
	if user.Shard != email.Shard {
		t.Fatalf("user and email shards differ ('%s' vs '%s')", user.Shard, email.Shard)
	}

199 200 201 202 203 204 205 206 207 208
	return user
}

func keysToBytes(keys []*as.UserEncryptionKey) [][]byte {
	var rawKeys [][]byte
	for _, k := range keys {
		rawKeys = append(rawKeys, k.Key)
	}
	return rawKeys
}