passwords.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package models
  15. import (
  16. "context"
  17. "crypto/sha256"
  18. "database/sql"
  19. "encoding/hex"
  20. "fmt"
  21. "time"
  22. "yunion.io/x/jsonutils"
  23. "yunion.io/x/log"
  24. "yunion.io/x/pkg/errors"
  25. "yunion.io/x/pkg/utils"
  26. "yunion.io/x/sqlchemy"
  27. notifyapi "yunion.io/x/onecloud/pkg/apis/notify"
  28. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  29. "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient"
  30. "yunion.io/x/onecloud/pkg/httperrors"
  31. "yunion.io/x/onecloud/pkg/keystone/options"
  32. o "yunion.io/x/onecloud/pkg/keystone/options"
  33. "yunion.io/x/onecloud/pkg/mcclient"
  34. "yunion.io/x/onecloud/pkg/mcclient/modules/notify"
  35. "yunion.io/x/onecloud/pkg/util/seclib2"
  36. "yunion.io/x/onecloud/pkg/util/stringutils2"
  37. )
  38. // +onecloud:swagger-gen-ignore
  39. type SPasswordManager struct {
  40. db.SResourceBaseManager
  41. }
  42. var PasswordManager *SPasswordManager
  43. func init() {
  44. PasswordManager = &SPasswordManager{
  45. SResourceBaseManager: db.NewResourceBaseManager(
  46. SPassword{},
  47. "password",
  48. "password",
  49. "passwords",
  50. ),
  51. }
  52. PasswordManager.SetVirtualObject(PasswordManager)
  53. PasswordManager.TableSpec().AddIndex(false, "local_user_id", "created_at_int")
  54. }
  55. /*
  56. +----------------+--------------+------+-----+---------+----------------+
  57. | Field | Type | Null | Key | Default | Extra |
  58. +----------------+--------------+------+-----+---------+----------------+
  59. | id | int(11) | NO | PRI | NULL | auto_increment |
  60. | local_user_id | int(11) | NO | MUL | NULL | |
  61. | password | varchar(128) | YES | | NULL | |
  62. | expires_at | datetime | YES | | NULL | |
  63. | self_service | tinyint(1) | NO | | 0 | |
  64. | password_hash | varchar(255) | YES | | NULL | |
  65. | created_at_int | bigint(20) | NO | | 0 | |
  66. | expires_at_int | bigint(20) | YES | | NULL | |
  67. | created_at | datetime | NO | | NULL | |
  68. +----------------+--------------+------+-----+---------+----------------+
  69. */
  70. type SPassword struct {
  71. db.SResourceBase
  72. Id int `primary:"true" auto_increment:"true"`
  73. LocalUserId int `nullable:"false" index:"true"`
  74. Password string `width:"128" charset:"ascii" nullable:"true"`
  75. ExpiresAt time.Time `nullable:"true"`
  76. SelfService bool `nullable:"false" default:"false"`
  77. PasswordHash string `width:"255" charset:"ascii" nullable:"true"`
  78. CreatedAtInt int64 `nullable:"false" default:"0"`
  79. ExpiresAtInt int64 `nullable:"true"`
  80. }
  81. func shaPassword(passwd string) string {
  82. shaOut := sha256.Sum224([]byte(passwd))
  83. return hex.EncodeToString(shaOut[:])
  84. }
  85. func (manager *SPasswordManager) CreateByInsertOrUpdate() bool {
  86. return false
  87. }
  88. func (manager *SPasswordManager) FetchLastPassword(localUserId int) (*SPassword, error) {
  89. passes, err := manager.fetchByLocaluserId(localUserId)
  90. if err != nil {
  91. return nil, err
  92. }
  93. if len(passes) == 0 {
  94. return nil, nil
  95. }
  96. return &passes[0], nil
  97. }
  98. func (manager *SPasswordManager) getFetchByLocaluserIdQuery(localUserId int) *sqlchemy.SQuery {
  99. passwords := manager.Query().SubQuery()
  100. q := passwords.Query().Equals("local_user_id", localUserId)
  101. q = q.Desc(passwords.Field("created_at_int"))
  102. q = q.Limit(options.Options.PasswordHistoryCount() + 1)
  103. return q
  104. }
  105. func (manager *SPasswordManager) FetchByLocaluserIdNewestPassword(localUserId int) (*SPassword, error) {
  106. obj, err := db.NewModelObject(manager)
  107. if err != nil {
  108. return nil, errors.Wrap(err, "new password object")
  109. }
  110. q := manager.getFetchByLocaluserIdQuery(localUserId)
  111. if err := q.First(obj); err != nil {
  112. return nil, errors.Wrap(err, "get newest password object")
  113. }
  114. return obj.(*SPassword), nil
  115. }
  116. func (manager *SPasswordManager) fetchByLocaluserId(localUserId int) ([]SPassword, error) {
  117. passes := make([]SPassword, 0)
  118. q := manager.getFetchByLocaluserIdQuery(localUserId)
  119. err := db.FetchModelObjects(manager, q, &passes)
  120. if err != nil && err != sql.ErrNoRows {
  121. return nil, errors.Wrap(err, "db.FetchModelObjects")
  122. }
  123. return passes, nil
  124. }
  125. func validatePasswordComplexity(password string) error {
  126. if options.Options.PasswordMinimalLength > 0 && len(password) < o.Options.PasswordMinimalLength {
  127. return errors.Wrap(httperrors.ErrWeakPassword, "too simple password")
  128. }
  129. if options.Options.PasswordCharComplexity > 0 {
  130. complexity := options.Options.PasswordCharComplexity
  131. if complexity > 4 {
  132. complexity = 4
  133. }
  134. if stringutils2.GetCharTypeCount(password) < complexity {
  135. return errors.Wrap(httperrors.ErrWeakPassword, "too simple password")
  136. }
  137. }
  138. return nil
  139. }
  140. func (manager *SPasswordManager) validatePassword(localUserId int, password string, skipHistoryCheck bool) error {
  141. err := validatePasswordComplexity(password)
  142. if err != nil {
  143. return errors.Wrap(err, "validatePasswordComplexity")
  144. }
  145. if !skipHistoryCheck && options.Options.PasswordUniqueHistoryCheck > 0 {
  146. shaPass := shaPassword(password)
  147. histPasses, err := manager.fetchByLocaluserId(localUserId)
  148. if err != nil {
  149. return errors.Wrap(err, "manager.fetchByLocaluserId")
  150. }
  151. for i := 0; i < len(histPasses) && i < options.Options.PasswordUniqueHistoryCheck; i += 1 {
  152. if histPasses[i].Password == shaPass {
  153. return errors.Error("repeated password")
  154. }
  155. }
  156. }
  157. return nil
  158. }
  159. func (manager *SPasswordManager) savePassword(localUserId int, password string, isSystemAccount bool) error {
  160. hash, err := seclib2.BcryptPassword(password)
  161. if err != nil {
  162. return errors.Wrap(err, "seclib2.BcryptPassword")
  163. }
  164. rec := &SPassword{
  165. LocalUserId: localUserId,
  166. PasswordHash: hash,
  167. Password: shaPassword(password),
  168. }
  169. rec.SetModelManager(PasswordManager, rec)
  170. now := time.Now()
  171. rec.CreatedAtInt = now.UnixNano() / 1000
  172. if options.Options.PasswordExpirationSeconds > 0 && !isSystemAccount {
  173. rec.ExpiresAt = now.Add(time.Second * time.Duration(options.Options.PasswordExpirationSeconds))
  174. rec.ExpiresAtInt = rec.ExpiresAt.UnixNano() / 1000
  175. }
  176. err = manager.TableSpec().Insert(context.TODO(), rec)
  177. if err != nil {
  178. return errors.Wrap(err, "Insert")
  179. }
  180. return nil
  181. }
  182. func (manager *SPasswordManager) delete(localUserId int) error {
  183. recs, err := manager.fetchByLocaluserId(localUserId)
  184. if err != nil {
  185. return errors.Wrap(err, "manager.fetchByLocaluserId")
  186. }
  187. for i := range recs {
  188. _, err = db.Update(&recs[i], func() error {
  189. return recs[i].MarkDelete()
  190. })
  191. if err != nil {
  192. return errors.Wrap(err, "recs[i].MarkDelete")
  193. }
  194. }
  195. return nil
  196. }
  197. func (passwd *SPassword) IsExpired() bool {
  198. if !passwd.ExpiresAt.IsZero() && passwd.ExpiresAt.Before(time.Now()) {
  199. return true
  200. }
  201. return false
  202. }
  203. // 定时任务判断用户是否需要密码过期通知
  204. func CheckAllUserPasswordIsExpired(ctx context.Context, userCred mcclient.TokenCredential, startRun bool) {
  205. pwds := []SPassword{}
  206. pwdQ := PasswordManager.Query()
  207. pwdQ = pwdQ.Desc("created_at")
  208. err := db.FetchModelObjects(PasswordManager, pwdQ, &pwds)
  209. if err != nil {
  210. log.Errorln("fetch Password error:", err)
  211. return
  212. }
  213. hasCheckedPwd := make(map[int]struct{})
  214. for _, pwd := range pwds {
  215. if _, isExist := hasCheckedPwd[pwd.LocalUserId]; isExist {
  216. continue
  217. }
  218. if pwd.ExpiresAt.IsZero() {
  219. continue
  220. }
  221. hasCheckedPwd[pwd.LocalUserId] = struct{}{}
  222. err = pwd.NeedSendNotify(ctx, userCred)
  223. if err != nil {
  224. log.Errorln(errors.Wrap(err, "pwd.NeedSendNotify"))
  225. }
  226. }
  227. }
  228. func (pwd *SPassword) NeedSendNotify(ctx context.Context, userCred mcclient.TokenCredential) error {
  229. expireTime := time.Date(pwd.ExpiresAt.Year(), pwd.ExpiresAt.Month(), pwd.ExpiresAt.Day(), 0, 0, 0, 0, time.Local)
  230. nowTime := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local)
  231. sub := expireTime.Sub(nowTime)
  232. subDay := int(sub.Hours() / 24)
  233. s := GetDefaultClientSession(ctx, userCred, options.Options.Region)
  234. resp, err := notify.NotifyTopic.List(s, jsonutils.Marshal(map[string]interface{}{
  235. "filter": fmt.Sprintf("name.equals('%s')", notifyapi.DefaultPasswordExpire),
  236. "scope": "system",
  237. }))
  238. if err != nil {
  239. return errors.Wrap(err, "list topics")
  240. }
  241. topics := []notifyapi.TopicDetails{}
  242. err = jsonutils.Update(&topics, resp.Data)
  243. if err != nil {
  244. return errors.Wrap(err, "update topic")
  245. }
  246. if len(topics) != 1 {
  247. return errors.Wrapf(errors.ErrNotSupported, "len topics :%d", len(topics))
  248. }
  249. if utils.IsInArray(subDay, topics[0].AdvanceDays) {
  250. localUser, err := LocalUserManager.fetchLocalUser("", "", pwd.LocalUserId)
  251. if err != nil {
  252. return errors.Wrap(err, "fetchLocalUser error:")
  253. }
  254. pwd.EventNotify(ctx, userCred, notifyapi.ActionPasswordExpireSoon, localUser.Name, subDay)
  255. }
  256. return nil
  257. }
  258. // 密码即将失效消息通知
  259. func (pwd *SPassword) EventNotify(ctx context.Context, userCred mcclient.TokenCredential, action notifyapi.SAction, userName string, advanceDays int) {
  260. resourceType := notifyapi.TOPIC_RESOURCE_USER
  261. detailsDecro := func(ctx context.Context, details *jsonutils.JSONDict) {
  262. details.Set("account", jsonutils.NewString(userName))
  263. details.Set("advance_days", jsonutils.NewInt(int64(advanceDays)))
  264. }
  265. pwd.Password = ""
  266. notifyclient.EventNotify(ctx, userCred, notifyclient.SEventNotifyParam{
  267. Obj: pwd,
  268. ObjDetailsDecorator: detailsDecro,
  269. ResourceType: resourceType,
  270. Action: action,
  271. AdvanceDays: advanceDays,
  272. })
  273. }
  274. func (manager *SPasswordManager) InitializeData() error {
  275. return nil
  276. }
  277. func (manager *SPasswordManager) cleanPasswords() error {
  278. userIds, err := manager.findUserIdsNeedCleanPassword()
  279. if err != nil {
  280. return errors.Wrap(err, "manager.findUserIdsNeedCleanPassword")
  281. }
  282. for _, userId := range userIds {
  283. err := manager.cleanPassword(userId)
  284. if err != nil {
  285. return errors.Wrap(err, "manager.cleanPassword")
  286. }
  287. }
  288. return nil
  289. }
  290. func (manager *SPasswordManager) findUserIdsNeedCleanPassword() ([]int, error) {
  291. q := manager.Query()
  292. q = q.AppendField(q.Field("local_user_id"))
  293. q = q.AppendField(sqlchemy.COUNT("count"))
  294. q = q.GroupBy(q.Field("local_user_id"))
  295. rows, err := q.Rows()
  296. if err != nil {
  297. return nil, errors.Wrap(err, "rows")
  298. }
  299. defer rows.Close()
  300. userIds := []int{}
  301. for rows.Next() {
  302. var localUserId int
  303. var count int
  304. err := rows.Scan(&localUserId, &count)
  305. if err != nil {
  306. return userIds, errors.Wrap(err, "rows.Scan")
  307. }
  308. if count > options.Options.PasswordHistoryCount()+1 {
  309. // need to clean passwords of this user
  310. userIds = append(userIds, localUserId)
  311. }
  312. }
  313. return userIds, nil
  314. }
  315. func (manager *SPasswordManager) findMinPasswordId(localUserId int) (int, error) {
  316. subQ := manager.Query().Equals("local_user_id", localUserId).Desc("created_at_int").Limit(options.Options.PasswordHistoryCount() + 1).SubQuery()
  317. q := subQ.Query().AppendField(sqlchemy.MIN("min_id", subQ.Field("id"))).GroupBy(subQ.Field("id"))
  318. rows, err := q.Rows()
  319. if err != nil {
  320. return -1, errors.Wrap(err, "rows")
  321. }
  322. defer rows.Close()
  323. minId := -1
  324. for rows.Next() {
  325. var id int
  326. err := rows.Scan(&id)
  327. if err != nil {
  328. return -1, errors.Wrap(err, "rows.Scan")
  329. }
  330. minId = id
  331. break
  332. }
  333. return minId, nil
  334. }
  335. func (manager *SPasswordManager) cleanPassword(localUserId int) error {
  336. minId, err := manager.findMinPasswordId(localUserId)
  337. if err != nil {
  338. return errors.Wrap(err, "manager.findMinPasswordId")
  339. }
  340. _, err = manager.TableSpec().GetTableSpec().Database().Exec(fmt.Sprintf("DELETE FROM %s WHERE local_user_id = ? AND id < ?", manager.TableSpec().Name()), localUserId, minId)
  341. if err != nil {
  342. return errors.Wrap(err, "batch delete passwords")
  343. }
  344. return err
  345. }
  346. func (manager *SPasswordManager) CleanPasswordsJob(ctx context.Context, userCred mcclient.TokenCredential, startRun bool) {
  347. err := manager.cleanPasswords()
  348. if err != nil {
  349. log.Errorln(errors.Wrap(err, "manager.cleanPasswords"))
  350. }
  351. }