Merge pull request #14829 from reasonerjt/user-common-dao-cleanup

Move user related funcs from common/dao
This commit is contained in:
Daniel Jiang 2021-05-12 10:56:18 +08:00 committed by GitHub
commit bd9a1c6722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 250 additions and 569 deletions

View File

@ -23,7 +23,6 @@ import (
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
libOrm "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/user"
@ -154,45 +153,6 @@ func clearAll() {
}
}
func TestLoginByUserName(t *testing.T) {
loginUser, err := LoginByDb(models.AuthModel{
Principal: username,
Password: password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
if loginUser == nil {
t.Errorf("No found for user logined by username and password: %s, %s", username, password)
}
if loginUser.Username != username {
t.Errorf("User's username does not match after login, expected: %s, actual: %s", username, loginUser.Username)
}
}
func TestLoginByEmail(t *testing.T) {
userQuery := models.User{
Email: "tester01@vmware.com",
Password: "Abc12345",
}
loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Email,
Password: userQuery.Password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
if loginUser == nil {
t.Errorf("No found for user logined by email and password : %v", userQuery)
}
if loginUser.Username != username {
t.Errorf("User's username does not match after login, expected: %s, actual: %s", username, loginUser.Username)
}
}
var currentUser *models.User
func TestGetUser(t *testing.T) {
@ -217,60 +177,6 @@ func TestGetUser(t *testing.T) {
assert.NotNil(t, err)
}
func TestResetUserPassword(t *testing.T) {
uuid := utils.GenerateRandomString()
err := UpdateUserResetUUID(models.User{ResetUUID: uuid, Email: currentUser.Email})
if err != nil {
t.Errorf("Error occurred in UpdateUserResetUuid: %v", err)
}
err = ResetUserPassword(
models.User{
UserID: currentUser.UserID,
PasswordVersion: utils.SHA256,
ResetUUID: uuid,
Salt: currentUser.Salt}, "HarborTester12345")
if err != nil {
t.Errorf("Error occurred in ResetUserPassword: %v", err)
}
loginedUser, err := LoginByDb(models.AuthModel{Principal: currentUser.Username, Password: "HarborTester12345"})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
if loginedUser.Username != username {
t.Errorf("The username returned by Login does not match, expected: %s, acutal: %s", username, loginedUser.Username)
}
}
func TestChangeUserPassword(t *testing.T) {
user := models.User{UserID: currentUser.UserID}
query, err := GetUser(user)
if err != nil {
t.Errorf("Error occurred when get user salt")
}
currentUser.Salt = query.Salt
err = ChangeUserPassword(
models.User{
UserID: currentUser.UserID,
Password: "NewHarborTester12345",
PasswordVersion: utils.SHA256,
Salt: currentUser.Salt})
if err != nil {
t.Errorf("Error occurred in ChangeUserPassword: %v", err)
}
loginedUser, err := LoginByDb(models.AuthModel{Principal: currentUser.Username, Password: "NewHarborTester12345"})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
if loginedUser.Username != username {
t.Errorf("The username returned by Login does not match, expected: %s, acutal: %s", username, loginedUser.Username)
}
}
func TestAddProject(t *testing.T) {
project := models.Project{
@ -345,29 +251,6 @@ func TestGetProjects(t *testing.T) {
}
}
func TestChangeUserProfile(t *testing.T) {
user := models.User{UserID: currentUser.UserID, Email: username + "@163.com", Realname: "test", Comment: "Unit Test"}
err := ChangeUserProfile(user)
if err != nil {
t.Errorf("Error occurred in ChangeUserProfile: %v", err)
}
loginedUser, err := GetUser(models.User{UserID: currentUser.UserID})
if err != nil {
t.Errorf("Error occurred in GetUser: %v", err)
}
if loginedUser != nil {
if loginedUser.Email != username+"@163.com" {
t.Errorf("user email does not update, expected: %s, acutal: %s", username+"@163.com", loginedUser.Email)
}
if loginedUser.Realname != "test" {
t.Errorf("user realname does not update, expected: %s, acutal: %s", "test", loginedUser.Realname)
}
if loginedUser.Comment != "Unit Test" {
t.Errorf("user email does not update, expected: %s, acutal: %s", "Unit Test", loginedUser.Comment)
}
}
}
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
func TestGetOrmer(t *testing.T) {
@ -430,12 +313,6 @@ func TestDeleteRepository(t *testing.T) {
}
}
func TestIsSuperUser(t *testing.T) {
assert := assert.New(t)
assert.True(IsSuperUser("admin"))
assert.False(IsSuperUser("none"))
}
func TestIsDupRecError(t *testing.T) {
assert.True(t, IsDupRecErr(fmt.Errorf("pq: duplicate key value violates unique constraint \"properties_k_key\"")))
assert.False(t, IsDupRecErr(fmt.Errorf("other error")))

View File

@ -39,44 +39,6 @@ var (
ErrRollBackOIDCUser = errors.New("sql: transaction roll back error in oicd_user")
)
// GetUserBySubIss ...
func GetUserBySubIss(sub, issuer string) (*models.User, error) {
var oidcUsers []models.OIDCUser
n, err := GetOrmer().Raw(`select * from oidc_user where subiss = ? `, sub+issuer).QueryRows(&oidcUsers)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
user, err := GetUser(models.User{
UserID: oidcUsers[0].UserID,
})
if err != nil {
return nil, err
}
if user == nil {
return nil, fmt.Errorf("can not get user %d", oidcUsers[0].UserID)
}
return user, nil
}
// GetOIDCUserByUserID ...
func GetOIDCUserByUserID(userID int) (*models.OIDCUser, error) {
var oidcUsers []models.OIDCUser
n, err := GetOrmer().Raw(`select * from oidc_user where user_id = ? `, userID).QueryRows(&oidcUsers)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
return &oidcUsers[0], nil
}
// UpdateOIDCUser updates the OIDCUser based on the input parm, only the column "secret" and "token" can be updated
func UpdateOIDCUser(oidcUser *models.OIDCUser) error {
cols := []string{"secret", "token"}

View File

@ -60,11 +60,6 @@ func TestOIDCUserMetaDaoMethods(t *testing.T) {
require.NotNil(t, err)
assert.Equal(t, "unable to onboard as empty oidc user", err.Error())
// test get by sub and iss
userGetBySubIss, err := GetUserBySubIss("QWE123", "123RT1")
require.Nil(t, err)
assert.Equal(t, "user111@email.com", userGetBySubIss.Email)
// test update
meta3 := &models.OIDCUser{
ID: ou111.ID,

View File

@ -15,13 +15,9 @@
package dao
import (
"errors"
"fmt"
"time"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
)
// GetUser ...
@ -71,88 +67,6 @@ func GetUser(query models.User) (*models.User, error) {
return &u[0], nil
}
// LoginByDb is used for user to login with database auth mode.
func LoginByDb(auth models.AuthModel) (*models.User, error) {
var users []models.User
o := GetOrmer()
n, err := o.Raw(`select * from harbor_user where (username = ? or email = ?) and deleted = false`,
auth.Principal, auth.Principal).QueryRows(&users)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
user := users[0]
if !matchPassword(&user, auth.Password) {
return nil, nil
}
user.Password = "" // do not return the password
return &user, nil
}
// ChangeUserPassword ...
func ChangeUserPassword(u models.User) error {
u.UpdateTime = time.Now()
u.Salt = utils.GenerateRandomString()
u.Password = utils.Encrypt(u.Password, u.Salt, utils.SHA256)
var err error
if u.PasswordVersion == utils.SHA1 {
u.PasswordVersion = utils.SHA256
_, err = GetOrmer().Update(&u, "Password", "PasswordVersion", "Salt", "UpdateTime")
} else {
_, err = GetOrmer().Update(&u, "Password", "Salt", "UpdateTime")
}
return err
}
// ResetUserPassword ...
func ResetUserPassword(u models.User, rawPassword string) error {
var rowsAffected int64
var err error
u.UpdateTime = time.Now()
u.Password = utils.Encrypt(rawPassword, u.Salt, utils.SHA256)
u.ResetUUID = ""
if u.PasswordVersion == utils.SHA1 {
u.PasswordVersion = utils.SHA256
rowsAffected, err = GetOrmer().Update(&u, "Password", "PasswordVersion", "ResetUUID", "UpdateTime")
} else {
rowsAffected, err = GetOrmer().Update(&u, "Password", "ResetUUID", "UpdateTime")
}
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.New("no record be changed, reset password failed")
}
return nil
}
// UpdateUserResetUUID ...
func UpdateUserResetUUID(u models.User) error {
o := GetOrmer()
_, err := o.Raw(`update harbor_user set reset_uuid=? where email=?`, u.ResetUUID, u.Email).Exec()
return err
}
// ChangeUserProfile - Update user in local db,
// cols to specify the columns need to update,
// Email, and RealName, Comment are updated by default.
func ChangeUserProfile(user models.User, cols ...string) error {
o := GetOrmer()
if len(cols) == 0 {
cols = []string{"Email", "Realname", "Comment"}
}
if _, err := o.Update(&user, cols...); err != nil {
log.Errorf("update user failed, error: %v", err)
return err
}
return nil
}
// OnBoardUser will check if a user exists in user table, if not insert the user and
// put the id in the pointer of user model, if it does exist, return the user's profile.
// This is used for ldap and uaa authentication, such the user can have an ID in Harbor.
@ -185,26 +99,8 @@ func OnBoardUser(u *models.User) error {
return nil
}
// IsSuperUser checks if the user is super user(conventionally id == 1) of Harbor
func IsSuperUser(username string) bool {
u, err := GetUser(models.User{
Username: username,
})
log.Debugf("Check if user %s is super user", username)
if err != nil {
log.Errorf("Failed to get user from DB, username: %s, error: %v", username, err)
return false
}
return u != nil && u.UserID == 1
}
// CleanUser - Clean this user information from DB
func CleanUser(id int64) error {
_, err := GetOrmer().QueryTable(&models.User{}).Filter("UserID", id).Delete()
return err
}
// MatchPassword returns true is password matched
func matchPassword(u *models.User, password string) bool {
return utils.Encrypt(password, u.Salt, u.PasswordVersion) == u.Password
}

View File

@ -15,7 +15,10 @@
package models
import (
"context"
"time"
"github.com/astaxie/beego/orm"
)
// UserTable is the name of table in DB that holds the user object
@ -50,3 +53,20 @@ type User struct {
func (u *User) TableName() string {
return UserTable
}
// FilterByUsernameOrEmail generates the query setter to match username or email column to the same value
func (u *User) FilterByUsernameOrEmail(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter {
usernameOrEmail, ok := value.(string)
if !ok {
return qs
}
subCond := orm.NewCondition()
subCond = subCond.Or("Username", usernameOrEmail).Or("Email", usernameOrEmail)
conds := qs.GetCond()
if conds == nil {
conds = orm.NewCondition()
}
qs = qs.SetCond(conds.AndCond(subCond))
return qs
}

View File

@ -21,8 +21,9 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
pkguser "github.com/goharbor/harbor/src/pkg/user"
)
// InitDatabaseFromEnv is used to initialize database for testing
@ -86,9 +87,7 @@ func updateUserInitialPassword(userID int, password string) error {
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
user.Salt = utils.GenerateRandomString()
user.Password = password
err = dao.ChangeUserPassword(*user)
err = pkguser.Mgr.UpdatePassword(orm.Context(), userID, password)
if err != nil {
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
}

View File

@ -37,7 +37,7 @@ type Controller interface {
// SetSysAdmin ...
SetSysAdmin(ctx context.Context, id int, adminFlag bool) error
// VerifyPassword ...
VerifyPassword(ctx context.Context, username string, password string) (bool, error)
VerifyPassword(ctx context.Context, usernameOrEmail string, password string) (bool, error)
// UpdatePassword ...
UpdatePassword(ctx context.Context, id int, password string) error
// List ...
@ -50,11 +50,13 @@ type Controller interface {
Get(ctx context.Context, id int, opt *Option) (*models.User, error)
// GetByName gets the user model by username, it only supports getting the basic and does not support opt
GetByName(ctx context.Context, username string) (*models.User, error)
// GetBySubIss gets the user model by subject and issuer, the result will contain the basic user model and does not support opt
GetBySubIss(ctx context.Context, sub, iss string) (*models.User, error)
// Delete ...
Delete(ctx context.Context, id int) error
// UpdateProfile update the profile based on the ID and data in the model in parm, only a subset of attributes in the model
// will be update, see the implementation of manager.
UpdateProfile(ctx context.Context, u *models.User) error
UpdateProfile(ctx context.Context, u *models.User, cols ...string) error
// SetCliSecret sets the OIDC CLI secret for a user
SetCliSecret(ctx context.Context, id int, secret string) error
}
@ -77,6 +79,14 @@ type controller struct {
oidcMetaMgr oidc.MetaManager
}
func (c *controller) GetBySubIss(ctx context.Context, sub, iss string) (*models.User, error) {
oidcMeta, err := c.oidcMetaMgr.GetBySubIss(ctx, sub, iss)
if err != nil {
return nil, err
}
return c.Get(ctx, oidcMeta.UserID, nil)
}
func (c *controller) GetByName(ctx context.Context, username string) (*models.User, error) {
return c.mgr.GetByName(ctx, username)
}
@ -89,8 +99,8 @@ func (c *controller) Create(ctx context.Context, u *models.User) (int, error) {
return c.mgr.Create(ctx, u)
}
func (c *controller) UpdateProfile(ctx context.Context, u *models.User) error {
return c.mgr.UpdateProfile(ctx, u)
func (c *controller) UpdateProfile(ctx context.Context, u *models.User, cols ...string) error {
return c.mgr.UpdateProfile(ctx, u, cols...)
}
func (c *controller) Get(ctx context.Context, id int, opt *Option) (*models.User, error) {
@ -132,8 +142,12 @@ func (c *controller) UpdatePassword(ctx context.Context, id int, password string
return c.mgr.UpdatePassword(ctx, id, password)
}
func (c *controller) VerifyPassword(ctx context.Context, username, password string) (bool, error) {
return c.mgr.VerifyLocalPassword(ctx, username, password)
func (c *controller) VerifyPassword(ctx context.Context, usernameOrEmail, password string) (bool, error) {
rec, err := c.mgr.MatchLocalPassword(ctx, usernameOrEmail, password)
if err != nil {
return false, err
}
return rec != nil, nil
}
func (c *controller) SetSysAdmin(ctx context.Context, id int, adminFlag bool) error {

View File

@ -21,9 +21,10 @@ import (
o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
@ -49,13 +50,14 @@ func (ia *InternalAPI) Prepare() {
// RenameAdmin we don't provide flexibility in this API, as this is a workaround.
func (ia *InternalAPI) RenameAdmin() {
if !dao.IsSuperUser(ia.SecurityCtx.GetUsername()) {
ctx := ia.Ctx.Request.Context()
if !auth.IsSuperUser(ctx, ia.SecurityCtx.GetUsername()) {
log.Errorf("User %s is not super user, not allow to rename admin.", ia.SecurityCtx.GetUsername())
ia.SendForbiddenError(errors.New(ia.SecurityCtx.GetUsername()))
return
}
newName := common.NewHarborAdminName
if err := dao.ChangeUserProfile(models.User{
if err := user.Ctl.UpdateProfile(ctx, &models.User{
UserID: 1,
Username: newName,
}, "username"); err != nil {

View File

@ -15,19 +15,19 @@
package auth
import (
"context"
"errors"
"fmt"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/config"
libErrors "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/usergroup/model"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/user"
"github.com/goharbor/harbor/src/pkg/usergroup/model"
)
// 1.5 seconds
@ -137,12 +137,12 @@ func Register(name string, h AuthenticateHelper) {
// Login authenticates user credentials based on setting.
func Login(m models.AuthModel) (*models.User, error) {
authMode, err := config.AuthMode(orm.Context())
ctx := orm.Context()
authMode, err := config.AuthMode(ctx)
if err != nil {
return nil, err
}
if authMode == "" || dao.IsSuperUser(m.Principal) {
if authMode == "" || IsSuperUser(ctx, m.Principal) {
authMode = common.DBAuth
}
log.Debug("Current AUTH_MODE is ", authMode)
@ -257,3 +257,13 @@ func PostAuthenticate(u *models.User) error {
}
return helper.PostAuthenticate(u)
}
// IsSuperUser checks if the user is super user(conventionally id == 1) of Harbor
func IsSuperUser(ctx context.Context, username string) bool {
u, err := user.Mgr.GetByName(ctx, username)
if err != nil {
log.Errorf("Failed to get user from DB, username: %s, error: %v", username, err)
return false
}
return u.UserID == 1
}

View File

@ -19,6 +19,8 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/user"
)
// Auth implements Authenticator interface to authenticate user against DB.
@ -28,7 +30,7 @@ type Auth struct {
// Authenticate calls dao to authenticate user.
func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u, err := dao.LoginByDb(m)
u, err := user.Mgr.MatchLocalPassword(orm.Context(), m.Principal, m.Password)
if err != nil {
return nil, err
}

View File

@ -283,7 +283,7 @@ func (l *Auth) PostAuthenticate(u *models.User) error {
if !Re.MatchString(u.Email) {
log.Debugf("Not a valid email address: %v, skip to sync", u.Email)
} else {
if err = dao.ChangeUserProfile(*u, "Email"); err != nil {
if err = user.Mgr.UpdateProfile(ctx, u, "Email"); err != nil {
u.Email = dbUser.Email
log.Errorf("failed to sync user email: %v", err)
}

View File

@ -16,8 +16,6 @@ package uaa
import (
"fmt"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"os"
"strings"
"sync"
@ -27,7 +25,10 @@ import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/uaa"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
userpkg "github.com/goharbor/harbor/src/pkg/user"
)
// Auth is the implementation of AuthenticateHelper to access uaa for authentication.
@ -95,10 +96,9 @@ func (u *Auth) PostAuthenticate(user *models.User) error {
user.UserID = dbUser.UserID
user.SysAdminFlag = dbUser.SysAdminFlag
fillEmailRealName(user)
if err2 := dao.ChangeUserProfile(*user, "Email", "Realname"); err2 != nil {
if err2 := userpkg.Mgr.UpdateProfile(orm.Context(), user, "Email", "Realname"); err2 != nil {
log.Warningf("Failed to update user profile, user: %s, error: %v", user.Username, err2)
}
return nil
}

View File

@ -15,25 +15,18 @@
package controllers
import (
"bytes"
"context"
"html/template"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"github.com/astaxie/beego"
o "github.com/astaxie/beego/orm"
"github.com/beego/i18n"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/utils"
email_util "github.com/goharbor/harbor/src/common/utils/email"
"github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/lib"
@ -41,7 +34,6 @@ import (
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/user"
)
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
@ -68,18 +60,18 @@ func redirectForOIDC(ctx context.Context, username string) bool {
if lib.GetAuthMode(ctx) != common.OIDCAuth {
return false
}
u, err := dao.GetUser(models.User{Username: username})
u, err := user.Ctl.GetByName(ctx, username)
if err != nil {
log.Warningf("Failed to get user by name: %s, error: %v", username, err)
}
if u == nil {
return true
}
ou, err := dao.GetOIDCUserByUserID(u.UserID)
us, err := user.Ctl.Get(ctx, u.UserID, &user.Option{WithOIDCInfo: true})
if err != nil {
log.Warningf("Failed to get OIDC user info for user, id: %d, error: %v", u.UserID, err)
}
if ou != nil {
if us != nil && us.OIDCUserMeta != nil {
return true
}
return false
@ -149,7 +141,7 @@ func (cc *CommonController) UserExists() {
query = q.New(q.KeyWords{"Email": value})
}
n, err := user.Mgr.Count(ctx, query)
n, err := user.Ctl.Count(ctx, query)
if err != nil {
log.Errorf("Error occurred in UserExists: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
@ -158,145 +150,6 @@ func (cc *CommonController) UserExists() {
cc.ServeJSON()
}
// SendResetEmail verifies the Email address and contact SMTP server to send reset password Email.
func (cc *CommonController) SendResetEmail() {
email := cc.GetString("email")
valid, err := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, email)
if err != nil {
log.Errorf("failed to match regexp: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if !valid {
cc.CustomAbort(http.StatusBadRequest, "invalid email")
}
queryUser := models.User{Email: email}
u, err := dao.GetUser(queryUser)
if err != nil {
log.Errorf("Error occurred in GetUser: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if u == nil {
log.Debugf("email %s not found", email)
cc.CustomAbort(http.StatusNotFound, "email_does_not_exist")
}
if !isUserResetable(u) {
log.Errorf("Resetting password for user with ID: %d is not allowed", u.UserID)
cc.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
uuid := utils.GenerateRandomString()
user := models.User{ResetUUID: uuid, Email: email}
if err = dao.UpdateUserResetUUID(user); err != nil {
log.Errorf("failed to update user reset UUID: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
messageTemplate, err := template.ParseFiles("views/reset-password-mail.tpl")
if err != nil {
log.Errorf("Parse email template file failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, err.Error())
}
message := new(bytes.Buffer)
harborURL, err := config.ExtEndpoint()
if err != nil {
log.Errorf("failed to get domain name: %v", err)
cc.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
err = messageTemplate.Execute(message, messageDetail{
Hint: cc.Tr("reset_email_hint"),
URL: harborURL,
UUID: uuid,
})
if err != nil {
log.Errorf("Message template error: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
settings, err := config.Email(orm.Context())
if err != nil {
log.Errorf("failed to get email configurations: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
}
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
err = email_util.Send(addr,
settings.Identity,
settings.Username,
settings.Password,
60, settings.SSL,
settings.Insecure,
settings.From,
[]string{u.Email},
"Reset Harbor user password",
message.String())
if err != nil {
log.Errorf("Send email failed: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "send_email_failed")
}
}
// ResetPassword handles request from the reset page and reset password
func (cc *CommonController) ResetPassword() {
resetUUID := cc.GetString("reset_uuid")
if resetUUID == "" {
cc.CustomAbort(http.StatusBadRequest, "Reset uuid is blank.")
}
queryUser := models.User{ResetUUID: resetUUID}
user, err := dao.GetUser(queryUser)
if err != nil {
log.Errorf("Error occurred in GetUser: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if user == nil {
log.Error("User does not exist")
cc.CustomAbort(http.StatusBadRequest, "User does not exist")
}
if !isUserResetable(user) {
log.Errorf("Resetting password for user with ID: %d is not allowed", user.UserID)
cc.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
rawPassword := cc.GetString("password")
if rawPassword != "" {
err = dao.ResetUserPassword(*user, rawPassword)
if err != nil {
log.Errorf("Error occurred in ResetUserPassword: %v", err)
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
} else {
cc.CustomAbort(http.StatusBadRequest, "password_is_required")
}
}
func isUserResetable(u *models.User) bool {
if u == nil {
return false
}
mode, err := config.AuthMode(orm.Context())
if err != nil {
log.Errorf("Failed to get the auth mode, error: %v", err)
return false
}
if mode == common.DBAuth {
return true
}
return u.UserID == 1
}
func init() {
// conf/app.conf -> os.Getenv("config_path")
configPath := os.Getenv("CONFIG_PATH")

View File

@ -15,10 +15,6 @@ package controllers
import (
"fmt"
"github.com/goharbor/harbor/src/core/middlewares"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"net/http"
"net/http/httptest"
"os"
@ -27,9 +23,13 @@ import (
"strings"
"testing"
"github.com/goharbor/harbor/src/core/middlewares"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/astaxie/beego"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
utilstest "github.com/goharbor/harbor/src/common/utils/test"
_ "github.com/goharbor/harbor/src/pkg/config/db"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
@ -58,35 +58,6 @@ func TestMain(m *testing.M) {
}
}
// TestUserResettable
func TestUserResettable(t *testing.T) {
assert := assert.New(t)
DBAuthConfig := map[string]interface{}{
common.AUTHMode: common.DBAuth,
common.TokenExpiration: 30,
}
LDAPAuthConfig := map[string]interface{}{
common.AUTHMode: common.LDAPAuth,
common.TokenExpiration: 30,
}
config.InitWithSettings(LDAPAuthConfig)
u1 := &models.User{
UserID: 3,
Username: "daniel",
Email: "daniel@test.com",
}
u2 := &models.User{
UserID: 1,
Username: "jack",
Email: "jack@test.com",
}
assert.False(isUserResetable(u1))
assert.True(isUserResetable(u2))
config.InitWithSettings(DBAuthConfig)
assert.True(isUserResetable(u1))
}
func TestRedirectForOIDC(t *testing.T) {
ctx := lib.WithAuthMode(orm.Context(), common.DBAuth)
assert.False(t, redirectForOIDC(ctx, "nonexist"))

View File

@ -17,8 +17,6 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"net/http"
"strings"
@ -26,9 +24,12 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/oidc"
)
@ -110,31 +111,23 @@ func (oc *OIDCController) Callback() {
oc.SendInternalServerError(err)
return
}
u, err := dao.GetUserBySubIss(info.Subject, info.Issuer)
if err != nil {
oc.SendInternalServerError(err)
return
}
tokenBytes, err := json.Marshal(token)
if err != nil {
oc.SendInternalServerError(err)
return
}
oc.SetSession(tokenKey, tokenBytes)
oidcSettings, err := config.OIDCSetting(ctx)
if err != nil {
oc.SendInternalServerError(err)
return
}
if u == nil {
u, err := user.Ctl.GetBySubIss(ctx, info.Subject, info.Issuer)
if errors.IsNotFoundErr(err) { // User is not onboarded, kickoff the onboard flow
// Recover the username from d.Username by default
username := info.Username
// Fix blanks in username
username = strings.Replace(username, " ", "_", -1)
oidcSettings, err := config.OIDCSetting(ctx)
if err != nil {
oc.SendInternalServerError(err)
return
}
// If automatic onboard is enabled, skip the onboard page
if oidcSettings.AutoOnboard {
log.Debug("Doing automatic onboarding\n")
@ -143,27 +136,31 @@ func (oc *OIDCController) Callback() {
oidcSettings.UserClaim))
return
}
user, onboarded := userOnboard(oc, info, username, tokenBytes)
userRec, onboarded := userOnboard(oc, info, username, tokenBytes)
if onboarded == false {
log.Error("User not onboarded\n")
return
}
log.Debug("User automatically onboarded\n")
u = user
u = userRec
} else {
oc.SetSession(userInfoKey, string(ouDataStr))
oc.Controller.Redirect(fmt.Sprintf("/oidc-onboard?username=%s", username), http.StatusFound)
// Once redirected, no further actions are done
return
}
} else if err != nil {
oc.SendInternalServerError(err)
return
}
oidc.InjectGroupsToUser(info, u)
oidcUser, err := dao.GetOIDCUserByUserID(u.UserID)
um, err := user.Ctl.Get(ctx, u.UserID, &user.Option{WithOIDCInfo: true})
if err != nil {
oc.SendInternalServerError(err)
return
}
_, t, err := secretAndToken(tokenBytes)
oidcUser := um.OIDCUserMeta
oidcUser.Token = t
if err := dao.UpdateOIDCUser(oidcUser); err != nil {
oc.SendInternalServerError(err)
@ -171,7 +168,6 @@ func (oc *OIDCController) Callback() {
}
oc.PopulateUserSession(*u)
oc.Controller.Redirect("/", http.StatusFound)
}
func userOnboard(oc *OIDCController, info *oidc.UserInfo, username string, tokenBytes []byte) (*models.User, bool) {

View File

@ -34,10 +34,10 @@ import (
"github.com/goharbor/harbor/src/common/dao"
common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
_ "github.com/goharbor/harbor/src/controller/event/handler"
"github.com/goharbor/harbor/src/controller/health"
"github.com/goharbor/harbor/src/controller/registry"
ctluser "github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/core/api"
_ "github.com/goharbor/harbor/src/core/auth/authproxy"
_ "github.com/goharbor/harbor/src/core/auth/db"
@ -66,7 +66,7 @@ const (
adminUserID = 1
)
func updateInitPassword(userID int, password string) error {
func updateInitPassword(ctx context.Context, userID int, password string) error {
queryUser := models.User{UserID: userID}
user, err := dao.GetUser(queryUser)
if err != nil {
@ -76,11 +76,7 @@ func updateInitPassword(userID int, password string) error {
return fmt.Errorf("user id: %d does not exist", userID)
}
if user.Salt == "" {
salt := utils.GenerateRandomString()
user.Salt = salt
user.Password = password
err = dao.ChangeUserPassword(*user)
err = ctluser.Ctl.UpdatePassword(ctx, userID, password)
if err != nil {
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
}
@ -203,7 +199,7 @@ func main() {
if err != nil {
log.Fatalf("failed to get admin's initial password: %v", err)
}
if err := updateInitPassword(adminUserID, password); err != nil {
if err := updateInitPassword(ctx, adminUserID, password); err != nil {
log.Error(err)
}

View File

@ -31,6 +31,8 @@ type MetaManager interface {
Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error)
// GetByUserID gets the oidc meta record by user's ID
GetByUserID(ctx context.Context, uid int) (*models.OIDCUser, error)
// GetBySubIss gets the oidc meta record by the subject and issuer
GetBySubIss(ctx context.Context, sub, iss string) (*models.OIDCUser, error)
// SetCliSecretByUserID updates the cli secret of a user based on the user ID
SetCliSecretByUserID(ctx context.Context, uid int, secret string) error
}
@ -39,6 +41,21 @@ type metaManager struct {
dao dao.MetaDAO
}
func (m *metaManager) GetBySubIss(ctx context.Context, sub, iss string) (*models.OIDCUser, error) {
logger := log.GetLogger(ctx)
l, err := m.dao.List(ctx, q.New(q.KeyWords{"subiss": sub + iss}))
if err != nil {
return nil, err
}
if len(l) == 0 {
return nil, errors.NotFoundError(nil).WithMessage("oidc info for user with issuer %s, subject %s not found", iss, sub)
}
if len(l) > 1 {
logger.Warningf("Multiple oidc info records found for issuer %s, subject %s", iss, sub)
}
return l[0], nil
}
func (m *metaManager) Create(ctx context.Context, oidcUser *models.OIDCUser) (int, error) {
return m.dao.Create(ctx, oidcUser)
}

View File

@ -74,17 +74,43 @@ func (suite *DaoTestSuite) TestCount() {
}
func (suite *DaoTestSuite) TestList() {
ctx := orm.Context()
{
users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"user_id": 1}))
users, err := suite.dao.List(ctx, q.New(q.KeyWords{"user_id": 1}))
suite.Nil(err)
suite.Len(users, 1)
}
{
users, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"username": "admin"}))
users, err := suite.dao.List(ctx, q.New(q.KeyWords{"username": "admin"}))
suite.Nil(err)
suite.Len(users, 1)
}
id, err := suite.dao.Create(ctx, &models.User{
Username: "list_test",
Realname: "list test",
Email: "list_test@test.com",
Password: "somepassword",
PasswordVersion: "sha256",
})
suite.appendClearSQL(id)
suite.Nil(err)
{
users, err := suite.dao.List(ctx, q.New(q.KeyWords{"username_or_email": "list_test"}))
suite.Nil(err)
suite.Len(users, 1)
}
{
users, err := suite.dao.List(ctx, q.New(q.KeyWords{"username_or_email": "list_test@test.com"}))
suite.Nil(err)
suite.Len(users, 1)
}
{
users, err := suite.dao.List(ctx, q.New(q.KeyWords{"username_or_email": "noremail_norusername"}))
suite.Nil(err)
suite.Len(users, 0)
}
}
func (suite *DaoTestSuite) TestCreate() {

View File

@ -35,7 +35,7 @@ var (
type Manager interface {
// Get get user by user id
Get(ctx context.Context, id int) (*models.User, error)
// GetByName get user by username
// GetByName get user by username, it will return an error if the user does not exist
GetByName(ctx context.Context, username string) (*models.User, error)
// List users according to the query
List(ctx context.Context, query *q.Query) (models.Users, error)
@ -48,11 +48,12 @@ type Manager interface {
// SetSysAdminFlag sets the system admin flag of the user in local DB
SetSysAdminFlag(ctx context.Context, id int, admin bool) error
// UpdateProfile updates the user's profile
UpdateProfile(ctx context.Context, user *models.User) error
UpdateProfile(ctx context.Context, user *models.User, col ...string) error
// UpdatePassword updates user's password
UpdatePassword(ctx context.Context, id int, newPassword string) error
// VerifyLocalPassword verifies the password against the record in DB based on the input
VerifyLocalPassword(ctx context.Context, username, password string) (bool, error)
// MatchLocalPassword tries to match the record in DB based on the input, the first return value is
// the user model corresponding to the entry in DB
MatchLocalPassword(ctx context.Context, username, password string) (*models.User, error)
}
// New returns a default implementation of Manager
@ -75,20 +76,29 @@ func (m *manager) Delete(ctx context.Context, id int) error {
return m.dao.Update(ctx, u, "username", "email", "deleted")
}
func (m *manager) VerifyLocalPassword(ctx context.Context, username, password string) (bool, error) {
u, err := m.GetByName(ctx, username)
func (m *manager) MatchLocalPassword(ctx context.Context, usernameOrEmail, password string) (*models.User, error) {
l, err := m.dao.List(ctx, q.New(q.KeyWords{"username_or_email": usernameOrEmail}))
if err != nil {
return false, err
return nil, err
}
return utils.Encrypt(password, u.Salt, u.PasswordVersion) == u.Password, nil
for _, entry := range l {
if utils.Encrypt(password, entry.Salt, entry.PasswordVersion) == entry.Password {
entry.Password = ""
return entry, nil
}
}
return nil, nil
}
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
return m.dao.Count(ctx, query)
}
func (m *manager) UpdateProfile(ctx context.Context, user *models.User) error {
return m.dao.Update(ctx, user, "email", "realname", "comment")
func (m *manager) UpdateProfile(ctx context.Context, user *models.User, cols ...string) error {
if cols == nil || len(cols) == 0 {
cols = []string{"Email", "Realname", "Comment"}
}
return m.dao.Update(ctx, user, cols...)
}
func (m *manager) UpdatePassword(ctx context.Context, id int, newPassword string) error {
@ -126,7 +136,7 @@ func (m *manager) Get(ctx context.Context, id int) (*models.User, error) {
return users[0], nil
}
// Get get user by username
// GetByName get user by username
func (m *manager) GetByName(ctx context.Context, username string) (*models.User, error) {
users, err := m.dao.List(ctx, q.New(q.KeyWords{"username": username}))
if err != nil {

View File

@ -15,15 +15,15 @@
package security
import (
"github.com/goharbor/harbor/src/lib/config"
"net/http"
"strings"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/oidc"
)
@ -48,15 +48,11 @@ func (i *idToken) Generate(req *http.Request) security.Context {
log.Warningf("failed to verify token: %v", err)
return nil
}
u, err := dao.GetUserBySubIss(claims.Subject, claims.Issuer)
u, err := user.Ctl.GetBySubIss(ctx, claims.Subject, claims.Issuer)
if err != nil {
log.Warningf("failed to get user based on token claims: %v", err)
return nil
}
if u == nil {
log.Warning("user matches token's claims is not onboarded.")
return nil
}
setting, err := config.OIDCSetting(ctx)
if err != nil {
log.Errorf("failed to get OIDC settings: %v", err)

View File

@ -120,6 +120,29 @@ func (_m *Controller) GetByName(ctx context.Context, username string) (*models.U
return r0, r1
}
// GetBySubIss provides a mock function with given fields: ctx, sub, iss
func (_m *Controller) GetBySubIss(ctx context.Context, sub string, iss string) (*models.User, error) {
ret := _m.Called(ctx, sub, iss)
var r0 *models.User
if rf, ok := ret.Get(0).(func(context.Context, string, string) *models.User); ok {
r0 = rf(ctx, sub, iss)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, sub, iss)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Controller) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
ret := _m.Called(ctx, query)
@ -185,13 +208,20 @@ func (_m *Controller) UpdatePassword(ctx context.Context, id int, password strin
return r0
}
// UpdateProfile provides a mock function with given fields: ctx, u
func (_m *Controller) UpdateProfile(ctx context.Context, u *models.User) error {
ret := _m.Called(ctx, u)
// UpdateProfile provides a mock function with given fields: ctx, u, cols
func (_m *Controller) UpdateProfile(ctx context.Context, u *models.User, cols ...string) error {
_va := make([]interface{}, len(cols))
for _i := range cols {
_va[_i] = cols[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, u)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.User) error); ok {
r0 = rf(ctx, u)
if rf, ok := ret.Get(0).(func(context.Context, *models.User, ...string) error); ok {
r0 = rf(ctx, u, cols...)
} else {
r0 = ret.Error(0)
}
@ -199,20 +229,20 @@ func (_m *Controller) UpdateProfile(ctx context.Context, u *models.User) error {
return r0
}
// VerifyPassword provides a mock function with given fields: ctx, username, password
func (_m *Controller) VerifyPassword(ctx context.Context, username string, password string) (bool, error) {
ret := _m.Called(ctx, username, password)
// VerifyPassword provides a mock function with given fields: ctx, usernameOrEmail, password
func (_m *Controller) VerifyPassword(ctx context.Context, usernameOrEmail string, password string) (bool, error) {
ret := _m.Called(ctx, usernameOrEmail, password)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
r0 = rf(ctx, username, password)
r0 = rf(ctx, usernameOrEmail, password)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, username, password)
r1 = rf(ctx, usernameOrEmail, password)
} else {
r1 = ret.Error(1)
}

View File

@ -143,6 +143,29 @@ func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users,
return r0, r1
}
// MatchLocalPassword provides a mock function with given fields: ctx, username, password
func (_m *Manager) MatchLocalPassword(ctx context.Context, username string, password string) (*models.User, error) {
ret := _m.Called(ctx, username, password)
var r0 *models.User
if rf, ok := ret.Get(0).(func(context.Context, string, string) *models.User); ok {
r0 = rf(ctx, username, password)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, username, password)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetSysAdminFlag provides a mock function with given fields: ctx, id, admin
func (_m *Manager) SetSysAdminFlag(ctx context.Context, id int, admin bool) error {
ret := _m.Called(ctx, id, admin)
@ -171,37 +194,23 @@ func (_m *Manager) UpdatePassword(ctx context.Context, id int, newPassword strin
return r0
}
// UpdateProfile provides a mock function with given fields: ctx, _a1
func (_m *Manager) UpdateProfile(ctx context.Context, _a1 *models.User) error {
ret := _m.Called(ctx, _a1)
// UpdateProfile provides a mock function with given fields: ctx, _a1, col
func (_m *Manager) UpdateProfile(ctx context.Context, _a1 *models.User, col ...string) error {
_va := make([]interface{}, len(col))
for _i := range col {
_va[_i] = col[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, _a1)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.User) error); ok {
r0 = rf(ctx, _a1)
if rf, ok := ret.Get(0).(func(context.Context, *models.User, ...string) error); ok {
r0 = rf(ctx, _a1, col...)
} else {
r0 = ret.Error(0)
}
return r0
}
// VerifyLocalPassword provides a mock function with given fields: ctx, username, password
func (_m *Manager) VerifyLocalPassword(ctx context.Context, username string, password string) (bool, error) {
ret := _m.Called(ctx, username, password)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok {
r0 = rf(ctx, username, password)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, username, password)
} else {
r1 = ret.Error(1)
}
return r0, r1
}