From 40830b8a37f5b8fb0404ec3f5712f91bd57dbd2f Mon Sep 17 00:00:00 2001 From: John Terzis Date: Wed, 9 Nov 2022 17:07:51 -0700 Subject: [PATCH] Space,Channel soft deletion with dendrite gating, tests (#889) Closes HNT-244. The following PR implements Space,Channel soft deletion using on-chain `disabled` flag scope to space, channel respectively. On message sync, dendrite will now gate disabled rooms by performing a leave on the user attempting to sync unless the user is the owner (more on this later). To re-join, given rooms (spaces,channels) are created by default using `invite` membership state, the owner will need to undo the on-chain `disabled` flag, setting it false then re-invite users that left the room as a side effect of it becoming disabled previously. The owner does not leave the space, channel because if they did then there would be no one left to invite users let alone themselves back in if the action is ever undone. What is not implemented in this PR: 1. **Transitive leaves on channels in a space** - If a space is disabled, users will leave the space but not the channels within the space. To allow for fully disabling a space and all its' channels, the client can offer a view to the owner that iterates over the channels and space to disable all on-chain. Furthermore, we could implement a batch on-chain method that fully disables all channels within a space (plus the space) in one on-chain call to save the owner gas. 2. **Data deletion** - No data is remove from the DAGs or on-chain. Therefore deletion is soft and reversible. 3. **New hook to check if a room is disabled** - the client can leverage existing on-chain public read only methods `getSpaceInfoBySpaceId`, `getChannelInfoByChannelId` to read the state of each in order to remove spaces, channels from a member's view that are disabled. --- clientapi/jsonerror/jsonerror.go | 9 ++++ clientapi/routing/routing.go | 4 +- .../personalities/syncapi.go | 3 +- setup/monolith.go | 2 +- syncapi/routing/routing.go | 33 +++++++++++-- syncapi/syncapi.go | 3 +- syncapi/syncapi_test.go | 14 ++++-- zion/zion_authorization.go | 48 +++++++++++++++++++ 8 files changed, 102 insertions(+), 14 deletions(-) diff --git a/clientapi/jsonerror/jsonerror.go b/clientapi/jsonerror/jsonerror.go index be7d13a96..4dc3d8603 100644 --- a/clientapi/jsonerror/jsonerror.go +++ b/clientapi/jsonerror/jsonerror.go @@ -171,6 +171,15 @@ func LeaveServerNoticeError() *MatrixError { } } +// ServerDisabledNoticeError is an error returned when trying to sync a server +// that is disabled +func ServerDisabledNoticeError() *MatrixError { + return &MatrixError{ + ErrCode: "Z_UNAUTHORISED_SERVER_DISABLED", + Err: "You cannot remain in a disabled server", + } +} + type IncompatibleRoomVersionError struct { RoomVersion string `json:"room_version"` Error string `json:"error"` diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 4746f73c8..efc4210f5 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -263,13 +263,13 @@ func Setup( return util.ErrorResponse(err) } - isAllowed, _ := authorization.IsAllowed(authz.AuthorizationArgs{ + isAllowed, err := authorization.IsAllowed(authz.AuthorizationArgs{ RoomId: vars["roomIDOrAlias"], UserId: device.UserID, Permission: authz.PermissionRead, }) - if !isAllowed { + if !isAllowed || err != nil { return util.JSONResponse{ Code: http.StatusUnauthorized, JSON: jsonerror.Forbidden("Unauthorised"), diff --git a/cmd/dendrite-polylith-multi/personalities/syncapi.go b/cmd/dendrite-polylith-multi/personalities/syncapi.go index 41637fe1d..1f9cb86e0 100644 --- a/cmd/dendrite-polylith-multi/personalities/syncapi.go +++ b/cmd/dendrite-polylith-multi/personalities/syncapi.go @@ -22,12 +22,13 @@ import ( func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { userAPI := base.UserAPIClient() + base.RoomserverHTTPClient() rsAPI := base.RoomserverHTTPClient() syncapi.AddPublicRoutes( base, - userAPI, rsAPI, + userAPI, rsAPI, rsAPI, base.KeyServerHTTPClient(), ) diff --git a/setup/monolith.go b/setup/monolith.go index 41a897024..18bfa2188 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -69,6 +69,6 @@ func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) { base, m.UserAPI, m.Client, ) syncapi.AddPublicRoutes( - base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, + base, m.UserAPI, m.RoomserverAPI, m.RoomserverAPI, m.KeyAPI, ) } diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go index 558606f93..666ee0b29 100644 --- a/syncapi/routing/routing.go +++ b/syncapi/routing/routing.go @@ -22,6 +22,7 @@ import ( "github.com/matrix-org/util" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/clientapi/routing" "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/fulltext" "github.com/matrix-org/dendrite/internal/httputil" @@ -45,6 +46,7 @@ func Setup( csMux *mux.Router, srp *sync.RequestPool, syncDB storage.Database, userAPI userapi.SyncUserAPI, rsAPI api.SyncRoomserverAPI, + crsAPI api.ClientRoomserverAPI, cfg *config.SyncAPI, clientCfg *config.ClientAPI, lazyLoadCache caching.LazyLoadCache, @@ -63,21 +65,44 @@ func Setup( v3mux.Handle("/rooms/{roomID}/messages", httputil.MakeAuthAPI("room_messages", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) - isAllowed, _ := authorization.IsAllowed(authz.AuthorizationArgs{ + if err != nil { + return util.ErrorResponse(err) + } + + isAllowed, err := authorization.IsAllowed(authz.AuthorizationArgs{ RoomId: vars["roomID"], UserId: device.UserID, Permission: authz.PermissionRead, }) + if err != nil { + switch err { + case zion.ErrSpaceDisabled, zion.ErrChannelDisabled: + // leave space / channel if it is disabled + resp := routing.LeaveRoomByID(req, device, crsAPI, vars["roomID"]) + if resp.Code == 200 { + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.ServerDisabledNoticeError(), + } + } + return resp + default: + // error client if something else is awry + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: jsonerror.Forbidden("Unauthorised"), + } + } + } + if !isAllowed { return util.JSONResponse{ Code: http.StatusUnauthorized, JSON: jsonerror.Forbidden("Unauthorised"), } } - if err != nil { - return util.ErrorResponse(err) - } + return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache) })).Methods(http.MethodGet, http.MethodOptions) diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index 4b07366c5..fb484dcfd 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -42,6 +42,7 @@ func AddPublicRoutes( base *base.BaseDendrite, userAPI userapi.SyncUserAPI, rsAPI api.SyncRoomserverAPI, + crsAPI api.ClientRoomserverAPI, keyAPI keyapi.SyncKeyAPI, ) { cfg := &base.Cfg.SyncAPI @@ -133,6 +134,6 @@ func AddPublicRoutes( routing.Setup( base.PublicClientAPIMux, requestPool, syncDB, userAPI, - rsAPI, cfg, clientCfg, base.Caches, base.Fulltext, + rsAPI, crsAPI, cfg, clientCfg, base.Caches, base.Fulltext, ) } diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go index a4985dbf4..ba01a46e4 100644 --- a/syncapi/syncapi_test.go +++ b/syncapi/syncapi_test.go @@ -32,6 +32,10 @@ type syncRoomserverAPI struct { rooms []*test.Room } +type clientRoomserverAPI struct { + rsapi.ClientRoomserverAPI +} + func (s *syncRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req *rsapi.QueryLatestEventsAndStateRequest, res *rsapi.QueryLatestEventsAndStateResponse) error { var room *test.Room for _, r := range s.rooms { @@ -120,7 +124,7 @@ func testSyncAccessTokens(t *testing.T, dbType test.DBType) { jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) msgs := toNATSMsgs(t, base, room.Events()...) - AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &clientRoomserverAPI{}, &syncKeyAPI{}) testrig.MustPublishMsgs(t, jsctx, msgs...) testCases := []struct { @@ -219,7 +223,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { // m.room.history_visibility msgs := toNATSMsgs(t, base, room.Events()...) sinceTokens := make([]string, len(msgs)) - AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &clientRoomserverAPI{}, &syncKeyAPI{}) for i, msg := range msgs { testrig.MustPublishMsgs(t, jsctx, msg) time.Sleep(100 * time.Millisecond) @@ -303,7 +307,7 @@ func testSyncAPIUpdatePresenceImmediately(t *testing.T, dbType test.DBType) { jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) - AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, &syncKeyAPI{}) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, &clientRoomserverAPI{}, &syncKeyAPI{}) w := httptest.NewRecorder() base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ "access_token": alice.AccessToken, @@ -421,7 +425,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) { rsAPI := roomserver.NewInternalAPI(base) rsAPI.SetFederationAPI(nil, nil) - AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI, &syncKeyAPI{}) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI, rsAPI, &syncKeyAPI{}) for _, tc := range testCases { testname := fmt.Sprintf("%s - %s", tc.historyVisibility, userType) @@ -541,7 +545,7 @@ func testSendToDevice(t *testing.T, dbType test.DBType) { jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) - AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, &syncKeyAPI{}) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, &clientRoomserverAPI{}, &syncKeyAPI{}) producer := producers.SyncAPIProducer{ TopicSendToDeviceEvent: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent), diff --git a/zion/zion_authorization.go b/zion/zion_authorization.go index 155116dd7..01219ebcc 100644 --- a/zion/zion_authorization.go +++ b/zion/zion_authorization.go @@ -2,6 +2,7 @@ package zion import ( _ "embed" + "errors" "github.com/ethereum/go-ethereum/common" "github.com/matrix-org/dendrite/authorization" @@ -18,6 +19,9 @@ var localhostJson []byte //go:embed contracts/zion_goerli/space-manager.json var goerliJson []byte +var ErrSpaceDisabled = errors.New("space disabled") +var ErrChannelDisabled = errors.New("channel disabled") + type ZionAuthorization struct { store StoreAPI spaceManagerLocalhost *zion_localhost.ZionSpaceManagerLocalhost @@ -95,8 +99,22 @@ func (za *ZionAuthorization) IsAllowed(args authorization.AuthorizationArgs) (bo switch za.chainId { case 1337, 31337: + // Check if space / channel is disabled. + disabled, err := za.isSpaceChannelDisabledLocalhost(roomInfo) + if disabled { + return false, ErrSpaceDisabled + } else if err != nil { + return false, err + } return za.isAllowedLocalhost(roomInfo, userIdentifier.AccountAddress, args.Permission) case 5: + // Check if space / channel is disabled. + disabled, err := za.isSpaceChannelDisabledGoerli(roomInfo) + if disabled { + return false, ErrChannelDisabled + } else if err != nil { + return false, err + } return za.isAllowedGoerli(roomInfo, userIdentifier.AccountAddress, args.Permission) default: log.Errorf("Unsupported chain id: %d", userIdentifier.ChainId) @@ -105,6 +123,36 @@ func (za *ZionAuthorization) IsAllowed(args authorization.AuthorizationArgs) (bo return false, nil } +func (za *ZionAuthorization) isSpaceChannelDisabledLocalhost(roomInfo RoomInfo) (bool, error) { + if za.spaceManagerLocalhost == nil { + return false, errors.New("error fetching space manager contract") + } + switch roomInfo.ChannelNetworkId { + case "": + spInfo, err := za.spaceManagerLocalhost.GetSpaceInfoBySpaceId(nil, roomInfo.SpaceNetworkId) + return spInfo.Disabled, err + default: + chInfo, err := za.spaceManagerLocalhost.GetChannelInfoByChannelId(nil, roomInfo.SpaceNetworkId, roomInfo.ChannelNetworkId) + return chInfo.Disabled, err + } + +} + +func (za *ZionAuthorization) isSpaceChannelDisabledGoerli(roomInfo RoomInfo) (bool, error) { + if za.spaceManagerGoerli == nil { + return false, errors.New("error fetching space manager contract") + } + switch roomInfo.ChannelNetworkId { + case "": + spInfo, err := za.spaceManagerGoerli.GetSpaceInfoBySpaceId(nil, roomInfo.SpaceNetworkId) + return spInfo.Disabled, err + default: + chInfo, err := za.spaceManagerGoerli.GetChannelInfoByChannelId(nil, roomInfo.SpaceNetworkId, roomInfo.ChannelNetworkId) + return chInfo.Disabled, err + } + +} + func (za *ZionAuthorization) isAllowedLocalhost( roomInfo RoomInfo, user common.Address,