swirl/security/auth.go
2022-01-06 16:54:14 +08:00

343 lines
7.5 KiB
Go

package security
import (
"context"
"strings"
"time"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/security"
"github.com/cuigh/auxo/security/certify"
"github.com/cuigh/auxo/security/certify/ldap"
"github.com/cuigh/auxo/security/passwd"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/misc"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Identifier identifies the user.
type Identifier struct {
ub biz.UserBiz
rb biz.RoleBiz
sb biz.SessionBiz
realms []RealmFunc
extractors []TokenExtractor
logger log.Logger
}
func NewIdentifier(s *misc.Setting, ub biz.UserBiz, rb biz.RoleBiz, sb biz.SessionBiz) *Identifier {
return &Identifier{
ub: ub,
rb: rb,
sb: sb,
realms: []RealmFunc{internalRealm(), ldapRealm(s, ub)},
extractors: []TokenExtractor{headerExtractor, queryExtractor},
logger: log.Get(PkgName),
}
}
func (c *Identifier) Apply(next web.HandlerFunc) web.HandlerFunc {
return func(ctx web.Context) error {
token := c.extractToken(ctx)
if token != "" {
var user web.User
if len(token) == 24 {
user = c.identifyBySession(token)
} else {
user = c.identifyByToken(token)
}
ctx.SetUser(user)
}
return next(ctx)
}
}
func (c *Identifier) Identify(ctx context.Context, loginName, password string) (identify Identity, err error) {
var (
u security.User
s *dao.Session
)
u, err = c.signIn(ctx, loginName, password)
if err != nil {
return
}
s, err = c.createSession(ctx, u)
if err != nil {
return nil, err
}
return &UserInfo{
id: u.ID(),
name: u.Name(),
token: s.ID,
perms: s.Perms,
}, nil
}
func (c *Identifier) signIn(ctx context.Context, loginName, password string) (user security.User, err error) {
privacy, err := c.ub.FindPrivacy(ctx, loginName)
if err != nil {
return nil, err
}
if privacy != nil && privacy.Status == biz.UserStatusBlocked {
return nil, misc.Error(misc.ErrAccountDisabled, certify.ErrAccountDisabled)
}
for _, login := range c.realms {
user, err = login(ctx, privacy, loginName, password)
if user != nil && err == nil {
return
}
}
if err == nil {
err = misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
}
return
}
func (c *Identifier) extractToken(ctx web.Context) (token string) {
for _, e := range c.extractors {
if token = e(ctx); token != "" {
return
}
}
return
}
func (c *Identifier) identifyBySession(token string) web.User {
ctx, cancel := misc.Context(30 * time.Second)
defer cancel()
session, err := c.sb.Find(ctx, token)
if err != nil {
c.logger.Error("failed to find session: ", err)
return nil
} else if session == nil || session.Expiry.Before(time.Now()) {
return nil
}
if session.Dirty {
if err = c.updateSession(ctx, session); err != nil {
c.logger.Error("failed to refresh session: ", err)
return nil
}
} else if time.Now().Add(time.Minute * 5).After(session.Expiry) {
c.renewSession(ctx, session)
}
return c.createUser(session)
}
func (c *Identifier) identifyByToken(token string) web.User {
ctx, cancel := misc.Context(30 * time.Second)
defer cancel()
u, err := c.ub.FindByToken(ctx, token)
if err != nil {
c.logger.Errorf("failed to find user by token '%s': %s", token, err)
return nil
} else if u == nil {
return nil
}
perms, err := c.rb.GetPerms(ctx, u.Roles)
if err != nil {
c.logger.Error("failed to load perms: ", err)
return nil
}
return &User{
token: token,
id: u.ID,
name: u.Name,
admin: u.Admin,
perm: NewPermMap(perms),
}
}
func (c *Identifier) createUser(s *dao.Session) web.User {
return &User{
token: s.ID,
id: s.UserID,
name: s.Username,
admin: s.Admin,
perm: PermMap(s.Perm),
}
}
func (c *Identifier) createSession(ctx context.Context, user security.User) (s *dao.Session, err error) {
s = &dao.Session{
ID: primitive.NewObjectID().Hex(),
UserID: user.ID(),
Username: user.Name(),
Expiry: time.Now().Add(misc.Options.TokenExpiry),
}
s.MaxExpiry = s.Expiry.Add(24 * time.Hour)
if err = c.fillSession(ctx, s); err == nil {
err = c.sb.Create(ctx, s)
}
return
}
func (c *Identifier) updateSession(ctx context.Context, s *dao.Session) (err error) {
if err = c.fillSession(ctx, s); err == nil {
err = c.sb.Update(ctx, s)
}
return
}
func (c *Identifier) fillSession(ctx context.Context, s *dao.Session) (err error) {
u, err := c.ub.FindByID(ctx, s.UserID)
if err != nil {
return err
} else if u == nil {
return errors.New("user not found")
}
if u.Admin {
s.Perms = []string{"*"}
} else {
s.Perms, err = c.rb.GetPerms(ctx, u.Roles)
if err != nil {
return err
}
s.Perm = uint64(NewPermMap(s.Perms))
}
s.Username = u.Name
s.Admin = u.Admin
s.Roles = u.Roles
return nil
}
func (c *Identifier) renewSession(ctx context.Context, s *dao.Session) {
expiry := time.Now().Add(misc.Options.TokenExpiry)
if expiry.After(s.MaxExpiry) {
expiry = s.MaxExpiry
}
err := c.sb.UpdateExpiry(ctx, s.ID, expiry)
if err != nil {
c.logger.Errorf("failed to renew token '%s': %s", s.ID, err)
}
}
type TokenExtractor func(ctx web.Context) string
func headerExtractor(ctx web.Context) (token string) {
const prefix = "Bearer "
if value := ctx.Header(web.HeaderAuthorization); strings.HasPrefix(value, prefix) {
token = value[len(prefix):]
}
return
}
func queryExtractor(ctx web.Context) (token string) {
return ctx.Query("token")
}
type RealmFunc func(ctx context.Context, u *biz.UserPrivacy, loginName, password string) (security.User, error)
func internalRealm() RealmFunc {
return func(ctx context.Context, u *biz.UserPrivacy, loginName, password string) (security.User, error) {
if u == nil || u.Type != biz.UserTypeInternal {
return nil, nil
}
if passwd.Validate(password, u.Password, u.Salt) {
return security.NewUser(u.ID, u.Name), nil
}
return nil, misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
}
}
func ldapRealm(s *misc.Setting, ub biz.UserBiz) RealmFunc {
const authBind = "bind"
var r certify.Realm
if s.LDAP.Enabled {
opts := []ldap.Option{
ldap.NameAttr(s.LDAP.NameAttr),
ldap.EmailAttr(s.LDAP.EmailAttr),
ldap.UserFilter(s.LDAP.UserFilter),
ldap.Security(ldap.SecurityPolicy(s.LDAP.Security)),
}
if s.LDAP.Authentication == authBind {
opts = append(opts, ldap.Binding(s.LDAP.BindDN, s.LDAP.BindPassword))
}
r = ldap.New(s.LDAP.Address, s.LDAP.BaseDN, s.LDAP.UserDN, opts...)
}
return func(ctx context.Context, u *biz.UserPrivacy, loginName, password string) (security.User, error) {
if r == nil || (u != nil && u.Type != biz.UserTypeLDAP) {
return nil, nil
}
user, err := r.Login(certify.NewSimpleToken(loginName, password))
if err != nil {
return nil, err
}
var (
id string
lu = user.(*ldap.User)
)
if u == nil {
id, err = ub.Create(ctx, &dao.User{
Type: biz.UserTypeLDAP,
LoginName: loginName,
Name: lu.Name(),
Email: lu.Email(),
}, nil)
if err != nil {
return nil, err
}
lu.SetID(id)
} else {
lu.SetID(u.ID)
}
return user, nil
}
}
type Identity interface {
ID() string
Name() string
Anonymous() bool
Token() string
Perms() []string
}
type UserInfo struct {
id string
name string
token string
perms []string
}
func (u *UserInfo) ID() string {
return u.id
}
func (u *UserInfo) Name() string {
return u.name
}
func (u *UserInfo) Anonymous() bool {
return u.id == ""
}
func (u *UserInfo) Token() string {
return u.token
}
func (u *UserInfo) Perms() []string {
return u.perms
}