diff --git a/muxrpc/test/go/deny_test.go b/muxrpc/test/go/deny_test.go index 5c8ca12..c0b9603 100644 --- a/muxrpc/test/go/deny_test.go +++ b/muxrpc/test/go/deny_test.go @@ -84,3 +84,141 @@ func TestConnEstablishmentDeniedKey(t *testing.T) { cancel() } + +func TestConnEstablishmentDenyNonMembersRestrictedRoom(t *testing.T) { + // defer leakcheck.Check(t) + ctx, cancel := context.WithCancel(context.Background()) + theBots := createServerAndBots(t, ctx, 2) + + r := require.New(t) + a := assert.New(t) + + const ( + indexSrv = iota + indexA + indexB + ) + + serv := theBots[indexSrv].srv + botA := theBots[indexA].srv + botB := theBots[indexB].srv + + // make sure we are running in restricted mode on the server + err := serv.Config.SetPrivacyMode(ctx, roomdb.ModeRestricted) + r.NoError(err) + + // allow A, deny B + theBots[indexSrv].srv.Members.Add(ctx, botA.Whoami(), roomdb.RoleMember) + // since we want to verify denying connections for non members in a restricted room, let us: + // a) NOT add B as a member + // theBots[indexSrv].srv.Members.Add(ctx, botB.Whoami(), roomdb.RoleMember) + + // hack: allow bots to dial the server + theBots[indexA].srv.Members.Add(ctx, serv.Whoami(), roomdb.RoleMember) + theBots[indexB].srv.Members.Add(ctx, serv.Whoami(), roomdb.RoleMember) + + // dial up B->A and C->A + // should work (we allowed A) + err = botA.Network.Connect(ctx, serv.Network.GetListenAddr()) + r.NoError(err, "connect A to the Server") + + // shouldn't work (we disallow B in restricted mode) + err = botB.Network.Connect(ctx, serv.Network.GetListenAddr()) + r.NoError(err, "connect B to the Server") // we dont see an error because it just establishes the tcp connection + + t.Log("letting handshaking settle..") + time.Sleep(1 * time.Second) + + var srvWho struct { + ID refs.FeedRef + } + + endpointB, has := botB.Network.GetEndpointFor(serv.Whoami()) + r.False(has, "botB has an endpoint for the server!") + if endpointB != nil { + a.Nil(endpointB, "should not have an endpoint on B (B is not a member, and the server is restricted)") + err = endpointB.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"}) + r.Error(err) + t.Log(srvWho.ID.Ref()) + } + + endpointA, has := botA.Network.GetEndpointFor(serv.Whoami()) + r.True(has, "botA has no endpoint for the server") + + err = endpointA.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"}) + r.NoError(err) + + t.Log("server whoami:", srvWho.ID.Ref()) + a.True(serv.Whoami().Equal(&srvWho.ID)) + + cancel() +} + +func TestConnEstablishmentAllowNonMembersCommunityRoom(t *testing.T) { + // defer leakcheck.Check(t) + ctx, cancel := context.WithCancel(context.Background()) + theBots := createServerAndBots(t, ctx, 2) + + r := require.New(t) + a := assert.New(t) + + const ( + indexSrv = iota + indexA + indexB + ) + + serv := theBots[indexSrv].srv + botA := theBots[indexA].srv + botB := theBots[indexB].srv + + // make sure we are running in community mode on the server + err := serv.Config.SetPrivacyMode(ctx, roomdb.ModeCommunity) + r.NoError(err) + pm, err := serv.Config.GetPrivacyMode(ctx) + r.NoError(err) + r.EqualValues(roomdb.ModeCommunity, pm) + + // allow A, allow B + theBots[indexSrv].srv.Members.Add(ctx, botA.Whoami(), roomdb.RoleMember) + // since we want to verify allowing connections for non members in a community room, let us: + // a) NOT add B as a member + // theBots[indexSrv].srv.Members.Add(ctx, botB.Whoami(), roomdb.RoleMember) + + // hack: allow bots to dial the server + theBots[indexA].srv.Members.Add(ctx, serv.Whoami(), roomdb.RoleMember) + theBots[indexB].srv.Members.Add(ctx, serv.Whoami(), roomdb.RoleMember) + + // dial up B->A and C->A + // should work (we allowed A) + err = botA.Network.Connect(ctx, serv.Network.GetListenAddr()) + r.NoError(err, "connect A to the Server") + + // should work (we don't disallow B in community mode) + err = botB.Network.Connect(ctx, serv.Network.GetListenAddr()) + r.NoError(err, "connect B to the Server") + + t.Log("letting handshaking settle..") + time.Sleep(1 * time.Second) + + var srvWho struct { + ID refs.FeedRef + } + + endpointB, has := botB.Network.GetEndpointFor(serv.Whoami()) + r.True(has, "botB has no endpoint for the server") + err = endpointB.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"}) + r.NoError(err) + t.Log(srvWho.ID.Ref()) + + endpointA, has := botA.Network.GetEndpointFor(serv.Whoami()) + r.True(has, "botA has no endpoint for the server") + + err = endpointA.Async(ctx, &srvWho, muxrpc.TypeJSON, muxrpc.Method{"whoami"}) + r.NoError(err) + + t.Log("server whoami:", srvWho.ID.Ref()) + a.True(serv.Whoami().Equal(&srvWho.ID)) + + cancel() +} diff --git a/roomsrv/init_network.go b/roomsrv/init_network.go index 883db6e..631cb3f 100644 --- a/roomsrv/init_network.go +++ b/roomsrv/init_network.go @@ -35,7 +35,7 @@ func (s *Server) initNetwork() error { // if privacy mode is restricted, deny connections from non-members if pm == roomdb.ModeRestricted { - if _, err := s.authorizer.GetByFeed(s.rootCtx, *remote); err != nil { + if _, err := s.Members.GetByFeed(s.rootCtx, *remote); err != nil { return nil, fmt.Errorf("access restricted to members") } } diff --git a/roomsrv/server.go b/roomsrv/server.go index 6ed2aed..d51817a 100644 --- a/roomsrv/server.go +++ b/roomsrv/server.go @@ -61,8 +61,6 @@ type Server struct { public typemux.HandlerMux master typemux.HandlerMux - authorizer roomdb.MembersService - StateManager *roomstate.Manager Members roomdb.MembersService @@ -89,7 +87,6 @@ func New( opts ...Option, ) (*Server, error) { var s Server - s.authorizer = membersdb s.Members = membersdb s.DeniedKeys = deniedkeysdb diff --git a/web/errors/errhandler.go b/web/errors/errhandler.go index 4918f97..37b0d2d 100644 --- a/web/errors/errhandler.go +++ b/web/errors/errhandler.go @@ -34,7 +34,7 @@ func (eh *ErrorHandler) SetRenderer(r *render.Renderer) { func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code int, err error) { log := logging.FromContext(req.Context()) - level.Error(log).Log("event", "handling error","path", req.URL.Path, "err",err) + level.Error(log).Log("event", "handling error", "path", req.URL.Path, "err", err) var redirectErr ErrRedirect if errors.As(err, &redirectErr) { if redirectErr.Reason != nil { diff --git a/web/handlers/admin/aliases_test.go b/web/handlers/admin/aliases_test.go index 836915a..8bf6888 100644 --- a/web/handlers/admin/aliases_test.go +++ b/web/handlers/admin/aliases_test.go @@ -53,7 +53,13 @@ func TestAliasesRevoke(t *testing.T) { urlRevoke := ts.URLTo(router.AdminAliasesRevoke) overviewURL := ts.URLTo(router.AdminMembersOverview) + aliasEntry := roomdb.Alias{ + ID: ts.User.ID, + Feed: ts.User.PubKey, + Name: "Blobby", + } ts.AliasesDB.RevokeReturns(nil) + ts.AliasesDB.ResolveReturns(aliasEntry, nil) addVals := url.Values{"name": []string{"the-name"}} rec := ts.Client.PostForm(urlRevoke, addVals) diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go index d1e4ae3..ea92822 100644 --- a/web/handlers/admin/invites.go +++ b/web/handlers/admin/invites.go @@ -76,11 +76,11 @@ func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interf case roomdb.ModeOpen: case roomdb.ModeCommunity: if member.Role == roomdb.RoleUnknown { - return nil, fmt.Errorf("warning: member with unknown role tried to create an invite") + return nil, weberrors.ErrNotAuthorized } case roomdb.ModeRestricted: if member.Role == roomdb.RoleMember || member.Role == roomdb.RoleUnknown { - return nil, fmt.Errorf("warning: non-admin/mod user tried to create an invite") + return nil, weberrors.ErrNotAuthorized } } diff --git a/web/handlers/admin/invites_test.go b/web/handlers/admin/invites_test.go index c074060..7b6161f 100644 --- a/web/handlers/admin/invites_test.go +++ b/web/handlers/admin/invites_test.go @@ -2,6 +2,7 @@ package admin import ( "net/http" + "net/http/httptest" "net/url" "testing" @@ -143,17 +144,28 @@ func TestInvitesCreate(t *testing.T) { a := assert.New(t) r := require.New(t) - urlRemove := ts.URLTo(router.AdminInvitesCreate) + urlCreate := ts.URLTo(router.AdminInvitesCreate) testInvite := "your-fake-test-invite" ts.InvitesDB.CreateReturns(testInvite, nil) - rec := ts.Client.PostForm(urlRemove, url.Values{}) - a.Equal(http.StatusOK, rec.Code) + totalCreateCallCount := 0 + createInviteShouldWork := func(works bool) *httptest.ResponseRecorder { + rec := ts.Client.PostForm(urlCreate, url.Values{}) + if works { + totalCreateCallCount += 1 + a.Equal(http.StatusOK, rec.Code) + r.Equal(totalCreateCallCount, ts.InvitesDB.CreateCallCount()) + _, userID := ts.InvitesDB.CreateArgsForCall(totalCreateCallCount - 1) + a.EqualValues(ts.User.ID, userID) + } else { + a.Equal(http.StatusForbidden, rec.Code) + r.Equal(totalCreateCallCount, ts.InvitesDB.CreateCallCount()) + } + return rec + } - r.Equal(1, ts.InvitesDB.CreateCallCount(), "expected one invites.Create call") - _, userID := ts.InvitesDB.CreateArgsForCall(0) - a.EqualValues(ts.User.ID, userID) + rec := createInviteShouldWork(true) doc, err := goquery.NewDocumentFromReader(rec.Body) require.NoError(t, err, "failed to parse response") @@ -167,4 +179,34 @@ func TestInvitesCreate(t *testing.T) { shownLink := doc.Find("#invite-facade-link").Text() a.Equal(wantURL.String(), shownLink) + + memberUser := roomdb.Member{ + ID: 7331, + Role: roomdb.RoleMember, + PubKey: generatePubKey(), + } + modUser := roomdb.Member{ + ID: 9001, + Role: roomdb.RoleModerator, + PubKey: generatePubKey(), + } + adminUser := roomdb.Member{ + ID: 1337, + Role: roomdb.RoleAdmin, + PubKey: generatePubKey(), + } + + /* test invite creation under various restricted mode with the roles member, mod, admin */ + modes := []roomdb.PrivacyMode{roomdb.ModeRestricted, roomdb.ModeCommunity} + for _, mode := range modes { + ts.ConfigDB.GetPrivacyModeReturns(mode, nil) + ts.User = memberUser + // members can only invite in community rooms + createInviteShouldWork(mode == roomdb.ModeCommunity) + // mods & admins can always invite + ts.User = modUser + createInviteShouldWork(true) + ts.User = adminUser + createInviteShouldWork(true) + } } diff --git a/web/handlers/admin/notices_test.go b/web/handlers/admin/notices_test.go index 724fb8c..1ca75a1 100644 --- a/web/handlers/admin/notices_test.go +++ b/web/handlers/admin/notices_test.go @@ -89,7 +89,7 @@ func TestNoticeAddLanguageOnlyAllowsPost(t *testing.T) { // verify that a GET request is no bueno u := ts.URLTo(router.AdminNoticeAddTranslation, "name", roomdb.NoticeNews.String()) _, resp := ts.Client.GetHTML(u) - a.Equal(http.StatusMethodNotAllowed, resp.Code, "GET should not be allowed for this route") + a.Equal(http.StatusBadRequest, resp.Code, "GET should not be allowed for this route") // next up, we verify that a correct POST request actually works: id := []string{"1"} diff --git a/web/handlers/admin/setup_test.go b/web/handlers/admin/setup_test.go index 3f12498..e95d41f 100644 --- a/web/handlers/admin/setup_test.go +++ b/web/handlers/admin/setup_test.go @@ -56,6 +56,14 @@ type testSession struct { RoomState *roomstate.Manager } +var pubKeyCount byte + +func generatePubKey() refs.FeedRef { + pk := refs.FeedRef{Algo: "ed25519", ID: bytes.Repeat([]byte{pubKeyCount}, 32)} + pubKeyCount++ + return pk +} + func newSession(t *testing.T) *testSession { var ts testSession @@ -76,7 +84,7 @@ func newSession(t *testing.T) *testSession { ts.netInfo = network.ServerEndpointDetails{ Domain: randutil.String(10), - RoomID: refs.FeedRef{Algo: "ed25519", ID: bytes.Repeat([]byte{0}, 32)}, + RoomID: generatePubKey(), UseSubdomainForAliases: true, } @@ -97,8 +105,9 @@ func newSession(t *testing.T) *testSession { // fake user ts.User = roomdb.Member{ - ID: 1234, - Role: roomdb.RoleModerator, + ID: 1234, + Role: roomdb.RoleModerator, + PubKey: generatePubKey(), } testPath := filepath.Join("testrun", t.Name()) @@ -121,10 +130,10 @@ func newSession(t *testing.T) *testSession { os.MkdirAll(sessionsPath, 0700) fsStore := sessions.NewFilesystemStore(sessionsPath, authKey, encKey) + // setup rendering flashHelper := weberrs.NewFlashHelper(fsStore, locHelper) - // setup rendering - + // template funcs // TODO: make testing utils and move these there testFuncs := web.TemplateFuncs(router, ts.netInfo) testFuncs["current_page_is"] = func(routeName string) bool { return true } @@ -143,11 +152,13 @@ func newSession(t *testing.T) *testSession { testFuncs["list_languages"] = func(*url.URL, string) string { return "" } testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) } + eh := weberrs.NewErrorHandler(locHelper, flashHelper) + renderOpts := []render.Option{ render.SetLogger(log), render.BaseTemplates("base.tmpl", "menu.tmpl", "flashes.tmpl"), render.AddTemplates(append(HTMLTemplates, "error.tmpl")...), - render.ErrorTemplate("error.tmpl"), + render.SetErrorHandler(eh.Handle), render.FuncMap(testFuncs), } renderOpts = append(renderOpts, locHelper.GetRenderFuncs()...) @@ -157,6 +168,8 @@ func newSession(t *testing.T) *testSession { t.Fatal(errors.Wrap(err, "setup: render init failed")) } + eh.SetRenderer(r) + handler := Handler( ts.netInfo, r, @@ -174,7 +187,7 @@ func newSession(t *testing.T) *testSession { }, ) - handler = members.MiddlewareForTests(ts.User)(handler) + handler = members.MiddlewareForTests(&ts.User)(handler) ts.Mux = http.NewServeMux() ts.Mux.Handle("/", handler) diff --git a/web/handlers/aliases_test.go b/web/handlers/aliases_test.go index 78b947f..c95b814 100644 --- a/web/handlers/aliases_test.go +++ b/web/handlers/aliases_test.go @@ -88,4 +88,11 @@ func TestAliasResolve(t *testing.T) { a.Equal(testAlias.Feed.Ref(), ar.UserID, "wrong user feed on response") a.Equal(ts.NetworkInfo.RoomID.Ref(), ar.RoomID, "wrong room feed on response") a.Equal(ts.NetworkInfo.MultiserverAddress(), ar.MultiserverAddress) + + /* alias resolving should not work for restricted rooms */ + ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeRestricted, nil) + htmlURL, err = routes.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name) + r.NoError(err) + html, resp = ts.Client.GetHTML(htmlURL) + a.Equal(http.StatusInternalServerError, resp.Code) } diff --git a/web/members/testing.go b/web/members/testing.go index 6f31f1b..2ef5ef9 100644 --- a/web/members/testing.go +++ b/web/members/testing.go @@ -13,10 +13,10 @@ import ( // This is part of testing.go because we need to use roomMemberContextKey, which shouldn't be exported either. // TODO: could be protected with an extra build tag. // (Sadly +build test does not exist https://github.com/golang/go/issues/21360 ) -func MiddlewareForTests(m roomdb.Member) func(http.Handler) http.Handler { +func MiddlewareForTests(m *roomdb.Member) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx := context.WithValue(req.Context(), roomMemberContextKey, &m) + ctx := context.WithValue(req.Context(), roomMemberContextKey, m) next.ServeHTTP(w, req.WithContext(ctx)) }) }