* add rdb config auth.debug for white_list

* update prober config support mode param

* feature: support access-token control with max connection, idle time, ...

* add token/session delete with auth check

* enable debug user for auth

* skip init sso db if not enable
This commit is contained in:
yubo 2021-01-07 09:13:04 +08:00 committed by GitHub
parent fb1354898c
commit 543d345aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 633 additions and 242 deletions

View File

@ -1,3 +1,4 @@
mode: whitelist # whitelist(default),all
metrics:
- name: mysql_queries
type: COUNTER

View File

@ -1 +1,5 @@
set names utf8;
use n9e_rdb;
alter table session add `access_token` char(128) default '' after sid;
alter table session add key (`access_token`);

View File

@ -344,12 +344,14 @@ CREATE TABLE `white_list` (
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE `session` (
`sid` char(128) NOT NULL,
`username` varchar(64) DEFAULT '',
`remote_addr` varchar(32) DEFAULT '',
`created_at` integer unsigned DEFAULT '0',
`updated_at` integer unsigned DEFAULT '0' NOT NULL,
`sid` char(128) NOT NULL,
`access_token` char(128) DEFAULT '',
`username` varchar(64) DEFAULT '',
`remote_addr` varchar(32) DEFAULT '',
`created_at` integer unsigned DEFAULT '0',
`updated_at` integer unsigned DEFAULT '0' NOT NULL,
PRIMARY KEY (`sid`),
KEY (`access_token`),
KEY (`username`),
KEY (`updated_at`)
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8;

View File

@ -9,11 +9,12 @@ import (
)
type Session struct {
Sid string `json:"sid"`
Username string `json:"username"`
RemoteAddr string `json:"remote_addr"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Sid string `json:"sid"`
AccessToken string `json:"-"`
Username string `json:"username"`
RemoteAddr string `json:"remote_addr"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
func SessionAll() (int64, error) {
@ -37,12 +38,12 @@ func SessionGet(sid string) (*Session, error) {
return &obj, nil
}
func SessionInsert(in *Session) error {
_, err := DB["rdb"].Insert(in)
func (s *Session) Save() error {
_, err := DB["rdb"].Insert(s)
return err
}
func SessionDel(sid string) error {
func SessionDelete(sid string) error {
_, err := DB["rdb"].Where("sid=?", sid).Delete(new(Session))
return err
}
@ -52,11 +53,6 @@ func SessionUpdate(in *Session) error {
return err
}
func SessionCleanup(ts int64) error {
n, err := DB["rdb"].Where("updated_at<?", ts).Delete(new(Session))
logger.Debugf("delete before updated_at %d session %d", ts, n)
return err
}
func SessionCleanupByCreatedAt(ts int64) error {
n, err := DB["rdb"].Where("created_at<?", ts).Delete(new(Session))
logger.Debugf("delete before created_at %d session %d", ts, n)
@ -67,7 +63,20 @@ func (s *Session) Update(cols ...string) error {
return err
}
// SessionGetWithCache will update session.UpdatedAt
func SessionGetByToken(token string) (*Session, error) {
var obj Session
has, err := DB["rdb"].Where("access_token=?", token).Get(&obj)
if err != nil {
return nil, fmt.Errorf("get session err %s", err)
}
if !has {
return nil, fmt.Errorf("not found")
}
return &obj, nil
}
// SessionGetWithCache will update session.UpdatedAt && token.LastAt
func SessionGetWithCache(sid string) (*Session, error) {
if sid == "" {
return nil, fmt.Errorf("unable to get sid")
@ -88,25 +97,8 @@ func SessionGetWithCache(sid string) (*Session, error) {
sess.Update("updated_at")
if sess.Username != "" {
cache.Set("sid."+sid, sess, time.Second*30)
cache.Set("sid."+sid, sess, time.Second*10)
}
return sess, nil
}
func SessionGetUserWithCache(sid string) (*User, error) {
s, err := SessionGetWithCache(sid)
if err != nil {
return nil, err
}
if s.Username == "" {
return nil, fmt.Errorf("user not found")
}
return UserMustGet("username=?", s.Username)
}
func SessionCacheDelete(sid string) {
cache.Delete("sid." + sid)
}

104
src/models/sso_token.go Normal file
View File

@ -0,0 +1,104 @@
package models
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/toolkits/pkg/cache"
)
type Token struct {
Id int64 `json:"id,omitempty"`
Name string `json:"name,omitempty" description:"access token name"`
AccessToken string `json:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken,omitempty"`
ClientId string `json:"clientId,omitempty"`
Authorize string `json:"authorize,omitempty"`
Previous string `json:"previous,omitempty"`
ExpiresIn int64 `json:"expiresIn,omitempty" description:"max 3 year, default:0, max time"`
Scope string `json:"scope,omitempty" description:"scope split by ' '"`
RedirectUri string `json:"redirectUri,omitempty"`
UserName string `json:"userName,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty" out:",date"`
LastAt int64 `json:"lastAt,omitempty" out:",date"`
}
func TokenAll() (int64, error) {
return DB["sso"].Count(new(Token))
}
func TokenGet(token string) (*Token, error) {
var obj Token
has, err := DB["sso"].Where("access_token=?", token).Get(&obj)
if err != nil {
return nil, fmt.Errorf("get token err %s", err)
}
if !has {
return nil, fmt.Errorf("not found")
}
return &obj, nil
}
func (p *Token) Session() *Session {
now := time.Now().Unix()
return &Session{
Sid: uuid.New().String(),
AccessToken: p.AccessToken,
Username: p.UserName,
RemoteAddr: "",
CreatedAt: now,
UpdatedAt: now,
}
}
func (p *Token) Update(cols ...string) error {
_, err := DB["sso"].Where("access_token=?", p.AccessToken).Cols(cols...).Update(p)
return err
}
func TokenDelete(token string) error {
_, err := DB["sso"].Where("access_token=?", token).Delete(new(Token))
return err
}
func TokenGets(where string, args ...interface{}) (tokens []Token, err error) {
if where != "" {
err = DB["sso"].Where(where, args...).Find(&tokens)
} else {
err = DB["sso"].Find(&tokens)
}
return
}
// TokenGetWithCache will update token.LastAt
func TokenGetWithCache(accessToken string) (*Session, error) {
if accessToken == "" {
return nil, fmt.Errorf("unable to get token")
}
sess := &Session{}
if err := cache.Get("access-token."+accessToken, &sess); err == nil {
return sess, nil
}
var err error
if sess, err = SessionGetByToken(accessToken); err != nil {
// try to get token from sso
if t, err := TokenGet(accessToken); err != nil {
return nil, fmt.Errorf("token not found")
} else {
sess = t.Session()
sess.Save()
}
}
// update session
sess.UpdatedAt = time.Now().Unix()
sess.Update("updated_at")
cache.Set("access-token."+accessToken, sess, time.Second*10)
return sess, nil
}

View File

@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"time"
"github.com/toolkits/pkg/logger"
)
type WhiteList struct {
@ -25,8 +27,9 @@ func WhiteListAccess(addr string) error {
if ip == 0 {
return fmt.Errorf("invalid remote address %s", addr)
}
logger.Debugf("WhiteListAccess htol(%s) %d", addr, ip)
now := time.Now().Unix()
count, _ := DB["rdb"].Where("start_ip_int<? and end_ip_int>? and start_time>? and end_time<?", ip, ip, now, now).Count(new(WhiteList))
count, _ := DB["rdb"].Where("start_ip_int<=? and end_ip_int>=? and start_time<=? and end_time>=?", ip, ip, now, now).Count(new(WhiteList))
if count == 0 {
return fmt.Errorf("access deny from %s", addr)
}

View File

@ -21,7 +21,7 @@ type Field struct {
Description string `json:"description,omitempty"`
Required bool `json:"required,omitempty"`
Items *Field `json:"items,omitempty" description:"arrays's items"`
Type string `json:"type,omitempty" description:"struct,boolean,integer,folat,string,array"`
Type string `json:"type,omitempty" description:"boolean,integer,folat,string,array"`
Ref string `json:"$ref,omitempty" description:"name of the struct ref"`
Fields []Field `json:"fields,omitempty" description:"fields of struct type"`
Definitions map[string][]Field `json:"definitions,omitempty"`

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/didi/nightingale/src/modules/monapi/collector"
"github.com/didi/nightingale/src/modules/prober/config"
@ -22,23 +23,47 @@ type MetricConfig struct {
}
type PluginConfig struct {
Metrics []MetricConfig `metrics`
Metrics []*MetricConfig `yaml:"metrics"`
Mode string `yaml:"mode"`
mode int `yaml:"-"`
}
type CachePluginConfig struct {
Name string
Mode int
Metrics map[string]*MetricConfig
}
const (
PluginModeWhitelist = iota
PluginModeOverlay
)
func (p *PluginConfig) Validate() error {
switch strings.ToLower(p.Mode) {
case "whitelist":
p.mode = PluginModeWhitelist
case "overlay":
p.mode = PluginModeOverlay
default:
p.mode = PluginModeWhitelist
}
return nil
}
var (
metricsConfig map[string]MetricConfig
metricsExpr map[string]map[string]MetricConfig
ignoreConfig bool
metricsConfig map[string]*MetricConfig
metricsExpr map[string]*CachePluginConfig
)
func InitPluginsConfig(cf *config.ConfYaml) {
metricsConfig = make(map[string]MetricConfig)
metricsExpr = make(map[string]map[string]MetricConfig)
ignoreConfig = cf.IgnoreConfig
metricsConfig = make(map[string]*MetricConfig)
metricsExpr = make(map[string]*CachePluginConfig)
plugins := collector.GetRemoteCollectors()
for _, plugin := range plugins {
metricsExpr[plugin] = make(map[string]MetricConfig)
pluginConfig := PluginConfig{}
cacheConfig := newCachePluginConfig()
config := PluginConfig{}
metricsExpr[plugin] = cacheConfig
file := filepath.Join(cf.PluginsConfig, plugin+".yml")
b, err := ioutil.ReadFile(file)
@ -47,12 +72,19 @@ func InitPluginsConfig(cf *config.ConfYaml) {
continue
}
if err := yaml.Unmarshal(b, &pluginConfig); err != nil {
if err := yaml.Unmarshal(b, &config); err != nil {
logger.Warningf("yaml.Unmarshal %s err %s", plugin, err)
continue
}
for _, v := range pluginConfig.Metrics {
if err := config.Validate(); err != nil {
logger.Warningf("%s Validate() err %s", plugin, err)
continue
}
cacheConfig.Name = plugin
cacheConfig.Mode = config.mode
for _, v := range config.Metrics {
if _, ok := metricsConfig[v.Name]; ok {
panic(fmt.Sprintf("plugin %s metrics %s is already exists", plugin, v.Name))
}
@ -65,8 +97,7 @@ func InitPluginsConfig(cf *config.ConfYaml) {
panic(fmt.Sprintf("plugin %s metrics %s expr %s parse err %s",
plugin, v.Name, v.Expr, err))
}
metricsExpr[plugin][v.Name] = v
cacheConfig.Metrics[v.Name] = v
}
}
logger.Infof("loaded plugin config %s", file)
@ -82,9 +113,9 @@ func (p *MetricConfig) Calc(vars map[string]float64) (float64, error) {
return p.notations.Calc(vars)
}
func Metric(metric string, typ telegraf.ValueType) (c MetricConfig, ok bool) {
func Metric(metric string, typ telegraf.ValueType) (c *MetricConfig, ok bool) {
c, ok = metricsConfig[metric]
if !ok && !ignoreConfig {
if !ok {
return
}
@ -95,7 +126,7 @@ func Metric(metric string, typ telegraf.ValueType) (c MetricConfig, ok bool) {
return
}
func GetMetricExprs(pluginName string) (c map[string]MetricConfig, ok bool) {
func GetMetricExprs(pluginName string) (c *CachePluginConfig, ok bool) {
c, ok = metricsExpr[pluginName]
return
}
@ -116,3 +147,9 @@ func metricType(typ telegraf.ValueType) string {
return "GAUGE"
}
}
func newCachePluginConfig() *CachePluginConfig {
return &CachePluginConfig{
Metrics: map[string]*MetricConfig{},
}
}

View File

@ -21,7 +21,6 @@ type ConfYaml struct {
Report report.ReportSection `yaml:"report"`
WorkerProcesses int `yaml:"workerProcesses"`
PluginsConfig string `yaml:"pluginsConfig"`
IgnoreConfig bool `yaml:"ignoreConfig"`
HTTP HTTPSection `yaml:"http"`
}

View File

@ -19,10 +19,10 @@ const (
)
type TokenNotation struct {
tokenType tokenType
o token.Token // operator
v string // variable
c float64 // const
tokenType tokenType
tokenOperator token.Token
tokenVariable string
tokenConst float64
}
type Notations []*TokenNotation
@ -38,11 +38,11 @@ func (s Notations) String() string {
tn := s[i]
switch tn.tokenType {
case tokenOperator:
out.WriteString(tn.o.String() + " ")
out.WriteString(tn.tokenOperator.String() + " ")
case tokenVar:
out.WriteString(tn.v + " ")
out.WriteString(tn.tokenVariable + " ")
case tokenConst:
out.WriteString(fmt.Sprintf("%.0f ", tn.c))
out.WriteString(fmt.Sprintf("%.0f ", tn.tokenConst))
}
}
return out.String()
@ -67,18 +67,18 @@ func (rpn Notations) Calc(vars map[string]float64) (float64, error) {
tn := rpn[i]
switch tn.tokenType {
case tokenVar:
if v, ok := vars[tn.v]; !ok {
return 0, fmt.Errorf("variable %s is not set", tn.v)
if v, ok := vars[tn.tokenVariable]; !ok {
return 0, fmt.Errorf("variable %s is not set", tn.tokenVariable)
} else {
logger.Debugf("get %s %f", tn.v, v)
logger.Debugf("get %s %f", tn.tokenVariable, v)
s.Push(v)
}
case tokenConst:
s.Push(tn.c)
s.Push(tn.tokenConst)
case tokenOperator:
op2 := s.Pop()
op1 := s.Pop()
switch tn.o {
switch tn.tokenOperator {
case token.ADD:
s.Push(op1 + op2)
case token.SUB:
@ -123,9 +123,9 @@ func NewNotations(src []byte) (output Notations, err error) {
return nil, fmt.Errorf("parseFloat error %s\t%s\t%q",
fset.Position(pos), tok, lit)
}
output.Push(&TokenNotation{tokenType: tokenConst, c: c})
output.Push(&TokenNotation{tokenType: tokenConst, tokenConst: c})
case token.IDENT:
output.Push(&TokenNotation{tokenType: tokenVar, v: lit})
output.Push(&TokenNotation{tokenType: tokenVar, tokenVariable: lit})
case token.LPAREN: // (
s.Push(tok)
case token.ADD, token.SUB, token.MUL, token.QUO: // + - * /
@ -135,7 +135,7 @@ func NewNotations(src []byte) (output Notations, err error) {
} else if op := s.Top(); op == token.LPAREN || priority(tok) > priority(op) {
s.Push(tok)
} else {
output.Push(&TokenNotation{tokenType: tokenOperator, o: s.Pop()})
output.Push(&TokenNotation{tokenType: tokenOperator, tokenOperator: s.Pop()})
goto opRetry
}
case token.RPAREN: // )
@ -143,7 +143,7 @@ func NewNotations(src []byte) (output Notations, err error) {
if op := s.Pop(); op == token.LPAREN {
break
} else {
output.Push(&TokenNotation{tokenType: tokenOperator, o: op})
output.Push(&TokenNotation{tokenType: tokenOperator, tokenOperator: op})
}
}
default:
@ -153,7 +153,7 @@ func NewNotations(src []byte) (output Notations, err error) {
out:
for i, l := 0, s.Len(); i < l; i++ {
output.Push(&TokenNotation{tokenType: tokenOperator, o: s.Pop()})
output.Push(&TokenNotation{tokenType: tokenOperator, tokenOperator: s.Pop()})
}
return
}

View File

@ -67,8 +67,7 @@ func (p *ruleEntity) calc() error {
vars[v.Metric] = v.Value
}
// TODO: add some variable from system or rule
for _, config := range configs {
for _, config := range configs.Metrics {
f, err := config.Calc(vars)
if err != nil {
logger.Debugf("calc err %s", err)
@ -85,6 +84,24 @@ func (p *ruleEntity) calc() error {
ValueUntyped: f,
})
}
if configs.Mode == cache.PluginModeOverlay {
for k, v := range vars {
if _, ok := configs.Metrics[k]; ok {
continue
}
p.metrics = append(p.metrics, &dataobj.MetricValue{
Nid: sample.Nid,
Metric: k,
Timestamp: sample.Timestamp,
Step: sample.Step,
CounterType: "GAUGE",
TagsMap: sample.TagsMap,
Value: v,
ValueUntyped: v,
})
}
}
return nil
}

View File

@ -3,6 +3,7 @@ package auth
import (
"github.com/didi/nightingale/src/models"
"github.com/didi/nightingale/src/modules/rdb/config"
"github.com/didi/nightingale/src/modules/rdb/ssoc"
)
var defaultAuth Authenticator
@ -11,8 +12,8 @@ func Init(cf config.AuthExtraSection) {
defaultAuth = *New(cf)
}
func WhiteListAccess(remoteAddr string) error {
return defaultAuth.WhiteListAccess(remoteAddr)
func WhiteListAccess(user *models.User, remoteAddr string) error {
return defaultAuth.WhiteListAccess(user, remoteAddr)
}
// PostLogin check user status after login
@ -28,10 +29,16 @@ func CheckPassword(password string) error {
return defaultAuth.CheckPassword(password)
}
// ChangePasswordRedirect check user should change password before login
// return change password redirect url
func ChangePasswordRedirect(user *models.User, redirect string) string {
return defaultAuth.ChangePasswordRedirect(user, redirect)
func PostCallback(in *ssoc.CallbackOutput) error {
return defaultAuth.PostCallback(in)
}
func DeleteSession(sid string) error {
return defaultAuth.DeleteSession(sid)
}
func DeleteToken(accessToken string) error {
return defaultAuth.DeleteToken(accessToken)
}
func Start() error {

View File

@ -1,6 +1,7 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"net/url"
@ -9,20 +10,28 @@ import (
"github.com/didi/nightingale/src/models"
"github.com/didi/nightingale/src/modules/rdb/cache"
"github.com/didi/nightingale/src/modules/rdb/config"
"github.com/didi/nightingale/src/modules/rdb/ssoc"
"github.com/didi/nightingale/src/toolkits/i18n"
pkgcache "github.com/toolkits/pkg/cache"
"github.com/toolkits/pkg/logger"
)
const (
ChangePasswordURL = "/change-password"
loginModeFifo = true
)
type Authenticator struct {
extraMode bool
whiteList bool
debug bool
debugUser string
frozenTime int64
writenOffTime int64
userExpire bool
ctx context.Context
cancel context.CancelFunc
}
// description:"enable user expire control, active -> frozen -> writen-off"
@ -34,43 +43,22 @@ func New(cf config.AuthExtraSection) *Authenticator {
return &Authenticator{
extraMode: true,
whiteList: cf.WhiteList,
debug: cf.Debug,
debugUser: cf.DebugUser,
frozenTime: 86400 * int64(cf.FrozenDays),
writenOffTime: 86400 * int64(cf.WritenOffDays),
}
}
func (p *Authenticator) WhiteListAccess(remoteAddr string) error {
if !p.whiteList {
func (p *Authenticator) WhiteListAccess(user *models.User, remoteAddr string) error {
if !p.extraMode || !p.whiteList || (p.debug && user.Username != p.debugUser) {
return nil
}
return models.WhiteListAccess(remoteAddr)
}
// ChangePasswordRedirect check user should change password before login
// return change password redirect url
func (p *Authenticator) ChangePasswordRedirect(user *models.User, redirect string) string {
if !p.extraMode {
return ""
if err := models.WhiteListAccess(remoteAddr); err != nil {
return err
}
cf := cache.AuthConfig()
var reason string
if user.PwdUpdatedAt == 0 {
reason = _s("First Login, please change the password in time")
} else if user.PwdUpdatedAt+cf.PwdExpiresIn*86400*30 < time.Now().Unix() {
reason = _s("Password expired, please change the password in time")
} else {
return ""
}
v := url.Values{
"redirect": {redirect},
"username": {user.Username},
"reason": {reason},
"pwdRules": cf.PwdRules(),
}
return ChangePasswordURL + "?" + v.Encode()
return nil
}
func (p *Authenticator) PostLogin(user *models.User, loginErr error) (err error) {
@ -85,7 +73,7 @@ func (p *Authenticator) PostLogin(user *models.User, loginErr error) (err error)
user.Update("status", "login_err_num", "locked_at", "updated_at", "logged_at")
}()
if !p.extraMode || user == nil {
if !p.extraMode || user == nil || (p.debug && user.Username != p.debugUser) {
err = loginErr
return
}
@ -192,42 +180,200 @@ func (p *Authenticator) CheckPassword(password string) error {
return checkPassword(cache.AuthConfig(), password)
}
// PostCallback between sso.Callback() and sessionLogin()
func (p *Authenticator) PostCallback(in *ssoc.CallbackOutput) error {
if !p.extraMode || (p.debug && in.User.Username != p.debugUser) {
return nil
}
cf := cache.AuthConfig()
if err := p.changePasswordRedirect(in, cf); err != nil {
return err
}
// check user session limit
tokens := []models.Token{}
if maxCnt := int(cf.MaxSessionNumber); maxCnt > 0 {
models.DB["sso"].SQL("select * from token where user_name=? order by id desc", in.User.Username).Find(&tokens)
if n := len(tokens); n > maxCnt {
for i := maxCnt; i < n; i++ {
logger.Debugf("[over limit] delete session by token %s %s", tokens[i].UserName, tokens[i].AccessToken)
deleteSessionByToken(&tokens[i])
}
}
}
return nil
}
// ChangePasswordRedirect check user should change password before login
// return err when need changePassword
func (p *Authenticator) changePasswordRedirect(in *ssoc.CallbackOutput, cf *models.AuthConfig) (err error) {
if in.User.PwdUpdatedAt == 0 {
err = _e("First Login, please change the password in time")
} else if cf.PwdExpiresIn > 0 && in.User.PwdUpdatedAt+cf.PwdExpiresIn*86400*30 < time.Now().Unix() {
err = _e("Password expired, please change the password in time")
}
if err != nil {
v := url.Values{
"redirect": {in.Redirect},
"username": {in.User.Username},
"reason": {err.Error()},
"pwdRules": cf.PwdRules(),
}
in.Redirect = ChangePasswordURL + "?" + v.Encode()
}
return
}
func (p *Authenticator) DeleteSession(sid string) error {
s, err := models.SessionGet(sid)
if err != nil {
return err
}
if !p.extraMode {
pkgcache.Delete("sid." + s.Sid)
models.SessionDelete(s.Sid)
return nil
}
return deleteSession(s)
}
func (p *Authenticator) DeleteToken(accessToken string) error {
if !p.extraMode {
return nil
}
token, err := models.TokenGet(accessToken)
if err != nil {
return err
}
return deleteSessionByToken(token)
}
func (p *Authenticator) Stop() error {
p.cancel()
return nil
}
func (p *Authenticator) Start() error {
p.ctx, p.cancel = context.WithCancel(context.Background())
if !p.extraMode {
return nil
}
go func() {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
for {
now := time.Now().Unix()
if p.frozenTime > 0 {
// 3个月以上未登录用户自动变为休眠状态
if _, err := models.DB["rdb"].Exec("update user set status=?, updated_at=?, locked_at=? where ((logged_at > 0 and logged_at<?) or (logged_at == 0 and created_at < ?)) and status in (?,?,?)",
models.USER_S_FROZEN, now, now, now-p.frozenTime,
models.USER_S_ACTIVE, models.USER_S_INACTIVE, models.USER_S_LOCKED); err != nil {
logger.Errorf("update user status error %s", err)
}
select {
case <-p.ctx.Done():
return
case <-t.C:
p.cleanupSession()
}
}
}()
if p.writenOffTime > 0 {
// 变为休眠状态后1年未激活用户自动变为已注销状态
if _, err := models.DB["rdb"].Exec("update user set status=?, updated_at=? where locked_at<? and status=?",
models.USER_S_WRITEN_OFF, now, now-p.writenOffTime, models.USER_S_FROZEN); err != nil {
logger.Errorf("update user status error %s", err)
}
go func() {
t := time.NewTicker(time.Hour)
defer t.Stop()
for {
select {
case <-p.ctx.Done():
return
case <-t.C:
p.updateUserStatus()
}
// reset login err num before 24 hours ago
if _, err := models.DB["rdb"].Exec("update user set login_err_num=0, updated_at=? where updated_at<? and login_err_num>0", now, now-86400); err != nil {
logger.Errorf("update user login err num error %s", err)
}
time.Sleep(time.Hour)
}
}()
return nil
}
// cleanup rdb.session & sso.token
func (p *Authenticator) cleanupSession() {
now := time.Now().Unix()
cf := cache.AuthConfig()
// idle session cleanup
if cf.MaxConnIdelTime > 0 {
expiresAt := now - cf.MaxConnIdelTime*60
sessions := []models.Session{}
if err := models.DB["rdb"].SQL("select * from session where updated_at < ? and username <> '' ", expiresAt).Find(&sessions); err != nil {
logger.Errorf("token idel time cleanup err %s", err)
}
logger.Debugf("find %d idle sessions that should be clean up", len(sessions))
for _, s := range sessions {
if p.debug && s.Username != p.debugUser {
continue
}
logger.Debugf("[idle] deleteSession %s %s", s.Username, s.Sid)
deleteSession(&s)
}
}
// session count limit cleanup
if maxCnt := int(cf.MaxSessionNumber); maxCnt > 0 {
tokens := []models.Token{}
userName := ""
cnt := 0
if err := models.DB["sso"].SQL("select * from token order by user_name, id desc").Find(&tokens); err != nil {
logger.Errorf("token idel time cleanup err %s", err)
}
for _, token := range tokens {
if userName != token.UserName {
userName = token.UserName
cnt = 0
}
cnt++
if cnt > maxCnt {
if p.debug && token.UserName != p.debugUser {
continue
}
logger.Debugf("[over limit] deleteSessionByToken %s %s idx %d max %d", token.UserName, token.AccessToken, cnt, maxCnt)
deleteSessionByToken(&token)
}
}
}
}
func (p *Authenticator) updateUserStatus() {
now := time.Now().Unix()
if p.frozenTime > 0 {
// 3个月以上未登录用户自动变为休眠状态
if _, err := models.DB["rdb"].Exec("update user set status=?, updated_at=?, locked_at=? where ((logged_at > 0 and logged_at<?) or (logged_at == 0 and created_at < ?)) and status in (?,?,?)",
models.USER_S_FROZEN, now, now, now-p.frozenTime,
models.USER_S_ACTIVE, models.USER_S_INACTIVE, models.USER_S_LOCKED); err != nil {
logger.Errorf("update user status error %s", err)
}
}
if p.writenOffTime > 0 {
// 变为休眠状态后1年未激活用户自动变为已注销状态
if _, err := models.DB["rdb"].Exec("update user set status=?, updated_at=? where locked_at<? and status=?",
models.USER_S_WRITEN_OFF, now, now-p.writenOffTime, models.USER_S_FROZEN); err != nil {
logger.Errorf("update user status error %s", err)
}
}
// reset login err num before 24 hours ago
if _, err := models.DB["rdb"].Exec("update user set login_err_num=0, updated_at=? where updated_at<? and login_err_num>0", now, now-86400); err != nil {
logger.Errorf("update user login err num error %s", err)
}
}
func activeUserAccess(cf *models.AuthConfig, user *models.User, loginErr error) error {
now := time.Now().Unix()
@ -250,7 +396,7 @@ func activeUserAccess(cf *models.AuthConfig, user *models.User, loginErr error)
user.LoginErrNum = 0
user.UpdatedAt = now
if cf.MaxSessionNumber > 0 {
if cf.MaxSessionNumber > 0 && !loginModeFifo {
if n, err := models.SessionUserAll(user.Username); err != nil {
return err
} else if n >= cf.MaxSessionNumber {
@ -258,18 +404,6 @@ func activeUserAccess(cf *models.AuthConfig, user *models.User, loginErr error)
}
}
if cf.PwdExpiresIn > 0 && user.PwdUpdatedAt > 0 {
// debug account
// TODO: remove me
if user.Username == "Demo.2022" {
if now-user.PwdUpdatedAt > cf.PwdExpiresIn*60 {
return _e("Password has been expired")
}
}
if now-user.PwdUpdatedAt > cf.PwdExpiresIn*30*86400 {
return _e("Password has been expired")
}
}
return nil
}
func inactiveUserAccess(cf *models.AuthConfig, user *models.User, loginErr error) error {
@ -354,6 +488,25 @@ func checkPassword(cf *models.AuthConfig, passwd string) error {
return nil
}
func deleteSession(s *models.Session) error {
pkgcache.Delete("sid." + s.Sid)
models.SessionDelete(s.Sid)
pkgcache.Delete("access-token." + s.AccessToken)
models.TokenDelete(s.AccessToken)
return nil
}
func deleteSessionByToken(t *models.Token) error {
if s, _ := models.SessionGetByToken(t.AccessToken); s != nil {
deleteSession(s)
} else {
pkgcache.Delete("access-token." + t.AccessToken)
models.TokenDelete(t.AccessToken)
}
return nil
}
func _e(format string, a ...interface{}) error {
return fmt.Errorf(i18n.Sprintf(format, a...))
}

View File

@ -21,6 +21,10 @@ func (p *configCache) AuthConfig() *models.AuthConfig {
}
func (p *configCache) loop(ctx context.Context, interval int) {
if err := p.update(); err != nil {
logger.Errorf("configCache update err %s", err)
}
go func() {
t := time.NewTicker(time.Duration(interval) * time.Second)
defer t.Stop()

View File

@ -29,10 +29,12 @@ type authSection struct {
}
type AuthExtraSection struct {
Enable bool `yaml:"enable"`
WhiteList bool `yaml:"whiteList"`
FrozenDays int `yaml:"frozenDays"`
WritenOffDays int `yaml:"writenOffDays"`
Enable bool `yaml:"enable"`
Debug bool `yaml:"debug" description:"debug"`
DebugUser string `yaml:"debugUser" description:"debug username"`
WhiteList bool `yaml:"whiteList"`
FrozenDays int `yaml:"frozenDays"`
WritenOffDays int `yaml:"writenOffDays"`
}
type wechatSection struct {

View File

@ -27,7 +27,6 @@ func shouldBeLogin() gin.HandlerFunc {
return func(c *gin.Context) {
sessionStart(c)
username := mustUsername(c)
logger.Debugf("set username %s", username)
c.Set("username", username)
c.Next()
sessionUpdate(c)
@ -137,14 +136,6 @@ func sessionUpdate(c *gin.Context) {
}
}
func sessionDestory(c *gin.Context) (sid string, err error) {
if sid, err = session.Destroy(c.Writer, c.Request); sid != "" {
models.SessionCacheDelete(sid)
}
return
}
func sessionUsername(c *gin.Context) string {
s, ok := session.FromContext(c.Request.Context())
if !ok {
@ -153,7 +144,7 @@ func sessionUsername(c *gin.Context) string {
return s.Get("username")
}
func sessionLogin(c *gin.Context, username, remoteAddr string) {
func sessionLogin(c *gin.Context, username, remoteAddr, accessToken string) {
s, ok := session.FromContext(c.Request.Context())
if !ok {
logger.Warningf("session.Start() err not found sessionStore")
@ -167,4 +158,8 @@ func sessionLogin(c *gin.Context, username, remoteAddr string) {
logger.Warningf("session.Set() err %s", err)
return
}
if err := s.Set("accessToken", accessToken); err != nil {
logger.Warningf("session.Set() err %s", err)
return
}
}

View File

@ -203,6 +203,11 @@ func Config(r *gin.Engine) {
v1.GET("/sessions/:sid/user", v1SessionGetUser)
v1.DELETE("/sessions/:sid", v1SessionDelete)
// token
v1.GET("/tokens/:token", v1TokenGet)
v1.GET("/tokens/:token/user", v1TokenGetUser)
v1.DELETE("/tokens/:token", v1TokenDelete)
// 第三方系统同步权限表的数据
v1.GET("/table/sync/role-operation", v1RoleOperationGets)
v1.GET("/table/sync/role-global-user", v1RoleGlobalUserGets)

View File

@ -23,6 +23,7 @@ import (
"github.com/didi/nightingale/src/modules/rdb/cache"
"github.com/didi/nightingale/src/modules/rdb/config"
"github.com/didi/nightingale/src/modules/rdb/redisc"
"github.com/didi/nightingale/src/modules/rdb/session"
"github.com/didi/nightingale/src/modules/rdb/ssoc"
)
@ -31,9 +32,6 @@ var (
loginCodeEmailTpl *template.Template
errUnsupportCaptcha = errors.New("unsupported captcha")
// TODO: set false
debug = true
// https://captcha.mojotv.cn
captchaDirver = base64Captcha.DriverString{
Height: 30,
@ -105,7 +103,7 @@ func login(c *gin.Context) {
return err
}
sessionLogin(c, user.Username, in.RemoteAddr)
sessionLogin(c, user.Username, in.RemoteAddr, "")
return nil
}()
renderMessage(c, err)
@ -162,30 +160,25 @@ func authCallbackV2(c *gin.Context) {
state := queryStr(c, "state", "")
redirect := queryStr(c, "redirect", "")
ret := &authRedirect{Redirect: redirect}
if code == "" && redirect != "" {
logger.Debugf("sso.callback() can't get code and redirect is not set")
renderData(c, ret, nil)
renderData(c, &ssoc.CallbackOutput{Redirect: redirect}, nil)
return
}
var err error
ret.Redirect, ret.User, err = ssoc.Callback(code, state)
ret, err := ssoc.Callback(code, state)
logger.Debugf("sso.callback() ret %s error %v", ret, err)
if err != nil {
logger.Debugf("sso.callback() error %s", err)
renderData(c, ret, err)
renderData(c, nil, err)
return
}
if redirect := auth.ChangePasswordRedirect(ret.User, ret.Redirect); redirect != "" {
logger.Debugf("sso.callback() redirect to changePassword %s", redirect)
ret.Redirect = redirect
renderData(c, ret, nil)
return
if err = auth.PostCallback(ret); err == nil {
logger.Debugf("sso.callback() successfully, set username %s", ret.User.Username)
sessionLogin(c, ret.User.Username, c.ClientIP(), ret.AccessToken)
} else {
logger.Debugf("sso.callback() redirect to changePassword %s", ret.Redirect)
}
logger.Debugf("sso.callback() successfully, set username %s", ret.User.Username)
sessionLogin(c, ret.User.Username, c.ClientIP())
renderData(c, ret, nil)
}
@ -283,10 +276,6 @@ func authLogin(in *v1LoginInput) (user *models.User, err error) {
if err = in.Validate(); err != nil {
return
}
if err := auth.WhiteListAccess(in.RemoteAddr); err != nil {
return nil, _e("Deny Access from %s with whitelist control", in.RemoteAddr)
}
defer func() {
models.LoginLogNew(in.Args[0], in.RemoteAddr, "in", err)
}()
@ -304,6 +293,12 @@ func authLogin(in *v1LoginInput) (user *models.User, err error) {
err = _e("Invalid login type %s", in.Type)
}
if user != nil {
if err := auth.WhiteListAccess(user, in.RemoteAddr); err != nil {
return nil, _e("Deny Access from %s with whitelist control", in.RemoteAddr)
}
}
if err = auth.PostLogin(user, err); err != nil {
return nil, err
}
@ -387,7 +382,7 @@ func sendLoginCode(c *gin.Context) {
return "", err
}
if debug {
if config.Config.Auth.ExtraMode.Debug {
return fmt.Sprintf("[debug]: %s", buf.String()), nil
}
@ -459,7 +454,7 @@ func sendRstCode(c *gin.Context) {
return "", err
}
if debug {
if config.Config.Auth.ExtraMode.Debug {
return fmt.Sprintf("[debug] msg: %s", buf.String()), nil
}
@ -684,19 +679,63 @@ func whiteListDel(c *gin.Context) {
}
func v1SessionGet(c *gin.Context) {
sess, err := models.SessionGetWithCache(urlParamStr(c, "sid"))
renderData(c, sess, err)
s, err := models.SessionGetWithCache(urlParamStr(c, "sid"))
renderData(c, s, err)
}
func v1SessionGetUser(c *gin.Context) {
user, err := models.SessionGetUserWithCache(urlParamStr(c, "sid"))
sid := urlParamStr(c, "sid")
user, err := func() (*models.User, error) {
s, err := models.SessionGetWithCache(sid)
if err != nil {
return nil, err
}
if s.Username == "" {
return nil, fmt.Errorf("user not found")
}
return models.UserMustGet("username=?", s.Username)
}()
renderData(c, user, err)
}
func v1SessionDelete(c *gin.Context) {
sid := urlParamStr(c, "sid")
logger.Debugf("session del sid %s", sid)
renderMessage(c, models.SessionDel(sid))
renderMessage(c, auth.DeleteSession(sid))
}
func v1TokenGet(c *gin.Context) {
t, err := models.TokenGetWithCache(urlParamStr(c, "token"))
renderData(c, t, err)
}
func v1TokenGetUser(c *gin.Context) {
token := urlParamStr(c, "token")
user, err := func() (*models.User, error) {
t, err := models.TokenGetWithCache(token)
if err != nil {
return nil, err
}
if t.Username == "" {
return nil, fmt.Errorf("user not found")
}
return models.UserMustGet("username=?", t.Username)
}()
renderData(c, user, err)
}
// just for auth.extraMode
func v1TokenDelete(c *gin.Context) {
token := urlParamStr(c, "token")
logger.Debugf("del token %s", token)
renderMessage(c, auth.DeleteToken(token))
}
// pwdRulesGet return pwd rules
@ -704,3 +743,11 @@ func pwdRulesGet(c *gin.Context) {
cf := cache.AuthConfig()
renderData(c, cf.PwdRules(), nil)
}
func sessionDestory(c *gin.Context) (sid string, err error) {
if sid, err = session.Destroy(c.Writer, c.Request); sid != "" {
auth.DeleteSession(sid)
}
return
}

View File

@ -62,6 +62,10 @@ func main() {
// 初始化数据库和相关数据
models.InitMySQL("rdb", "hbs")
if config.Config.SSO.Enable && config.Config.Auth.ExtraMode.Enable {
models.InitMySQL("sso")
}
models.InitSalt()
models.InitRooter()

View File

@ -217,6 +217,8 @@ func (p *SessionStore) Set(k, v string) error {
p.session.Username = v
case "remoteAddr":
p.session.RemoteAddr = v
case "accessToken":
p.session.AccessToken = v
default:
fmt.Errorf("unsupported session field %s", k)
}

View File

@ -4,7 +4,6 @@ import (
"time"
"github.com/didi/nightingale/src/models"
"github.com/didi/nightingale/src/modules/rdb/cache"
"github.com/didi/nightingale/src/modules/rdb/config"
"github.com/toolkits/pkg/logger"
)
@ -20,20 +19,13 @@ func newDbStorage(cf *config.SessionSection, opts *options) (storage, error) {
case <-opts.ctx.Done():
return
case <-t.C:
if st := cache.AuthConfig().MaxConnIdelTime * 60; st > 0 {
err := models.SessionCleanup(time.Now().Unix() - st)
if err != nil {
logger.Errorf("session gc err %s", err)
}
} else {
ct := config.Config.HTTP.Session.CookieLifetime
if ct == 0 {
ct = 86400
err := models.SessionCleanupByCreatedAt(time.Now().Unix() - ct)
if err != nil {
logger.Errorf("session gc err %s", err)
}
}
ct := config.Config.HTTP.Session.CookieLifetime
if ct == 0 {
ct = 86400
}
err := models.SessionCleanupByCreatedAt(time.Now().Unix() - ct)
if err != nil {
logger.Errorf("session gc err %s", err)
}
}
@ -60,12 +52,12 @@ func (p *dbStorage) get(sid string) (*models.Session, error) {
}
func (p *dbStorage) insert(s *models.Session) error {
return models.SessionInsert(s)
return s.Save()
}
func (p *dbStorage) del(sid string) error {
return models.SessionDel(sid)
return models.SessionDelete(sid)
}
func (p *dbStorage) update(s *models.Session) error {

View File

@ -3,6 +3,7 @@ package ssoc
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
@ -107,40 +108,58 @@ func LogoutLocation(redirect string) string {
url.QueryEscape(redirect))
}
// Callback 用 code 兑换 accessToken 以及 用户信息,
func Callback(code, state string) (string, *models.User, error) {
s, err := models.AuthStateGet("state=?", state)
if err != nil {
return "", nil, errState
}
s.Del()
u, err := exchangeUser(code)
if err != nil {
return "", nil, errUser
}
user, err := models.UserGet("username=?", u.Username)
if err != nil {
return "", nil, errUser
}
if user == nil {
user = u
err = user.Save()
} else if cli.coverAttributes {
user.Email = u.Email
user.Dispname = u.Dispname
user.Phone = u.Phone
user.Im = u.Im
err = user.Update("email", "dispname", "phone", "im")
}
return s.Redirect, user, err
type CallbackOutput struct {
Redirect string `json:"redirect"`
AccessToken string `json:"accessToken"`
User *models.User `json:"user"`
Msg string `json:"msg"`
}
func exchangeUser(code string) (*models.User, error) {
func (p CallbackOutput) String() string {
b, _ := json.Marshal(p)
return string(b)
}
// Callback 用 code 兑换 accessToken 以及 用户信息,
func Callback(code, state string) (*CallbackOutput, error) {
s, err := models.AuthStateGet("state=?", state)
if err != nil {
return nil, errState
}
s.Del()
ret, err := exchangeUser(code)
if err != nil {
return nil, errUser
}
ret.Redirect = s.Redirect
user, err := models.UserGet("username=?", ret.User.Username)
if err != nil {
return nil, errUser
}
if user != nil {
// user exists
if cli.coverAttributes {
user.Email = ret.User.Email
user.Dispname = ret.User.Dispname
user.Phone = ret.User.Phone
user.Im = ret.User.Im
user.Update("email", "dispname", "phone", "im")
}
ret.User = user
} else {
// create user from sso
if err := ret.User.Save(); err != nil {
return nil, err
}
}
return ret, nil
}
func exchangeUser(code string) (*CallbackOutput, error) {
ctx := context.Background()
oauth2Token, err := cli.config.Exchange(ctx, code)
if err != nil {
@ -169,13 +188,15 @@ func exchangeUser(code string) (*models.User, error) {
}
}
return &models.User{
Username: v(cli.attributes.username),
Dispname: v(cli.attributes.dispname),
Phone: v(cli.attributes.phone),
Email: v(cli.attributes.email),
Im: v(cli.attributes.im),
}, nil
return &CallbackOutput{
AccessToken: oauth2Token.AccessToken,
User: &models.User{
Username: v(cli.attributes.username),
Dispname: v(cli.attributes.dispname),
Phone: v(cli.attributes.phone),
Email: v(cli.attributes.email),
Im: v(cli.attributes.im),
}}, nil
}
func CreateClient(w http.ResponseWriter, body io.ReadCloser) error {