From 2f2c18ae84d47f6d568d5e5f9ff7181064c9cf72 Mon Sep 17 00:00:00 2001 From: "Crom (Thibaut CHARLES)" Date: Fri, 11 Oct 2019 20:43:16 +0200 Subject: [PATCH] Guest user registration Signed-off-by: Thibaut CHARLES cromfr@gmail.com --- appservice/appservice.go | 2 +- .../auth/storage/accounts/accounts_table.go | 14 ++-- clientapi/auth/storage/accounts/storage.go | 4 +- clientapi/routing/register.go | 80 ++++++++++++++++--- cmd/create-account/main.go | 2 +- common/config/config.go | 3 + dendrite-config.yaml | 3 + testfile | 17 ++++ 8 files changed, 102 insertions(+), 23 deletions(-) diff --git a/appservice/appservice.go b/appservice/appservice.go index 8703959f8..c4de6d828 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -118,7 +118,7 @@ func generateAppServiceAccount( ctx := context.Background() // Create an account for the application service - acc, err := accountsDB.CreateAccount(ctx, as.SenderLocalpart, "", as.ID) + acc, err := accountsDB.CreateAccount(ctx, false, as.SenderLocalpart, "", as.ID) if err != nil { return err } else if acc == nil { diff --git a/clientapi/auth/storage/accounts/accounts_table.go b/clientapi/auth/storage/accounts/accounts_table.go index e86654eca..3cfcd1a65 100644 --- a/clientapi/auth/storage/accounts/accounts_table.go +++ b/clientapi/auth/storage/accounts/accounts_table.go @@ -36,16 +36,18 @@ CREATE TABLE IF NOT EXISTS account_accounts ( -- The password hash for this account. Can be NULL if this is a passwordless account. password_hash TEXT, -- Identifies which application service this account belongs to, if any. - appservice_id TEXT + appservice_id TEXT, + -- Whether it is a guest account with limited access or not + is_guest BOOL -- TODO: - -- is_guest, is_admin, upgraded_ts, devices, any email reset stuff? + -- is_admin, upgraded_ts, devices, any email reset stuff? ); -- Create sequence for autogenerated numeric usernames CREATE SEQUENCE IF NOT EXISTS numeric_username_seq START 1; ` const insertAccountSQL = "" + - "INSERT INTO account_accounts(localpart, created_ts, password_hash, appservice_id) VALUES ($1, $2, $3, $4)" + "INSERT INTO account_accounts(localpart, created_ts, password_hash, appservice_id, is_guest) VALUES ($1, $2, $3, $4, $5)" const selectAccountByLocalpartSQL = "" + "SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1" @@ -91,16 +93,16 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server // this account will be passwordless. Returns an error if this account already exists. Returns the account // on success. func (s *accountsStatements) insertAccount( - ctx context.Context, localpart, hash, appserviceID string, + ctx context.Context, isGuest bool, localpart, hash, appserviceID string, ) (*authtypes.Account, error) { createdTimeMS := time.Now().UnixNano() / 1000000 stmt := s.insertAccountStmt var err error if appserviceID == "" { - _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, nil) + _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, nil, isGuest) } else { - _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, appserviceID) + _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, appserviceID, isGuest) } if err != nil { return nil, err diff --git a/clientapi/auth/storage/accounts/storage.go b/clientapi/auth/storage/accounts/storage.go index 020a38376..d7dfabc4c 100644 --- a/clientapi/auth/storage/accounts/storage.go +++ b/clientapi/auth/storage/accounts/storage.go @@ -122,7 +122,7 @@ func (d *Database) SetDisplayName( // for this account. If no password is supplied, the account will be a passwordless account. If the // account already exists, it will return nil, nil. func (d *Database) CreateAccount( - ctx context.Context, localpart, plaintextPassword, appserviceID string, + ctx context.Context, isGuest bool, localpart, plaintextPassword, appserviceID string, ) (*authtypes.Account, error) { var err error @@ -140,7 +140,7 @@ func (d *Database) CreateAccount( } return nil, err } - return d.accounts.insertAccount(ctx, localpart, hash, appserviceID) + return d.accounts.insertAccount(ctx, isGuest, localpart, hash, appserviceID) } // SaveMembership saves the user matching a given localpart as a member of a given diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index d0f36a6fd..062d78bf4 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -56,6 +56,12 @@ var ( Help: "Total number of registered users", }, ) + amtGuestUsers = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "dendrite_clientapi_guest_users_total", + Help: "Total number of guest users", + }, + ) ) const ( @@ -68,6 +74,7 @@ const ( func init() { // Register prometheus metrics. They must be registered to be exposed. prometheus.MustRegister(amtRegUsers) + prometheus.MustRegister(amtGuestUsers) } // sessionsDict keeps track of completed auth stages for each session. @@ -443,18 +450,24 @@ func Register( deviceDB *devices.Database, cfg *config.Dendrite, ) util.JSONResponse { - var r registerRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { return *resErr } - // Retrieve or generate the sessionID - sessionID := r.Auth.Session - if sessionID == "" { - // Generate a new, random session ID - sessionID = util.RandomString(sessionIDLength) + var isGuest bool + switch req.URL.Query().Get("kind") { + case "user", "": + isGuest = false + case "guest": + isGuest = true + // Guest registration ignores all JSON body except initial_device_display_name + r = registerRequest{ + InitialDisplayName: r.InitialDisplayName, + } + default: + return util.MessageResponse(http.StatusBadRequest, "kind must be either 'user' or 'guest'") } // Don't allow numeric usernames less than MAX_INT64. @@ -497,12 +510,27 @@ func Register( logger := util.GetLogger(req.Context()) logger.WithFields(log.Fields{ + "guest": isGuest, "username": r.Username, "auth.type": r.Auth.Type, "session_id": r.Auth.Session, }).Info("Processing registration request") - return handleRegistrationFlow(req, r, sessionID, cfg, accountDB, deviceDB) + if isGuest { + // Immediately register a guest account + return handleGuestRegistration(req, r, cfg, accountDB, deviceDB) + } else { + // Start registration flow + + // Retrieve or generate the sessionID + sessionID := r.Auth.Session + if sessionID == "" { + // Generate a new, random session ID + sessionID = util.RandomString(sessionIDLength) + } + + return handleRegistrationFlow(req, r, sessionID, cfg, accountDB, deviceDB) + } } // handleRegistrationFlow will direct and complete registration flow stages @@ -597,6 +625,27 @@ func handleRegistrationFlow( req, r, sessionID, cfg, accountDB, deviceDB) } +// Registers immediately a guest account, if guest registration is enabled on the server +func handleGuestRegistration( + req *http.Request, + r registerRequest, + cfg *config.Dendrite, + accountDB *accounts.Database, + deviceDB *devices.Database, +) util.JSONResponse { + + // Exit if guest registration is forbidden + if cfg.Matrix.GuestAccessDisabled { + return util.MessageResponse(http.StatusForbidden, "Guest access has been disabled") + } + + // Create guest user + return completeRegistration( + req.Context(), accountDB, deviceDB, true, r.Username, r.Password, "", + r.InhibitLogin, r.InitialDisplayName, r.DeviceID, + ) +} + // handleApplicationServiceRegistration handles the registration of an // application service's user by validating the AS from its access token and // registering the user. Its two first parameters must be the two return values @@ -636,7 +685,7 @@ func handleApplicationServiceRegistration( // Don't need to worry about appending to registration stages as // application service registration is entirely separate. return completeRegistration( - req.Context(), accountDB, deviceDB, r.Username, "", appserviceID, + req.Context(), accountDB, deviceDB, false, r.Username, "", appserviceID, r.InhibitLogin, r.InitialDisplayName, r.DeviceID, ) } @@ -656,7 +705,7 @@ func checkAndCompleteFlow( if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) { // This flow was completed, registration can continue return completeRegistration( - req.Context(), accountDB, deviceDB, r.Username, r.Password, "", + req.Context(), accountDB, deviceDB, false, r.Username, r.Password, "", r.InhibitLogin, r.InitialDisplayName, r.DeviceID, ) } @@ -708,10 +757,10 @@ func LegacyRegister( return util.MessageResponse(http.StatusForbidden, "HMAC incorrect") } - return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil, nil) + return completeRegistration(req.Context(), accountDB, deviceDB, false, r.Username, r.Password, "", false, nil, nil) case authtypes.LoginTypeDummy: // there is nothing to do - return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil, nil) + return completeRegistration(req.Context(), accountDB, deviceDB, false, r.Username, r.Password, "", false, nil, nil) default: return util.JSONResponse{ Code: http.StatusNotImplemented, @@ -759,6 +808,7 @@ func completeRegistration( ctx context.Context, accountDB *accounts.Database, deviceDB *devices.Database, + isGuest bool, username, password, appserviceID string, inhibitLogin common.WeakBoolean, displayName, deviceID *string, @@ -777,7 +827,7 @@ func completeRegistration( } } - acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID) + acc, err := accountDB.CreateAccount(ctx, isGuest, username, password, appserviceID) if err != nil { return util.JSONResponse{ Code: http.StatusInternalServerError, @@ -791,7 +841,11 @@ func completeRegistration( } // Increment prometheus counter for created users - amtRegUsers.Inc() + if isGuest { + amtRegUsers.Inc() + } else { + amtGuestUsers.Inc() + } // Check whether inhibit_login option is set. If so, don't create an access // token or a device for this user diff --git a/cmd/create-account/main.go b/cmd/create-account/main.go index fc51a5bb6..46fdb8521 100644 --- a/cmd/create-account/main.go +++ b/cmd/create-account/main.go @@ -69,7 +69,7 @@ func main() { os.Exit(1) } - account, err := accountDB.CreateAccount(context.Background(), *username, *password, "") + account, err := accountDB.CreateAccount(context.Background(), false, *username, *password, "") if err != nil { fmt.Println(err.Error()) os.Exit(1) diff --git a/common/config/config.go b/common/config/config.go index 0332d0358..2db85dc60 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -99,6 +99,9 @@ type Dendrite struct { // If set disables new users from registering (except via shared // secrets) RegistrationDisabled bool `yaml:"registration_disabled"` + // If set disables new users from registering (except via shared + // secrets) + GuestAccessDisabled bool `yaml:"guest_access_disabled"` } `yaml:"matrix"` // The configuration specific to the media repostitory. diff --git a/dendrite-config.yaml b/dendrite-config.yaml index a8d39aa1e..1ff302c60 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -18,6 +18,9 @@ matrix: - vector.im - matrix.org + # Setting this to false will allow guest user to register on the server + guest_access_disabled: true + # The media repository config media: # The base path to where the media files will be stored. May be relative or absolute. diff --git a/testfile b/testfile index 5ad3426e9..389a413d6 100644 --- a/testfile +++ b/testfile @@ -182,3 +182,20 @@ Regular users cannot create room aliases within the AS namespace Deleting a non-existent alias should return a 404 Users can't delete other's aliases Outbound federation can query room alias directory +Guest user cannot call /events globally +Guest users can join guest_access rooms +Guest user can set display names +Guest user cannot upgrade other users +Guest non-joined user cannot call /events on shared room +Guest non-joined user cannot call /events on invited room +Guest non-joined user cannot call /events on joined room +Guest non-joined user cannot call /events on default room +Guest non-joined users can get state for world_readable rooms +Guest non-joined users can get individual state for world_readable rooms +Guest non-joined users cannot room initalSync for non-world_readable rooms +Guest non-joined users can get individual state for world_readable rooms after leaving +Guest non-joined users cannot send messages to guest_access rooms if not joined +Guest users can sync from world_readable guest_access rooms if joined +Guest users can sync from default guest_access rooms if joined +Real non-joined users cannot room initalSync for non-world_readable rooms +Newly joined room is included in an incremental sync after invite \ No newline at end of file