mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
343 lines
7.5 KiB
Go
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
|
|
}
|