多租户更改
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// Package basic is an authenticator by login-password.
|
||||
package basic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Define default constraints on login and password
|
||||
const (
|
||||
defaultMinLoginLength = 2
|
||||
defaultMaxLoginLength = 32
|
||||
|
||||
defaultMinPasswordLength = 3
|
||||
)
|
||||
|
||||
// Token suitable as a login: starts and ends with a Unicode letter (class L) or number (class N),
|
||||
// contains Unicode letters, numbers, dot and underscore.
|
||||
var loginPattern = regexp.MustCompile(`^[\pL\pN][_.\pL\pN]*[\pL\pN]+$`)
|
||||
|
||||
// authenticator is the type to map authentication methods to.
|
||||
type authenticator struct {
|
||||
name string
|
||||
addToTags bool
|
||||
|
||||
minPasswordLength int
|
||||
minLoginLength int
|
||||
}
|
||||
|
||||
func (a *authenticator) checkLoginPolicy(uname string) error {
|
||||
rlogin := []rune(uname)
|
||||
if len(rlogin) < a.minLoginLength || len(rlogin) > defaultMaxLoginLength || !loginPattern.MatchString(uname) {
|
||||
return types.ErrPolicy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *authenticator) checkPasswordPolicy(password string) error {
|
||||
if len([]rune(password)) < a.minPasswordLength {
|
||||
return types.ErrPolicy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSecret(bsecret []byte) (uname, password string, err error) {
|
||||
secret := string(bsecret)
|
||||
|
||||
splitAt := strings.Index(secret, ":")
|
||||
if splitAt < 0 {
|
||||
err = types.ErrMalformed
|
||||
return
|
||||
}
|
||||
|
||||
uname = strings.ToLower(secret[:splitAt])
|
||||
password = secret[splitAt+1:]
|
||||
return
|
||||
}
|
||||
|
||||
// Init initializes the basic authenticator.
|
||||
func (a *authenticator) Init(jsonconf json.RawMessage, name string) error {
|
||||
if name == "" {
|
||||
return errors.New("auth_basic: authenticator name cannot be blank")
|
||||
}
|
||||
|
||||
if a.name != "" {
|
||||
return errors.New("auth_basic: already initialized as " + a.name + "; " + name)
|
||||
}
|
||||
|
||||
type configType struct {
|
||||
// AddToTags indicates that the user name should be used as a searchable tag.
|
||||
AddToTags bool `json:"add_to_tags"`
|
||||
MinPasswordLength int `json:"min_password_length"`
|
||||
MinLoginLength int `json:"min_login_length"`
|
||||
}
|
||||
|
||||
var config configType
|
||||
if err := json.Unmarshal(jsonconf, &config); err != nil {
|
||||
return errors.New("auth_basic: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")")
|
||||
}
|
||||
a.name = name
|
||||
a.addToTags = config.AddToTags
|
||||
a.minPasswordLength = config.MinPasswordLength
|
||||
if a.minPasswordLength <= 0 {
|
||||
a.minPasswordLength = defaultMinPasswordLength
|
||||
}
|
||||
a.minLoginLength = config.MinLoginLength
|
||||
if a.minLoginLength > defaultMaxLoginLength {
|
||||
return errors.New("auth_basic: min_login_length exceeds the limit")
|
||||
}
|
||||
if a.minLoginLength <= 0 {
|
||||
a.minLoginLength = defaultMinLoginLength
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInitialized returns true if the handler is initialized.
|
||||
func (a *authenticator) IsInitialized() bool {
|
||||
return a.name != ""
|
||||
}
|
||||
|
||||
// AddRecord adds a basic authentication record to DB.
|
||||
func (a *authenticator) AddRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) {
|
||||
if err := auth.BindRecord(ctx, rec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uname, password, err := parseSecret(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = a.checkLoginPolicy(uname); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = a.checkPasswordPolicy(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passhash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var expires time.Time
|
||||
if rec.Lifetime > 0 {
|
||||
expires = time.Now().Add(time.Duration(rec.Lifetime)).UTC().Round(time.Millisecond)
|
||||
}
|
||||
|
||||
authLevel := rec.AuthLevel
|
||||
if authLevel == auth.LevelNone {
|
||||
authLevel = auth.LevelAuth
|
||||
}
|
||||
|
||||
err = store.Users.AddAuthRecord(ctx.TenantID, rec.Uid, authLevel, a.name, uname, passhash, expires)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rec.AuthLevel = authLevel
|
||||
if a.addToTags {
|
||||
rec.Tags = append(rec.Tags, a.name+":"+uname)
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// UpdateRecord updates password for basic authentication.
|
||||
func (a *authenticator) UpdateRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) {
|
||||
if err := auth.BindRecord(ctx, rec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uname, password, err := parseSecret(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
login, authLevel, _, _, err := store.Users.GetAuthRecord(ctx.TenantID, rec.Uid, a.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// User does not have a record.
|
||||
if login == "" {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
if uname == "" || uname == login {
|
||||
// User is changing just the password.
|
||||
uname = login
|
||||
} else if err = a.checkLoginPolicy(uname); err != nil {
|
||||
return nil, err
|
||||
} else if uid, _, _, _, err := store.Users.GetAuthUniqueRecord(ctx.TenantID, a.name, uname); err != nil {
|
||||
return nil, err
|
||||
} else if !uid.IsZero() {
|
||||
// The (new) user name already exists. Report an error.
|
||||
return nil, types.ErrDuplicate
|
||||
}
|
||||
|
||||
if err = a.checkPasswordPolicy(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passhash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
var expires time.Time
|
||||
if rec.Lifetime > 0 {
|
||||
expires = types.TimeNow().Add(time.Duration(rec.Lifetime))
|
||||
}
|
||||
err = store.Users.UpdateAuthRecord(ctx.TenantID, rec.Uid, authLevel, a.name, uname, passhash, expires)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove old tag from the list of tags
|
||||
oldTag := a.name + ":" + login
|
||||
for i, tag := range rec.Tags {
|
||||
if tag == oldTag {
|
||||
rec.Tags[i] = rec.Tags[len(rec.Tags)-1]
|
||||
rec.Tags = rec.Tags[:len(rec.Tags)-1]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
// Add new tag
|
||||
rec.Tags = append(rec.Tags, a.name+":"+uname)
|
||||
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// Authenticate checks login and password.
|
||||
func (a *authenticator) Authenticate(ctx auth.AuthContext, secret []byte) (*auth.Rec, []byte, error) {
|
||||
if !ctx.IsValid() {
|
||||
return nil, nil, types.ErrMalformed
|
||||
}
|
||||
uname, password, err := parseSecret(secret)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
uid, authLvl, passhash, expires, err := store.Users.GetAuthUniqueRecord(ctx.TenantID, a.name, uname)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if uid.IsZero() {
|
||||
// Invalid login.
|
||||
return nil, nil, types.ErrFailed
|
||||
}
|
||||
if !expires.IsZero() && expires.Before(time.Now()) {
|
||||
// The record has expired
|
||||
return nil, nil, types.ErrExpired
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(passhash, []byte(password))
|
||||
if err != nil {
|
||||
// Invalid password
|
||||
return nil, nil, types.ErrFailed
|
||||
}
|
||||
|
||||
var lifetime time.Duration
|
||||
if !expires.IsZero() {
|
||||
lifetime = time.Until(expires)
|
||||
}
|
||||
return &auth.Rec{
|
||||
TenantID: ctx.TenantID,
|
||||
Uid: uid,
|
||||
AuthLevel: authLvl,
|
||||
Lifetime: auth.Duration(lifetime),
|
||||
Features: 0,
|
||||
State: types.StateUndefined}, nil, nil
|
||||
}
|
||||
|
||||
// AsTag convert search token into a prefixed tag, if possible.
|
||||
func (a *authenticator) AsTag(token string) string {
|
||||
if !a.addToTags {
|
||||
return ""
|
||||
}
|
||||
|
||||
if err := a.checkLoginPolicy(token); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.name + ":" + token
|
||||
}
|
||||
|
||||
// IsUnique checks login uniqueness and policy compliance.
|
||||
func (a *authenticator) IsUnique(ctx auth.AuthContext, secret []byte) (bool, error) {
|
||||
if !ctx.IsValid() {
|
||||
return false, types.ErrMalformed
|
||||
}
|
||||
uname, _, err := parseSecret(secret)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := a.checkLoginPolicy(uname); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
uid, _, _, _, err := store.Users.GetAuthUniqueRecord(ctx.TenantID, a.name, uname)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if uid.IsZero() {
|
||||
return true, nil
|
||||
}
|
||||
return false, types.ErrDuplicate
|
||||
}
|
||||
|
||||
// GenSecret is not supported, generates an error.
|
||||
func (authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) {
|
||||
return nil, time.Time{}, types.ErrUnsupported
|
||||
}
|
||||
|
||||
// DelRecords deletes saved authentication records of the given user.
|
||||
func (a *authenticator) DelRecords(tenantID types.TenantID, uid types.Uid) error {
|
||||
return store.Users.DelAuthRecords(tenantID, uid, a.name)
|
||||
}
|
||||
|
||||
// RestrictedTags returns tag namespaces (prefixes) restricted by this adapter.
|
||||
func (a *authenticator) RestrictedTags() ([]string, error) {
|
||||
var prefix []string
|
||||
if a.addToTags {
|
||||
prefix = []string{a.name}
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
// GetResetParams returns authenticator parameters passed to password reset handler.
|
||||
func (a *authenticator) GetResetParams(tenantID types.TenantID, uid types.Uid) (map[string]any, error) {
|
||||
login, _, _, _, err := store.Users.GetAuthRecord(tenantID, uid, a.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// User does not have a record matching the authentication scheme.
|
||||
if login == "" {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
params := make(map[string]any)
|
||||
params["login"] = login
|
||||
return params, nil
|
||||
}
|
||||
|
||||
const realName = "basic"
|
||||
|
||||
// GetRealName returns the hardcoded name of the authenticator.
|
||||
func (authenticator) GetRealName() string {
|
||||
return realName
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.RegisterAuthScheme(realName, &authenticator{})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user