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.
This commit is contained in:
John Terzis 2022-11-09 17:07:51 -07:00 committed by GitHub
parent df41f84bfa
commit 40830b8a37
8 changed files with 102 additions and 14 deletions

View file

@ -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 { type IncompatibleRoomVersionError struct {
RoomVersion string `json:"room_version"` RoomVersion string `json:"room_version"`
Error string `json:"error"` Error string `json:"error"`

View file

@ -263,13 +263,13 @@ func Setup(
return util.ErrorResponse(err) return util.ErrorResponse(err)
} }
isAllowed, _ := authorization.IsAllowed(authz.AuthorizationArgs{ isAllowed, err := authorization.IsAllowed(authz.AuthorizationArgs{
RoomId: vars["roomIDOrAlias"], RoomId: vars["roomIDOrAlias"],
UserId: device.UserID, UserId: device.UserID,
Permission: authz.PermissionRead, Permission: authz.PermissionRead,
}) })
if !isAllowed { if !isAllowed || err != nil {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusUnauthorized, Code: http.StatusUnauthorized,
JSON: jsonerror.Forbidden("Unauthorised"), JSON: jsonerror.Forbidden("Unauthorised"),

View file

@ -22,12 +22,13 @@ import (
func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) {
userAPI := base.UserAPIClient() userAPI := base.UserAPIClient()
base.RoomserverHTTPClient()
rsAPI := base.RoomserverHTTPClient() rsAPI := base.RoomserverHTTPClient()
syncapi.AddPublicRoutes( syncapi.AddPublicRoutes(
base, base,
userAPI, rsAPI, userAPI, rsAPI, rsAPI,
base.KeyServerHTTPClient(), base.KeyServerHTTPClient(),
) )

View file

@ -69,6 +69,6 @@ func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) {
base, m.UserAPI, m.Client, base, m.UserAPI, m.Client,
) )
syncapi.AddPublicRoutes( syncapi.AddPublicRoutes(
base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, base, m.UserAPI, m.RoomserverAPI, m.RoomserverAPI, m.KeyAPI,
) )
} }

View file

@ -22,6 +22,7 @@ import (
"github.com/matrix-org/util" "github.com/matrix-org/util"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "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/caching"
"github.com/matrix-org/dendrite/internal/fulltext" "github.com/matrix-org/dendrite/internal/fulltext"
"github.com/matrix-org/dendrite/internal/httputil" "github.com/matrix-org/dendrite/internal/httputil"
@ -45,6 +46,7 @@ func Setup(
csMux *mux.Router, srp *sync.RequestPool, syncDB storage.Database, csMux *mux.Router, srp *sync.RequestPool, syncDB storage.Database,
userAPI userapi.SyncUserAPI, userAPI userapi.SyncUserAPI,
rsAPI api.SyncRoomserverAPI, rsAPI api.SyncRoomserverAPI,
crsAPI api.ClientRoomserverAPI,
cfg *config.SyncAPI, cfg *config.SyncAPI,
clientCfg *config.ClientAPI, clientCfg *config.ClientAPI,
lazyLoadCache caching.LazyLoadCache, 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 { 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)) 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"], RoomId: vars["roomID"],
UserId: device.UserID, UserId: device.UserID,
Permission: authz.PermissionRead, 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 { if !isAllowed {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusUnauthorized, Code: http.StatusUnauthorized,
JSON: jsonerror.Forbidden("Unauthorised"), JSON: jsonerror.Forbidden("Unauthorised"),
} }
} }
if err != nil {
return util.ErrorResponse(err)
}
return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache) return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache)
})).Methods(http.MethodGet, http.MethodOptions) })).Methods(http.MethodGet, http.MethodOptions)

View file

@ -42,6 +42,7 @@ func AddPublicRoutes(
base *base.BaseDendrite, base *base.BaseDendrite,
userAPI userapi.SyncUserAPI, userAPI userapi.SyncUserAPI,
rsAPI api.SyncRoomserverAPI, rsAPI api.SyncRoomserverAPI,
crsAPI api.ClientRoomserverAPI,
keyAPI keyapi.SyncKeyAPI, keyAPI keyapi.SyncKeyAPI,
) { ) {
cfg := &base.Cfg.SyncAPI cfg := &base.Cfg.SyncAPI
@ -133,6 +134,6 @@ func AddPublicRoutes(
routing.Setup( routing.Setup(
base.PublicClientAPIMux, requestPool, syncDB, userAPI, base.PublicClientAPIMux, requestPool, syncDB, userAPI,
rsAPI, cfg, clientCfg, base.Caches, base.Fulltext, rsAPI, crsAPI, cfg, clientCfg, base.Caches, base.Fulltext,
) )
} }

View file

@ -32,6 +32,10 @@ type syncRoomserverAPI struct {
rooms []*test.Room rooms []*test.Room
} }
type clientRoomserverAPI struct {
rsapi.ClientRoomserverAPI
}
func (s *syncRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req *rsapi.QueryLatestEventsAndStateRequest, res *rsapi.QueryLatestEventsAndStateResponse) error { func (s *syncRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req *rsapi.QueryLatestEventsAndStateRequest, res *rsapi.QueryLatestEventsAndStateResponse) error {
var room *test.Room var room *test.Room
for _, r := range s.rooms { 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) jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
msgs := toNATSMsgs(t, base, room.Events()...) 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...) testrig.MustPublishMsgs(t, jsctx, msgs...)
testCases := []struct { testCases := []struct {
@ -219,7 +223,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) {
// m.room.history_visibility // m.room.history_visibility
msgs := toNATSMsgs(t, base, room.Events()...) msgs := toNATSMsgs(t, base, room.Events()...)
sinceTokens := make([]string, len(msgs)) 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 { for i, msg := range msgs {
testrig.MustPublishMsgs(t, jsctx, msg) testrig.MustPublishMsgs(t, jsctx, msg)
time.Sleep(100 * time.Millisecond) 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) jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
defer jetstream.DeleteAllStreams(jsctx, &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() w := httptest.NewRecorder()
base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
"access_token": alice.AccessToken, "access_token": alice.AccessToken,
@ -421,7 +425,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
rsAPI := roomserver.NewInternalAPI(base) rsAPI := roomserver.NewInternalAPI(base)
rsAPI.SetFederationAPI(nil, nil) 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 { for _, tc := range testCases {
testname := fmt.Sprintf("%s - %s", tc.historyVisibility, userType) 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) jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
defer jetstream.DeleteAllStreams(jsctx, &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{ producer := producers.SyncAPIProducer{
TopicSendToDeviceEvent: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent), TopicSendToDeviceEvent: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),

View file

@ -2,6 +2,7 @@ package zion
import ( import (
_ "embed" _ "embed"
"errors"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/matrix-org/dendrite/authorization" "github.com/matrix-org/dendrite/authorization"
@ -18,6 +19,9 @@ var localhostJson []byte
//go:embed contracts/zion_goerli/space-manager.json //go:embed contracts/zion_goerli/space-manager.json
var goerliJson []byte var goerliJson []byte
var ErrSpaceDisabled = errors.New("space disabled")
var ErrChannelDisabled = errors.New("channel disabled")
type ZionAuthorization struct { type ZionAuthorization struct {
store StoreAPI store StoreAPI
spaceManagerLocalhost *zion_localhost.ZionSpaceManagerLocalhost spaceManagerLocalhost *zion_localhost.ZionSpaceManagerLocalhost
@ -95,8 +99,22 @@ func (za *ZionAuthorization) IsAllowed(args authorization.AuthorizationArgs) (bo
switch za.chainId { switch za.chainId {
case 1337, 31337: 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) return za.isAllowedLocalhost(roomInfo, userIdentifier.AccountAddress, args.Permission)
case 5: 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) return za.isAllowedGoerli(roomInfo, userIdentifier.AccountAddress, args.Permission)
default: default:
log.Errorf("Unsupported chain id: %d", userIdentifier.ChainId) log.Errorf("Unsupported chain id: %d", userIdentifier.ChainId)
@ -105,6 +123,36 @@ func (za *ZionAuthorization) IsAllowed(args authorization.AuthorizationArgs) (bo
return false, nil 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( func (za *ZionAuthorization) isAllowedLocalhost(
roomInfo RoomInfo, roomInfo RoomInfo,
user common.Address, user common.Address,