diff --git a/build/notify/root/opt/yunion/share/template/content@cn/USER_LOGIN_EXCEPTION b/build/notify/root/opt/yunion/share/template/content@cn/USER_LOGIN_EXCEPTION index dcae36ca25..eaef1bc1c5 100644 --- a/build/notify/root/opt/yunion/share/template/content@cn/USER_LOGIN_EXCEPTION +++ b/build/notify/root/opt/yunion/share/template/content@cn/USER_LOGIN_EXCEPTION @@ -1,5 +1,5 @@ {{- if .admin -}} -域{{ .domain }}下的账号{{ .user }}由于异常登录已被锁定,请您核实用户使用情况。如果需要为用户解锁,请到用户列表启用该用户。 +{{ .domain }}域下的账号{{ .user }}由于异常登录已被锁定,请您核实用户使用情况。如果需要为用户解锁,请到用户列表启用该用户。 {{- else -}} 您的账号{{ .user }}由于异常登录已被锁定,请联系管理员解锁账号。 {{- end -}} diff --git a/build/notify/root/opt/yunion/share/template/title@cn/USER_LOGIN_EXCEPTION b/build/notify/root/opt/yunion/share/template/title@cn/USER_LOGIN_EXCEPTION index 55a1746fe5..ed803d52c7 100644 --- a/build/notify/root/opt/yunion/share/template/title@cn/USER_LOGIN_EXCEPTION +++ b/build/notify/root/opt/yunion/share/template/title@cn/USER_LOGIN_EXCEPTION @@ -1 +1 @@ -安全告警 +异常登录 diff --git a/build/notify/root/opt/yunion/share/template/title@en/USER_LOGIN_EXCEPTION b/build/notify/root/opt/yunion/share/template/title@en/USER_LOGIN_EXCEPTION index ce81af334e..16b279812d 100644 --- a/build/notify/root/opt/yunion/share/template/title@en/USER_LOGIN_EXCEPTION +++ b/build/notify/root/opt/yunion/share/template/title@en/USER_LOGIN_EXCEPTION @@ -1 +1 @@ -Security Alerts +Abnormal Login diff --git a/pkg/cloudcommon/notifyclient/notify.go b/pkg/cloudcommon/notifyclient/notify.go index c5b46a9756..77fbb291ea 100644 --- a/pkg/cloudcommon/notifyclient/notify.go +++ b/pkg/cloudcommon/notifyclient/notify.go @@ -19,6 +19,7 @@ import ( "fmt" "html/template" "io/ioutil" + "os" "path/filepath" "regexp" "strings" @@ -56,6 +57,8 @@ var ( notifyclientI18nTable = i18n.Table{} AdminSessionGenerator SAdminSessionGenerator = getAdminSesion UserLangFetcher SUserLangFetcher = getUserLang + topicWithTemplateSet = &sync.Map{} + checkTemplates bool ) type SAdminSessionGenerator func(ctx context.Context, region string, apiVersion string) (*mcclient.ClientSession, error) @@ -102,6 +105,29 @@ func init() { notifyclientI18nTable.Set(SUFFIX, i18n.NewTableEntry().EN("en").CN("cn")) } +func hasTemplateOfTopic(topic string) bool { + if checkTemplates { + _, ok := topicWithTemplateSet.Load(topic) + return ok + } + path := filepath.Join(consts.NotifyTemplateDir, consts.GetServiceType(), "content@cn") + fileInfoList, err := ioutil.ReadDir(path) + if err != nil { + if os.IsNotExist(err) { + checkTemplates = true + return false + } + log.Errorf("unable to read dir %s", path) + return false + } + for i := range fileInfoList { + topicWithTemplateSet.Store(fileInfoList[i].Name(), nil) + } + checkTemplates = true + _, ok := topicWithTemplateSet.Load(topic) + return ok +} + func getTemplateString(suffix string, topic string, contType string, channel npk.TNotifyChannel) ([]byte, error) { contType = contType + "@" + suffix if len(channel) > 0 { @@ -364,6 +390,20 @@ func genMsgViaLang(ctx context.Context, p sNotifyParams) ([]npk.SNotifyMessage, reIds = p.recipientId } + if !hasTemplateOfTopic(p.event) { + msg := npk.SNotifyMessage{} + msg.Uid = reIds + msg.Priority = p.priority + msg.Contacts = p.contacts + msg.ContactType = p.channel + msg.Topic = p.event + msg.Msg = p.data.String() + msg.Tag = p.tag + msg.Metadata = p.metadata + msg.IgnoreNonexistentReceiver = p.ignoreNonexistentReceiver + return []npk.SNotifyMessage{msg}, nil + } + langMap, err := lang(ctx, p.channel, reIds, p.contacts) if err != nil { return nil, err @@ -403,7 +443,6 @@ func intelliNotify(ctx context.Context, p sNotifyParams) { } for i := range msgs { msg := msgs[i] - log.Infof("msg: %s", jsonutils.Marshal(msg)) notifyClientWorkerMan.Run(func() { s, err := AdminSessionGenerator(context.Background(), consts.GetRegion(), "") if err != nil { diff --git a/pkg/keystone/driver/sql/sql.go b/pkg/keystone/driver/sql/sql.go index 598620e062..86a88b3284 100644 --- a/pkg/keystone/driver/sql/sql.go +++ b/pkg/keystone/driver/sql/sql.go @@ -82,7 +82,25 @@ func (sql *SSQLDriver) Authenticate(ctx context.Context, ident mcclient.SAuthent } func (sql *SSQLDriver) alertNotify(ctx context.Context, uext *api.SUserExtended, triggerTime time.Time) { - // get all users + // users + data := jsonutils.NewDict() + data.Set("user", jsonutils.NewString(uext.Name)) + data.Set("domain", jsonutils.NewString(uext.DomainName)) + metadata := map[string]interface{}{ + "trigger_time": triggerTime, + } + p := notifyclient.SNotifyParams{ + RecipientId: []string{uext.Id}, + Priority: notify.NotifyPriorityCritical, + Event: notifyclient.USER_LOGIN_EXCEPTION, + Data: data, + Tag: noapi.NOTIFICATION_TAG_ALERT, + Metadata: metadata, + IgnoreNonexistentReceiver: true, + } + notifyclient.NotifyWithTag(ctx, p) + + // admin user daUserIds, err := getDomainAdminUserIds(uext.DomainName) if err != nil { log.Errorf("unable to get user with role domainadmin in domain %s: %v", uext.DomainName, err) @@ -94,23 +112,9 @@ func (sql *SSQLDriver) alertNotify(ctx context.Context, uext *api.SUserExtended, userSet := sets.NewString(daUserIds...) userSet.Insert(aUserIds...) userSet.Insert(uext.Id) - - data := jsonutils.NewDict() - data.Set("user", jsonutils.NewString(uext.Name)) - data.Set("domain", jsonutils.NewString(uext.DomainName)) - metadata := map[string]interface{}{ - "trigger_time": triggerTime, - } - // user - p := notifyclient.SNotifyParams{ - RecipientId: userSet.UnsortedList(), - Priority: notify.NotifyPriorityCritical, - Event: notifyclient.USER_LOGIN_EXCEPTION, - Data: data, - Tag: noapi.NOTIFICATION_TAG_ALERT, - Metadata: metadata, - IgnoreNonexistentReceiver: true, - } + data.Set("admin", jsonutils.JSONTrue) + p.RecipientId = userSet.UnsortedList() + p.Data = data notifyclient.NotifyWithTag(ctx, p) } diff --git a/pkg/keystone/models/users.go b/pkg/keystone/models/users.go index 8a7ab324b7..798a1ec35f 100644 --- a/pkg/keystone/models/users.go +++ b/pkg/keystone/models/users.go @@ -735,6 +735,19 @@ func (user *SUser) PostUpdate(ctx context.Context, userCred mcclient.TokenCreden } logclient.AddActionLogWithContext(ctx, user, logclient.ACT_UPDATE_PASSWORD, nil, userCred, true) } + if enabled, _ := data.Bool("enabled"); enabled { + localUser, err := LocalUserManager.fetchLocalUser(user.Id, user.DomainId, 0) + if err != nil { + if err == sql.ErrNoRows { + return + } + log.Errorf("unable to fetch localUser of user %q in domain %q: %v", user.Id, user.DomainId, err) + return + } + if err = localUser.ClearFailedAuth(); err != nil { + log.Errorf("unable to clear failed auth: %v", err) + } + } } func (user *SUser) ValidateDeleteCondition(ctx context.Context) error { diff --git a/pkg/notify/models/notification.go b/pkg/notify/models/notification.go index 92b0838614..a4507d5021 100644 --- a/pkg/notify/models/notification.go +++ b/pkg/notify/models/notification.go @@ -201,7 +201,7 @@ func (nm *SNotificationManager) FetchCustomizeColumns( var err error for i := range rows { - rows[i], err = objs[i].(*SNotification).getMoreDetails(ctx, query, rows[i]) + rows[i], err = objs[i].(*SNotification).getMoreDetails(ctx, userCred, query, rows[i]) if err != nil { log.Errorf("Notification.getMoreDetails: %v", err) } @@ -233,11 +233,25 @@ func (n *SNotification) ReceiverNotificationsNotOK() ([]SReceiverNotification, e return rns, nil } -func (n *SNotification) ReceiveDetails() ([]api.ReceiveDetail, error) { - subRQ := ReceiverManager.Query("id", "name").SubQuery() +func (n *SNotification) ReceiveDetails(userCred mcclient.TokenCredential, scope string) ([]api.ReceiveDetail, error) { + RQ := ReceiverManager.Query("id", "name") q := ReceiverNotificationManager.Query("receiver_id", "notification_id", "contact", "send_at", "send_by", "status", "failed_reason").Equals("notification_id", n.Id) - q.AppendField(subRQ.Field("name", "receiver_name")) - q = q.LeftJoin(subRQ, sqlchemy.OR(sqlchemy.Equals(q.Field("receiver_id"), subRQ.Field("id")), sqlchemy.Equals(q.Field("contact"), subRQ.Field("id")))) + s := rbacutils.TRbacScope(scope) + + switch s { + case rbacutils.ScopeSystem: + subRQ := RQ.SubQuery() + q.AppendField(subRQ.Field("name", "receiver_name")) + q = q.LeftJoin(subRQ, sqlchemy.OR(sqlchemy.Equals(q.Field("receiver_id"), subRQ.Field("id")), sqlchemy.Equals(q.Field("contact"), subRQ.Field("id")))) + case rbacutils.ScopeDomain: + subRQ := RQ.Equals("domain_id", userCred.GetDomainId()).SubQuery() + q.AppendField(subRQ.Field("name", "receiver_name")) + q = q.Join(subRQ, sqlchemy.OR(sqlchemy.Equals(q.Field("receiver_id"), subRQ.Field("id")), sqlchemy.Equals(q.Field("contact"), subRQ.Field("id")))) + default: + subRQ := RQ.Equals("id", userCred.GetUserId()).SubQuery() + q.AppendField(subRQ.Field("name", "receiver_name")) + q = q.Join(subRQ, sqlchemy.OR(sqlchemy.Equals(q.Field("receiver_id"), subRQ.Field("id")), sqlchemy.Equals(q.Field("contact"), subRQ.Field("id")))) + } ret := make([]api.ReceiveDetail, 0, 2) err := q.All(&ret) if err != nil && errors.Cause(err) != sql.ErrNoRows { @@ -247,7 +261,7 @@ func (n *SNotification) ReceiveDetails() ([]api.ReceiveDetail, error) { return ret, nil } -func (n *SNotification) getMoreDetails(ctx context.Context, query jsonutils.JSONObject, out api.NotificationDetails) (api.NotificationDetails, error) { +func (n *SNotification) getMoreDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, out api.NotificationDetails) (api.NotificationDetails, error) { // get title adn content p, err := TemplateManager.NotifyFilter(n.ContactType, n.Topic, n.Message, getTemplateLangFromCtx(ctx)) if err != nil { @@ -255,8 +269,10 @@ func (n *SNotification) getMoreDetails(ctx context.Context, query jsonutils.JSON } out.Title = p.Title out.Content = p.Message + + scope, _ := query.GetString("scope") // get receive details - out.ReceiveDetails, err = n.ReceiveDetails() + out.ReceiveDetails, err = n.ReceiveDetails(userCred, scope) if err != nil { return out, err } @@ -294,10 +310,11 @@ func (nm *SNotificationManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient case rbacutils.ScopeDomain: subRq := ReceiverManager.Query("id").Equals("domain_id", owner.GetDomainId()).SubQuery() RNq := ReceiverNotificationManager.Query("notification_id", "receiver_id") - subRNq := RNq.Join(subRq, sqlchemy.Equals(RNq.Field("receiver_id"), subRq.Field("id"))).SubQuery() + subRNq := RNq.Join(subRq, sqlchemy.OR(sqlchemy.Equals(RNq.Field("receiver_id"), subRq.Field("id")), sqlchemy.Equals(RNq.Field("contact"), subRq.Field("id")))).SubQuery() q = q.Join(subRNq, sqlchemy.Equals(q.Field("id"), subRNq.Field("notification_id"))) case rbacutils.ScopeProject, rbacutils.ScopeUser: - subq := ReceiverNotificationManager.Query("notification_id").Equals("receiver_id", owner.GetUserId()).SubQuery() + sq := ReceiverNotificationManager.Query("notification_id") + subq := sq.Filter(sqlchemy.OR(sqlchemy.Equals(sq.Field("receiver_id"), owner.GetUserId()), sqlchemy.Equals(sq.Field("contact"), owner.GetUserId()))).SubQuery() q = q.Join(subq, sqlchemy.Equals(q.Field("id"), subq.Field("notification_id"))) } return q diff --git a/pkg/notify/models/template.go b/pkg/notify/models/template.go index 1f70b18cc2..f00f8a7937 100644 --- a/pkg/notify/models/template.go +++ b/pkg/notify/models/template.go @@ -146,8 +146,9 @@ func (tm *STemplateManager) GetCompanyInfo(ctx context.Context) (SCompanyInfo, e } var ( - ForceInitType = []string{ - api.EMAIL, + forceInitTopic = []string{ + "VERIFY", + "USER_LOGIN_EXCEPTION", } notifyclientI18nTable = i18n.Table{} defaultLang = api.TEMPLATE_LANG_CN @@ -187,7 +188,7 @@ func (tm *STemplateManager) InitializeData() error { for _, template := range templates { q := tm.Query().Equals("contact_type", template.ContactType).Equals("topic", template.Topic).Equals("template_type", template.TemplateType).Equals("lang", template.Lang) count, _ := q.CountWithError() - if count > 0 && !utils.IsInStringArray(template.ContactType, ForceInitType) { + if count > 0 && !utils.IsInStringArray(template.Topic, forceInitTopic) { continue } if count == 0 {