Guest user registration

Signed-off-by: Thibaut CHARLES cromfr@gmail.com
This commit is contained in:
Crom (Thibaut CHARLES) 2019-10-11 20:43:16 +02:00
parent 145921f207
commit 2f2c18ae84
No known key found for this signature in database
GPG key ID: 45A3D5F880B9E6D0
8 changed files with 102 additions and 23 deletions

View file

@ -118,7 +118,7 @@ func generateAppServiceAccount(
ctx := context.Background() ctx := context.Background()
// Create an account for the application service // 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 { if err != nil {
return err return err
} else if acc == nil { } else if acc == nil {

View file

@ -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. -- The password hash for this account. Can be NULL if this is a passwordless account.
password_hash TEXT, password_hash TEXT,
-- Identifies which application service this account belongs to, if any. -- 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: -- 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 for autogenerated numeric usernames
CREATE SEQUENCE IF NOT EXISTS numeric_username_seq START 1; CREATE SEQUENCE IF NOT EXISTS numeric_username_seq START 1;
` `
const insertAccountSQL = "" + 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 = "" + const selectAccountByLocalpartSQL = "" +
"SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1" "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 // this account will be passwordless. Returns an error if this account already exists. Returns the account
// on success. // on success.
func (s *accountsStatements) insertAccount( func (s *accountsStatements) insertAccount(
ctx context.Context, localpart, hash, appserviceID string, ctx context.Context, isGuest bool, localpart, hash, appserviceID string,
) (*authtypes.Account, error) { ) (*authtypes.Account, error) {
createdTimeMS := time.Now().UnixNano() / 1000000 createdTimeMS := time.Now().UnixNano() / 1000000
stmt := s.insertAccountStmt stmt := s.insertAccountStmt
var err error var err error
if appserviceID == "" { if appserviceID == "" {
_, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, nil) _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, nil, isGuest)
} else { } else {
_, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, appserviceID) _, err = stmt.ExecContext(ctx, localpart, createdTimeMS, hash, appserviceID, isGuest)
} }
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -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 // 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. // account already exists, it will return nil, nil.
func (d *Database) CreateAccount( func (d *Database) CreateAccount(
ctx context.Context, localpart, plaintextPassword, appserviceID string, ctx context.Context, isGuest bool, localpart, plaintextPassword, appserviceID string,
) (*authtypes.Account, error) { ) (*authtypes.Account, error) {
var err error var err error
@ -140,7 +140,7 @@ func (d *Database) CreateAccount(
} }
return nil, err 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 // SaveMembership saves the user matching a given localpart as a member of a given

View file

@ -56,6 +56,12 @@ var (
Help: "Total number of registered users", Help: "Total number of registered users",
}, },
) )
amtGuestUsers = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "dendrite_clientapi_guest_users_total",
Help: "Total number of guest users",
},
)
) )
const ( const (
@ -68,6 +74,7 @@ const (
func init() { func init() {
// Register prometheus metrics. They must be registered to be exposed. // Register prometheus metrics. They must be registered to be exposed.
prometheus.MustRegister(amtRegUsers) prometheus.MustRegister(amtRegUsers)
prometheus.MustRegister(amtGuestUsers)
} }
// sessionsDict keeps track of completed auth stages for each session. // sessionsDict keeps track of completed auth stages for each session.
@ -443,18 +450,24 @@ func Register(
deviceDB *devices.Database, deviceDB *devices.Database,
cfg *config.Dendrite, cfg *config.Dendrite,
) util.JSONResponse { ) util.JSONResponse {
var r registerRequest var r registerRequest
resErr := httputil.UnmarshalJSONRequest(req, &r) resErr := httputil.UnmarshalJSONRequest(req, &r)
if resErr != nil { if resErr != nil {
return *resErr return *resErr
} }
// Retrieve or generate the sessionID var isGuest bool
sessionID := r.Auth.Session switch req.URL.Query().Get("kind") {
if sessionID == "" { case "user", "":
// Generate a new, random session ID isGuest = false
sessionID = util.RandomString(sessionIDLength) 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. // Don't allow numeric usernames less than MAX_INT64.
@ -497,12 +510,27 @@ func Register(
logger := util.GetLogger(req.Context()) logger := util.GetLogger(req.Context())
logger.WithFields(log.Fields{ logger.WithFields(log.Fields{
"guest": isGuest,
"username": r.Username, "username": r.Username,
"auth.type": r.Auth.Type, "auth.type": r.Auth.Type,
"session_id": r.Auth.Session, "session_id": r.Auth.Session,
}).Info("Processing registration request") }).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 // handleRegistrationFlow will direct and complete registration flow stages
@ -597,6 +625,27 @@ func handleRegistrationFlow(
req, r, sessionID, cfg, accountDB, deviceDB) 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 // handleApplicationServiceRegistration handles the registration of an
// application service's user by validating the AS from its access token and // 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 // 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 // Don't need to worry about appending to registration stages as
// application service registration is entirely separate. // application service registration is entirely separate.
return completeRegistration( return completeRegistration(
req.Context(), accountDB, deviceDB, r.Username, "", appserviceID, req.Context(), accountDB, deviceDB, false, r.Username, "", appserviceID,
r.InhibitLogin, r.InitialDisplayName, r.DeviceID, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
) )
} }
@ -656,7 +705,7 @@ func checkAndCompleteFlow(
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) { if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
// This flow was completed, registration can continue // This flow was completed, registration can continue
return completeRegistration( 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, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
) )
} }
@ -708,10 +757,10 @@ func LegacyRegister(
return util.MessageResponse(http.StatusForbidden, "HMAC incorrect") 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: case authtypes.LoginTypeDummy:
// there is nothing to do // 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: default:
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusNotImplemented, Code: http.StatusNotImplemented,
@ -759,6 +808,7 @@ func completeRegistration(
ctx context.Context, ctx context.Context,
accountDB *accounts.Database, accountDB *accounts.Database,
deviceDB *devices.Database, deviceDB *devices.Database,
isGuest bool,
username, password, appserviceID string, username, password, appserviceID string,
inhibitLogin common.WeakBoolean, inhibitLogin common.WeakBoolean,
displayName, deviceID *string, 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 { if err != nil {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusInternalServerError, Code: http.StatusInternalServerError,
@ -791,7 +841,11 @@ func completeRegistration(
} }
// Increment prometheus counter for created users // 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 // Check whether inhibit_login option is set. If so, don't create an access
// token or a device for this user // token or a device for this user

View file

@ -69,7 +69,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
account, err := accountDB.CreateAccount(context.Background(), *username, *password, "") account, err := accountDB.CreateAccount(context.Background(), false, *username, *password, "")
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)

View file

@ -99,6 +99,9 @@ type Dendrite struct {
// If set disables new users from registering (except via shared // If set disables new users from registering (except via shared
// secrets) // secrets)
RegistrationDisabled bool `yaml:"registration_disabled"` RegistrationDisabled bool `yaml:"registration_disabled"`
// If set disables new users from registering (except via shared
// secrets)
GuestAccessDisabled bool `yaml:"guest_access_disabled"`
} `yaml:"matrix"` } `yaml:"matrix"`
// The configuration specific to the media repostitory. // The configuration specific to the media repostitory.

View file

@ -18,6 +18,9 @@ matrix:
- vector.im - vector.im
- matrix.org - matrix.org
# Setting this to false will allow guest user to register on the server
guest_access_disabled: true
# The media repository config # The media repository config
media: media:
# The base path to where the media files will be stored. May be relative or absolute. # The base path to where the media files will be stored. May be relative or absolute.

View file

@ -182,3 +182,20 @@ Regular users cannot create room aliases within the AS namespace
Deleting a non-existent alias should return a 404 Deleting a non-existent alias should return a 404
Users can't delete other's aliases Users can't delete other's aliases
Outbound federation can query room alias directory 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