Skip to content

Commit face844

Browse files
authored
Support registering as an admin via shared secrets (#321)
* Support registering as an admin via shared secrets Requires homeservers to use 'complement' as the shared secret. * Add extra requirement on images README * nolint * Fix broken admin flag * Needs
1 parent 2f108ce commit face844

12 files changed

+83
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ If you're looking to run against a custom Dockerfile, it must meet the following
7676
- The homeserver needs to manage its own storage within the image.
7777
- The homeserver needs to accept the server name given by the environment variable `SERVER_NAME` at runtime.
7878
- The homeserver needs to assume dockerfile `CMD` or `ENTRYPOINT` instructions will be run multiple times.
79+
- The homeserver needs to use `complement` as the registration shared secret for `/_synapse/admin/v1/register`, if supported. If this endpoint 404s then these tests are skipped.
7980

8081

8182
### Developing locally

dockerfiles/synapse/homeserver.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enable_registration: true
1212
tls_certificate_path: /conf/server.tls.crt
1313
tls_private_key_path: /conf/server.tls.key
1414
bcrypt_rounds: 4
15+
registration_shared_secret: complement
1516

1617
listeners:
1718
- port: 8448

internal/client/client.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package client
22

33
import (
44
"bytes"
5+
"crypto/hmac"
6+
"crypto/sha1" // nolint:gosec
7+
"encoding/hex"
58
"encoding/json"
69
"fmt"
710
"io/ioutil"
@@ -17,6 +20,11 @@ import (
1720
"github.com/tidwall/gjson"
1821

1922
"github.com/matrix-org/complement/internal/b"
23+
"github.com/matrix-org/complement/internal/must"
24+
)
25+
26+
const (
27+
SharedSecret = "complement"
2028
)
2129

2230
// RequestOpt is a functional option which will modify an outgoing HTTP request.
@@ -313,6 +321,44 @@ func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID,
313321
return userID, accessToken
314322
}
315323

324+
// RegisterSharedSecret registers a new account with a shared secret via HMAC
325+
// See https://github.com/matrix-org/synapse/blob/e550ab17adc8dd3c48daf7fedcd09418a73f524b/synapse/_scripts/register_new_matrix_user.py#L40
326+
func (c *CSAPI) RegisterSharedSecret(t *testing.T, user, pass string, isAdmin bool) (userID, password string) {
327+
resp := c.DoFunc(t, "GET", []string{"_synapse", "admin", "v1", "register"})
328+
if resp.StatusCode != 200 {
329+
t.Skipf("Homeserver image does not support shared secret registration, /_synapse/admin/v1/register returned HTTP %d", resp.StatusCode)
330+
return
331+
}
332+
body := must.ParseJSON(t, resp.Body)
333+
nonce := gjson.GetBytes(body, "nonce")
334+
if !nonce.Exists() {
335+
t.Fatalf("Malformed shared secret GET response: %s", string(body))
336+
}
337+
mac := hmac.New(sha1.New, []byte(SharedSecret))
338+
mac.Write([]byte(nonce.Str))
339+
mac.Write([]byte("\x00"))
340+
mac.Write([]byte(user))
341+
mac.Write([]byte("\x00"))
342+
mac.Write([]byte(pass))
343+
mac.Write([]byte("\x00"))
344+
if isAdmin {
345+
mac.Write([]byte("admin"))
346+
} else {
347+
mac.Write([]byte("notadmin"))
348+
}
349+
sig := mac.Sum(nil)
350+
reqBody := map[string]interface{}{
351+
"nonce": nonce.Str,
352+
"username": user,
353+
"password": pass,
354+
"mac": hex.EncodeToString(sig),
355+
"admin": isAdmin,
356+
}
357+
resp = c.MustDoFunc(t, "POST", []string{"_synapse", "admin", "v1", "register"}, WithJSONBody(t, reqBody))
358+
body = must.ParseJSON(t, resp.Body)
359+
return gjson.GetBytes(body, "user_id").Str, gjson.GetBytes(body, "access_token").Str
360+
}
361+
316362
// GetCapbabilities queries the server's capabilities
317363
func (c *CSAPI) GetCapabilities(t *testing.T) []byte {
318364
t.Helper()

internal/docker/deployment.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (d *Deployment) Client(t *testing.T, hsName, userID string) *client.CSAPI {
6262
}
6363

6464
// RegisterUser within a homeserver and return an authenticatedClient, Fails the test if the hsName is not found.
65-
func (d *Deployment) RegisterUser(t *testing.T, hsName, localpart, password string) *client.CSAPI {
65+
func (d *Deployment) RegisterUser(t *testing.T, hsName, localpart, password string, isAdmin bool) *client.CSAPI {
6666
t.Helper()
6767
dep, ok := d.HS[hsName]
6868
if !ok {
@@ -75,7 +75,12 @@ func (d *Deployment) RegisterUser(t *testing.T, hsName, localpart, password stri
7575
SyncUntilTimeout: 5 * time.Second,
7676
Debug: d.Deployer.debugLogging,
7777
}
78-
userID, accessToken := client.RegisterUser(t, localpart, password)
78+
var userID, accessToken string
79+
if isAdmin {
80+
userID, accessToken = client.RegisterSharedSecret(t, localpart, password, isAdmin)
81+
} else {
82+
userID, accessToken = client.RegisterUser(t, localpart, password)
83+
}
7984

8085
// remember the token so subsequent calls to deployment.Client return the user
8186
dep.AccessTokens[userID] = accessToken

tests/csapi/account_change_password_pushers_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build !dendrite_blacklist
12
// +build !dendrite_blacklist
23

34
package csapi_tests
@@ -18,7 +19,7 @@ func TestChangePasswordPushers(t *testing.T) {
1819
defer deployment.Destroy(t)
1920
password1 := "superuser"
2021
password2 := "my_new_password"
21-
passwordClient := deployment.RegisterUser(t, "hs1", "test_change_password_pusher_user", password1)
22+
passwordClient := deployment.RegisterUser(t, "hs1", "test_change_password_pusher_user", password1, false)
2223

2324
// sytest: Pushers created with a different access token are deleted on password change
2425
t.Run("Pushers created with a different access token are deleted on password change", func(t *testing.T) {

tests/csapi/account_change_password_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestChangePassword(t *testing.T) {
1818
defer deployment.Destroy(t)
1919
password1 := "superuser"
2020
password2 := "my_new_password"
21-
passwordClient := deployment.RegisterUser(t, "hs1", "test_change_password_user", password1)
21+
passwordClient := deployment.RegisterUser(t, "hs1", "test_change_password_user", password1, false)
2222
unauthedClient := deployment.Client(t, "hs1", "")
2323
sessionTest := createSession(t, deployment, "test_change_password_user", "superuser")
2424
// sytest: After changing password, can't log in with old password

tests/csapi/account_deactivate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestDeactivateAccount(t *testing.T) {
1414
deployment := Deploy(t, b.BlueprintAlice)
1515
defer deployment.Destroy(t)
1616
password := "superuser"
17-
authedClient := deployment.RegisterUser(t, "hs1", "test_deactivate_user", password)
17+
authedClient := deployment.RegisterUser(t, "hs1", "test_deactivate_user", password, false)
1818
unauthedClient := deployment.Client(t, "hs1", "")
1919
// sytest: Can't deactivate account with wrong password
2020
t.Run("Can't deactivate account with wrong password", func(t *testing.T) {

tests/csapi/admin_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package csapi_tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/matrix-org/complement/internal/b"
7+
)
8+
9+
// Check if this homeserver supports Synapse-style admin registration.
10+
// Not all images support this currently.
11+
func TestCanRegisterAdmin(t *testing.T) {
12+
deployment := Deploy(t, b.BlueprintAlice)
13+
defer deployment.Destroy(t)
14+
deployment.RegisterUser(t, "hs1", "admin", "adminpassword", true)
15+
}

tests/csapi/apidoc_device_management_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestDeviceManagement(t *testing.T) {
1515
deployment := Deploy(t, b.BlueprintAlice)
1616
defer deployment.Destroy(t)
1717
unauthedClient := deployment.Client(t, "hs1", "")
18-
authedClient := deployment.RegisterUser(t, "hs1", "test_device_management_user", "superuser")
18+
authedClient := deployment.RegisterUser(t, "hs1", "test_device_management_user", "superuser", false)
1919

2020
// sytest: GET /device/{deviceId}
2121
t.Run("GET /device/{deviceId}", func(t *testing.T) {

tests/csapi/apidoc_login_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestLogin(t *testing.T) {
1717
deployment := Deploy(t, b.BlueprintAlice)
1818
defer deployment.Destroy(t)
1919
unauthedClient := deployment.Client(t, "hs1", "")
20-
_ = deployment.RegisterUser(t, "hs1", "test_login_user", "superuser")
20+
_ = deployment.RegisterUser(t, "hs1", "test_login_user", "superuser", false)
2121
t.Run("parallel", func(t *testing.T) {
2222
// sytest: GET /login yields a set of flows
2323
t.Run("GET /login yields a set of flows", func(t *testing.T) {

0 commit comments

Comments
 (0)