~emersion/soju

5be25711c7366a79d7bf361a9124e21ca4bd3f6a — delthas 4 months ago f5611ae
Add support for the user create admin service command

This adds support for user create, a new service command only accessible
to admin users. This lets users create other users on the fly and makes
soju start the user routine immediately; unlike sojuctl which currently
requires closing soju, creating the user, and starting soju again.
3 files changed, 74 insertions(+), 6 deletions(-)

M doc/soju.1.scd
M server.go
M service.go
M doc/soju.1.scd => doc/soju.1.scd +9 -6
@@ 138,6 138,12 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
		Connect with the specified nickname. By default, the account's username
		is used.

*network delete* <name>
	Disconnect and delete a network.

*network status*
	Show a list of saved networks and their current status.

*certfp generate* *[options...]* <network name>
	Generate self-signed certificate and use it for authentication.



@@ 159,15 165,12 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
*certfp reset* <network name>
	Disable SASL EXTERNAL authentication and remove stored certificate.

*user create* -username <username> -password <password> [-admin]
    Create a new soju user. Only admin users can create new accounts.

*change-password* <new password>
	Change current user password.

*network delete* <name>
	Disconnect and delete a network.

*network status*
	Show a list of saved networks and their current status.

# AUTHORS

Maintained by Simon Ser <contact@emersion.fr>, who is assisted by other

M server.go => server.go +20 -0
@@ 88,6 88,26 @@ func (s *Server) Run() error {
	select {}
}

func (s *Server) createUser(user *User) (*user, error) {
	s.lock.Lock()
	defer s.lock.Unlock()

	if _, ok := s.users[user.Username]; ok {
		return nil, fmt.Errorf("user %q already exists", user.Username)
	}

	err := s.db.StoreUser(user)
	if err != nil {
		return nil, fmt.Errorf("could not create user in db: %v", err)
	}

	s.Logger.Printf("starting bouncer for new user %q", user.Username)
	u := newUser(s, user)
	s.users[u.Username] = u
	go u.run()
	return u, nil
}

func (s *Server) getUser(name string) *user {
	s.lock.Lock()
	u := s.users[name]

M service.go => service.go +45 -0
@@ 162,6 162,17 @@ func init() {
				},
			},
		},
		"user": {
			children: serviceCommandSet{
				"create": {
					usage:  "-username <username> -password <password> [-admin]",
					desc:   "create a new soju user",
					handle: handleUserCreate,
					admin:  true,
				},
			},
			admin: true,
		},
		"change-password": {
			usage:  "<new password>",
			desc:   "change your password",


@@ 567,3 578,37 @@ func handlePasswordChange(dc *downstreamConn, params []string) error {
	sendServicePRIVMSG(dc, "password updated")
	return nil
}

func handleUserCreate(dc *downstreamConn, params []string) error {
	fs := newFlagSet()
	username := fs.String("username", "", "")
	password := fs.String("password", "", "")
	admin := fs.Bool("admin", false, "")

	if err := fs.Parse(params); err != nil {
		return err
	}
	if *username == "" {
		return fmt.Errorf("flag -username is required")
	}
	if *password == "" {
		return fmt.Errorf("flag -password is required")
	}

	hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %v", err)
	}

	user := &User{
		Username: *username,
		Password: string(hashed),
		Admin:    *admin,
	}
	if _, err := dc.srv.createUser(user); err != nil {
		return fmt.Errorf("could not create user: %v", err)
	}

	sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
	return nil
}