~shulhan/karajo

de4d09f1b644fc9880f41e8e59a68b2bf8bde3d6 — Shulhan 7 months ago 6129e3d
all: implement user for authentication with web user interface

The list of user can be loaded from file, with the following format

  [user "$name"]
  password = $bcrypt_hash

The password is set as hash of plain text using bcrypt (version $2a$).
5 files changed, 121 insertions(+), 0 deletions(-)

M go.mod
M go.sum
A testdata/etc/karajo/user.conf
A user.go
A user_test.go
M go.mod => go.mod +1 -0
@@ 5,6 5,7 @@ go 1.19
require (
	git.sr.ht/~shulhan/ciigo v0.9.3
	github.com/shuLhan/share v0.46.0
	golang.org/x/crypto v0.6.0
)

require (

M go.sum => go.sum +2 -0
@@ 4,6 4,8 @@ git.sr.ht/~shulhan/ciigo v0.9.3 h1:q6EqGVvIU8ymkPqBS4HEyHIhbfVhJn6urwvGDg83TAY=
git.sr.ht/~shulhan/ciigo v0.9.3/go.mod h1:SsRnbqnBo+9jWDDZD/uc9IkdgGpBfp39vV0JPXNro9c=
github.com/shuLhan/share v0.46.0 h1:cF0Ngj7wVA6TIcdSmfrqxOwMB3hZ+4df5cJf4GGCun4=
github.com/shuLhan/share v0.46.0/go.mod h1:BhnIWJxq84BTOs3Z2gLFAN8ih9mBfhZbRIjqGupGJag=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=

A testdata/etc/karajo/user.conf => testdata/etc/karajo/user.conf +2 -0
@@ 0,0 1,2 @@
[user "test"]
password = $2a$10$9XMRfqpnzY2421fwYm5dd.CidJf7dHHWIESeeNGXuajHRf.Lqzy7a

A user.go => user.go +70 -0
@@ 0,0 1,70 @@
// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info>
// SPDX-License-Identifier: GPL-3.0-or-later

package karajo

import (
	"fmt"
	"os"

	"github.com/shuLhan/share/lib/ini"
	"golang.org/x/crypto/bcrypt"
)

// User represent the account that can access Karajo user interface using
// name and password.
// The Password field store the bcrypt hash of plain password.
type User struct {
	Name     string
	Password string `ini:"::password"`
}

// loadUsers load user from file, return the map with user's name as key.
// If the file is not exist it will return empty users without an error.
func loadUsers(file string) (users map[string]*User, err error) {
	type container struct {
		Users map[string]*User `ini:"user"`
	}

	var (
		logp    = `loadUsers`
		content []byte
	)

	content, err = os.ReadFile(file)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf(`%s: %w`, logp, err)
	}

	var cont = container{
		Users: make(map[string]*User),
	}

	err = ini.Unmarshal(content, &cont)
	if err != nil {
		return nil, fmt.Errorf(`%s: %w`, logp, err)
	}

	users = cont.Users
	cont.Users = nil

	var (
		name string
		u    *User
	)
	for name, u = range users {
		u.Name = name
	}

	return users, nil
}

// authenticate return true if the hash of plain password match with user's
// Password.
func (u *User) authenticate(plain string) bool {
	var err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(plain))
	return err == nil
}

A user_test.go => user_test.go +46 -0
@@ 0,0 1,46 @@
// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info>
// SPDX-License-Identifier: GPL-3.0-or-later

package karajo

import (
	"testing"

	"github.com/shuLhan/share/lib/test"
)

func TestLoadUsers(t *testing.T) {
	var (
		expUsers = map[string]*User{
			`test`: &User{
				Name:     `test`,
				Password: `$2a$10$9XMRfqpnzY2421fwYm5dd.CidJf7dHHWIESeeNGXuajHRf.Lqzy7a`,
			},
		}
		gotUsers map[string]*User
		err      error
	)

	gotUsers, err = loadUsers(`testdata/etc/karajo/user.conf`)
	if err != nil {
		t.Fatal(err)
	}

	test.Assert(t, `loadUsers`, expUsers, gotUsers)
}

func TestUser_authenticate(t *testing.T) {
	var (
		u = &User{
			Name:     `test`,
			Password: `$2a$10$9XMRfqpnzY2421fwYm5dd.CidJf7dHHWIESeeNGXuajHRf.Lqzy7a`,
		}
		got bool
	)

	got = u.authenticate(`s3cr3t`)
	test.Assert(t, `authenticate: invalid`, false, got)

	got = u.authenticate(`s3cret`)
	test.Assert(t, `authenticate: valid`, true, got)
}