Commit aef048c2 authored by ale's avatar ale

Add integration tests

Start a full HTTP server, backed by an in-memory LDAP server, and test
the API directly.
parent 9a84289a
......@@ -3,7 +3,6 @@ package backend
import (
"context"
"errors"
"fmt"
"os"
"strings"
......@@ -30,6 +29,7 @@ type ldapConn interface {
// all users.
type backend struct {
conn ldapConn
baseDN string
userQuery *queryConfig
userResourceQueries []*queryConfig
resources *resourceRegistry
......@@ -71,7 +71,8 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
rsrc.register(accountserver.ResourceTypeDatabase, &databaseResourceHandler{baseDN: base})
return &backend{
conn: conn,
conn: conn,
baseDN: base,
userQuery: mustCompileQueryConfig(&queryConfig{
Base: "uid=${user},ou=People," + base,
Scope: "base",
......@@ -186,9 +187,8 @@ func newUser(entry *ldap.Entry) (*accountserver.User, error) {
return user, nil
}
func getUserDN(user *accountserver.User) string {
// TODO: fix this
return fmt.Sprintf("uid=%s,ou=People,", user.Name)
func (tx *backendTX) getUserDN(user *accountserver.User) string {
return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
}
// GetUser returns a user.
......@@ -266,23 +266,23 @@ func (tx *backendTX) readAttributeValues(ctx context.Context, dn, attribute stri
}
func (tx *backendTX) SetUserPassword(ctx context.Context, user *accountserver.User, encryptedPassword string) error {
tx.setAttr(getUserDN(user), "userPassword", encryptedPassword)
tx.setAttr(tx.getUserDN(user), "userPassword", encryptedPassword)
return nil
}
func (tx *backendTX) GetUserEncryptionKeys(ctx context.Context, user *accountserver.User) ([]*accountserver.UserEncryptionKey, error) {
rawKeys := tx.readAttributeValues(ctx, getUserDN(user), "storageEncryptionKey")
rawKeys := tx.readAttributeValues(ctx, tx.getUserDN(user), "storageEncryptionKey")
return decodeUserEncryptionKeys(rawKeys), nil
}
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *accountserver.User, keys []*accountserver.UserEncryptionKey) error {
encKeys := encodeUserEncryptionKeys(keys)
tx.setAttr(getUserDN(user), "storageEncryptionKey", encKeys...)
tx.setAttr(tx.getUserDN(user), "storageEncryptionKey", encKeys...)
return nil
}
func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *accountserver.User, pub []byte) error {
tx.setAttr(getUserDN(user), "storageEncryptionPublicKey", string(pub))
tx.setAttr(tx.getUserDN(user), "storageEncryptionPublicKey", string(pub))
return nil
}
......@@ -328,12 +328,12 @@ func (tx *backendTX) DeleteApplicationSpecificPassword(ctx context.Context, user
}
func (tx *backendTX) SetUserTOTPSecret(ctx context.Context, user *accountserver.User, secret string) error {
tx.setAttr(getUserDN(user), "totpSecret", secret)
tx.setAttr(tx.getUserDN(user), "totpSecret", secret)
return nil
}
func (tx *backendTX) DeleteUserTOTPSecret(ctx context.Context, user *accountserver.User) error {
tx.setAttr(getUserDN(user), "totpSecret")
tx.setAttr(tx.getUserDN(user), "totpSecret")
return nil
}
......
package integrationtest
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.autistici.org/ai3/accountserver"
"git.autistici.org/ai3/accountserver/backend"
"git.autistici.org/ai3/accountserver/ldaptest"
"git.autistici.org/ai3/accountserver/server"
sso "git.autistici.org/id/go-sso"
"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
}
func (c *testClient) ssoTicket(username string) string {
var groups []string
if username == testAdminUser {
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()
if resp.StatusCode != 200 {
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"))
}
data, _ = ioutil.ReadAll(resp.Body)
log.Printf("response:\n%s\n", string(data))
if out == nil {
return nil
}
return json.Unmarshal(data, out)
}
func startService(t testing.TB) (func(), *testClient) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com",
LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
})
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)
var svcConfig accountserver.Config
svcConfig.SSO.PublicKeyFile = ssoPubKeyFile
svcConfig.SSO.Domain = testSSODomain
svcConfig.SSO.Service = testSSOService
svcConfig.SSO.AdminGroup = testAdminGroup
svcConfig.AvailableDomains = map[string][]string{
accountserver.ResourceTypeEmail: []string{"example.com"},
}
service, err := accountserver.NewAccountService(be, &svcConfig)
if err != nil {
stop()
t.Fatal("NewAccountService", err)
}
as := server.New(service, be)
srv := httptest.NewServer(as.Handler())
c := &testClient{
srvURL: srv.URL,
signer: signer,
}
return func() {
stop()
srv.Close()
ssoStop()
}, c
}
// Verify that authentication on GetUser works as expected:
// - users can fetch their own data but not other users'
// - admins can read everything.
func TestIntegration_GetUser_Auth(t *testing.T) {
stop, c := startService(t)
defer stop()
testdata := []struct {
authUser string
expectedOk bool
}{
{"uno@investici.org", true},
{"due@investici.org", false},
{testAdminUser, true},
}
for _, td := range testdata {
var user accountserver.User
err := c.request("/api/user/get", &accountserver.GetUserRequest{
RequestBase: accountserver.RequestBase{
Username: "uno@investici.org",
SSO: c.ssoTicket(td.authUser),
},
}, &user)
if td.expectedOk && err != nil {
t.Errorf("access error for user %s: expected ok, got error: %v", td.authUser, err)
} else if !td.expectedOk && err == nil {
t.Errorf("access error for user %s: expected error, got ok", td.authUser)
}
}
}
// Verify that authentication on GetResource works as expected:
// - users can fetch their own data but not other users'
// - admins can read everything.
func TestIntegration_ChangeUserPassword(t *testing.T) {
stop, c := startService(t)
defer stop()
err := c.request("/api/user/change_password", &accountserver.ChangeUserPasswordRequest{
PrivilegedRequestBase: accountserver.PrivilegedRequestBase{
RequestBase: accountserver.RequestBase{
Username: "uno@investici.org",
SSO: c.ssoTicket("uno@investici.org"),
},
CurPassword: "password",
},
Password: "new_password",
}, nil)
if err != nil {
t.Fatal("ChangePassword", err)
}
}
dn: dc=example,dc=com
objectclass: domain
objectclass: top
dc: example
dn: ou=People,dc=example,dc=com
objectclass: top
objectclass: organizationalUnit
ou: People
dn: ou=Lists,dc=example,dc=com
objectclass: top
objectclass: organizationalUnit
ou: Lists
dn: uid=uno@investici.org,ou=People,dc=example,dc=com
cn: uno@investici.org
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
loginShell: /bin/false
uidNumber: 19475
shadowMax: 99999
gidNumber: 2000
gecos: uno@investici.org
sn: Private
homeDirectory: /var/empty
uid: uno@investici.org
givenName: Private
shadowLastChange: 12345
shadowWarning: 7
preferredLanguage: it
userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ
FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5
WEtkeDV0QTE=
dn: mail=uno@investici.org,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
recoverQuestion:: dGkgc2VpIG1haSDDuMOgdHRvIG1hbGUgY2FkZW5kbyBkYSB1biBwYWxhenpv
IGRpIG90dG8gcGlhbmk/
objectClass: top
objectClass: virtualMailUser
userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ
FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5
WEtkeDV0QTE=
uidNumber: 19475
host: host2
mailAlternateAddress: uno@anche.no
recoverAnswer: {crypt}$1$wtEa4TKB$lxeyenkQ1yfxECn7WVQQ0/
gidNumber: 2000
mail: uno@investici.org
creationDate: 2002-05-07
mailMessageStore: investici.org/uno/
originalHost: host2
dn: ftpname=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
givenName: Private
cn: uno
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: ftpAccount
loginShell: /bin/false
shadowWarning: 7
uidNumber: 19475
host: host2
shadowMax: 99999
ftpname: uno
gidNumber: 33
gecos: FTP Account for uno@investici.org
sn: Private
homeDirectory: /home/users/investici.org/uno
uid: uno
creationDate: 01-08-2013
shadowLastChange: 12345
originalHost: host2
userPassword:: e2NyeXB0fSQ2JElDYkx1WTI3QWl6bC5FeEgkUDhOZHJ3VEtxZ2UwQUp3QW9oNE1
EYlUxU3EySGtuRkF1cEx2RUI0U28waEw5NWtpZ3dIeXQuQnYxS0J5SFM2MXd6RnZuLnJsMEN4eFpx
RVgzUnVxbDE=
dn: alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
parentSite: autistici.org
objectClass: top
objectClass: subSite
alias: uno
host: host2
documentRoot: /home/users/investici.org/uno/html-uno
creationDate: 01-08-2013
originalHost: host2
statsId: 2191
dn: cn=example.com,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
acceptMail: true
objectClass: top
objectClass: virtualHost
cn: example.com
host: host2
documentRoot: /home/users/investici.org/uno/html-example.com
creationDate: 02-08-2013
originalHost: host2
statsId: 2192
dn: dbname=unodb,alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
clearPassword: password
objectClass: top
objectClass: dbMysql
dbname: unodb
dbuser: unodb
host: host2
creationDate: 01-08-2013
originalHost: host2
......@@ -10,7 +10,7 @@ import (
as "git.autistici.org/ai3/accountserver"
)
type txHandler func(as.TX, http.ResponseWriter, *http.Request) error
type txHandler func(as.TX, http.ResponseWriter, *http.Request) (interface{}, error)
var errBadRequest = errors.New("bad request")
......@@ -26,7 +26,7 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer {
}
}
var emptyResponse = map[string]string{}
var emptyResponse struct{}
func errToStatus(err error) int {
switch {
......@@ -41,168 +41,158 @@ func errToStatus(err error) int {
}
}
func (s *AccountServer) handleGetUser(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleGetUser(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.GetUserRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
user, err := s.service.GetUser(r.Context(), tx, &req)
if err != nil {
log.Printf("GetUser(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, user)
return nil
return user, nil
}
func (s *AccountServer) handleChangeUserPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleChangeUserPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.ChangeUserPasswordRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.ChangeUserPassword(r.Context(), tx, &req); err != nil {
log.Printf("ChangeUserPassword(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) handleCreateApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleCreateApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.CreateApplicationSpecificPasswordRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
resp, err := s.service.CreateApplicationSpecificPassword(r.Context(), tx, &req)
if err != nil {
log.Printf("CreateApplicationSpecificPassword(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, resp)
return nil
return resp, nil
}
func (s *AccountServer) handleDeleteApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleDeleteApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.DeleteApplicationSpecificPasswordRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.DeleteApplicationSpecificPassword(r.Context(), tx, &req); err != nil {
log.Printf("DeleteApplicationSpecificPassword(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) handleEnableResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleEnableResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.EnableResourceRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.EnableResource(r.Context(), tx, &req); err != nil {
log.Printf("EnableResource(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) handleDisableResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleDisableResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.DisableResourceRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.DisableResource(r.Context(), tx, &req); err != nil {
log.Printf("DisableResource(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) handleChangeResourcePassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleChangeResourcePassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.ChangeResourcePasswordRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.ChangeResourcePassword(r.Context(), tx, &req); err != nil {
log.Printf("ChangeResourcePassword(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.MoveResourceRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
resp, err := s.service.MoveResource(r.Context(), tx, &req)
if err != nil {
log.Printf("MoveResource(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, resp)
return nil
return resp, nil
}
func (s *AccountServer) handleEnableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleEnableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.EnableOTPRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
resp, err := s.service.EnableOTP(r.Context(), tx, &req)
if err != nil {
log.Printf("EnableOTP(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, resp)
return nil
return resp, nil
}
func (s *AccountServer) handleDisableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) error {
func (s *AccountServer) handleDisableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.DisableOTPRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return errBadRequest
return nil, errBadRequest
}
if err := s.service.DisableOTP(r.Context(), tx, &req); err != nil {
log.Printf("DisableOTP(%s): error: %v", req.Username, err)
http.Error(w, err.Error(), errToStatus(err))
return err
return nil, err
}
serverutil.EncodeJSONResponse(w, emptyResponse)
return nil
return &emptyResponse, nil
}
func (s *AccountServer) withTx(f txHandler) http.HandlerFunc {
......@@ -214,15 +204,19 @@ func (s *AccountServer) withTx(f txHandler) http.HandlerFunc {
return
}
if err := f(tx, w, r); err == nil {
// Automatically commit the transaction (if
// the handler didn't do this itself).
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Commit error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := f(tx, w, r)
if err != nil {
return
}
// Automatically commit the transaction (if
// the handler didn't do this itself).
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Commit error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
serverutil.EncodeJSONResponse(w, resp)
}
}
......
......@@ -161,7 +161,9 @@ func (i *ResourceID) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
*i, err = ParseResourceID(s)
if s != "" {
*i, err = ParseResourceID(s)
}
return err
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment