From 0de5999f52557c03b11b5e7eb082019734d5c052 Mon Sep 17 00:00:00 2001 From: Yan Date: Thu, 28 Mar 2019 12:46:35 +0800 Subject: [PATCH] add the controller for ocdi onboard user Signed-off-by: wang yan --- .../postgresql/0004_1.8.0_schema.up.sql | 19 +++ src/common/dao/oidc_user.go | 154 ++++++++++++++++++ src/common/dao/oidc_user_test.go | 123 ++++++++++++++ src/common/models/base.go | 3 +- src/common/models/oidc_user.go | 20 +++ src/common/models/user.go | 1 + src/core/controllers/controllers_test.go | 6 + src/core/controllers/oidc.go | 2 +- 8 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 src/common/dao/oidc_user.go create mode 100644 src/common/dao/oidc_user_test.go create mode 100644 src/common/models/oidc_user.go diff --git a/make/migrations/postgresql/0004_1.8.0_schema.up.sql b/make/migrations/postgresql/0004_1.8.0_schema.up.sql index f36592a33..88e4bea5b 100644 --- a/make/migrations/postgresql/0004_1.8.0_schema.up.sql +++ b/make/migrations/postgresql/0004_1.8.0_schema.up.sql @@ -13,6 +13,25 @@ CREATE TABLE robot ( CREATE TRIGGER robot_update_time_at_modtime BEFORE UPDATE ON robot FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column(); +CREATE TABLE oidc_user ( + id SERIAL NOT NULL, + user_id int NOT NULL, + secret varchar(255) NOT NULL, + /* + Subject and Issuer + Subject: Subject Identifier. + Issuer: Issuer Identifier for the Issuer of the response. + The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can rely upon as a stable identifier for the End-User + */ + subiss varchar(255) NOT NULL, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE (subiss) +); + +CREATE TRIGGER odic_user_update_time_at_modtime BEFORE UPDATE ON oidc_user FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column(); + /*add master role*/ INSERT INTO role (role_code, name) VALUES ('DRWS', 'master'); diff --git a/src/common/dao/oidc_user.go b/src/common/dao/oidc_user.go new file mode 100644 index 000000000..6f00726a6 --- /dev/null +++ b/src/common/dao/oidc_user.go @@ -0,0 +1,154 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "fmt" + "strings" + "time" + + "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" +) + +// AddOIDCUser adds a oidc user +func AddOIDCUser(meta *models.OIDCUser) (int64, error) { + now := time.Now() + meta.CreationTime = now + meta.UpdateTime = now + id, err := GetOrmer().Insert(meta) + if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return 0, ErrDupRows + } + return 0, err + } + return id, nil +} + +// GetOIDCUserByID ... +func GetOIDCUserByID(id int64) (*models.OIDCUser, error) { + oidcUser := &models.OIDCUser{ + ID: id, + } + if err := GetOrmer().Read(oidcUser); err != nil { + if err == orm.ErrNoRows { + return nil, nil + } + return nil, err + } + + return oidcUser, nil +} + +// GetUserBySub ... +func GetUserBySub(sub string) (*models.User, error) { + var oidcUsers []models.OIDCUser + n, err := GetOrmer().Raw(`select * from oidc_user where subiss = ? `, sub).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 ... +func UpdateOIDCUser(oidcUser *models.OIDCUser) error { + oidcUser.UpdateTime = time.Now() + _, err := GetOrmer().Update(oidcUser) + return err +} + +// DeleteOIDCUser ... +func DeleteOIDCUser(id int64) error { + _, err := GetOrmer().QueryTable(&models.OIDCUser{}).Filter("ID", id).Delete() + return err +} + +// OnBoardOIDCUser onboard OIDC user +func OnBoardOIDCUser(u models.User) error { + o := orm.NewOrm() + err := o.Begin() + if err != nil { + return err + } + // the password is the required attribute of user table, + // but not required in the oidc user login scenario. + u.Email = "odicpassword" + var errInsert error + userID, err := o.Insert(&u) + if err != nil { + errInsert = err + log.Errorf("fail to insert user, %v", err) + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + errInsert = ErrDupRows + } + err := o.Rollback() + if err != nil { + return err + } + return errInsert + + } + u.OIDCUserMeta.UserID = int(userID) + _, err = o.Insert(u.OIDCUserMeta) + if err != nil { + errInsert = err + log.Errorf("fail to insert user, %v", err) + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + errInsert = ErrDupRows + } + err := o.Rollback() + if err != nil { + return err + } + return errInsert + } + err = o.Commit() + if err != nil { + return err + } + + return nil +} diff --git a/src/common/dao/oidc_user_test.go b/src/common/dao/oidc_user_test.go new file mode 100644 index 000000000..2e27a0716 --- /dev/null +++ b/src/common/dao/oidc_user_test.go @@ -0,0 +1,123 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "testing" + + "github.com/goharbor/harbor/src/common/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + user111 = models.User{ + Username: "user111", + Email: "user111@email.com", + } + user222 = models.User{ + Username: "user222", + Email: "user222@email.com", + } + ou111 = &models.OIDCUser{ + SubIss: "QWE123123RT1", + Secret: "QWEQWE1", + } + ou222 = &models.OIDCUser{ + SubIss: "QWE123123RT2", + Secret: "QWEQWE2", + } +) + +func TestOIDCUserMetaDaoMethods(t *testing.T) { + + err := OnBoardUser(&user111) + require.Nil(t, err) + ou111.UserID = user111.UserID + err = OnBoardUser(&user222) + require.Nil(t, err) + ou222.UserID = user222.UserID + + // test add + _, err = AddOIDCUser(ou111) + require.Nil(t, err) + _, err = AddOIDCUser(ou222) + require.Nil(t, err) + + // test get + oidcUser1, err := GetOIDCUserByID(ou111.ID) + require.Nil(t, err) + assert.Equal(t, ou111.UserID, oidcUser1.UserID) + + // test update + meta3 := &models.OIDCUser{ + ID: ou111.ID, + UserID: ou111.UserID, + SubIss: "newSub", + } + require.Nil(t, UpdateOIDCUser(meta3)) + oidcUser1Update, err := GetOIDCUserByID(ou111.ID) + require.Nil(t, err) + assert.Equal(t, "newSub", oidcUser1Update.SubIss) + + user, err := GetUserBySub("newSub") + require.Nil(t, err) + assert.Equal(t, "user111", user.Username) +} + +func TestOIDCOnboard(t *testing.T) { + user333 := models.User{ + Username: "user333", + Email: "user333@email.com", + } + user555 := models.User{ + Username: "user555", + Email: "user555@email.com", + } + ou333 := &models.OIDCUser{ + UserID: 333, + SubIss: "QWE123123RT1", + Secret: "QWEQWE333", + } + ouDupSub := &models.OIDCUser{ + UserID: 444, + SubIss: "QWE123123RT1", + Secret: "QWEQWE444", + } + + // duplicate user -- ErrDupRows + user111.OIDCUserMeta = ou333 + err := OnBoardOIDCUser(user111) + require.NotNil(t, err) + require.Equal(t, err, ErrDupRows) + + // duplicate OIDC user -- ErrDupRows + user333.OIDCUserMeta = ou111 + err = OnBoardOIDCUser(user333) + require.NotNil(t, err) + require.Equal(t, err, ErrDupRows) + + // success + user333.OIDCUserMeta = ou333 + err = OnBoardOIDCUser(user333) + require.Nil(t, err) + + // duplicate OIDC user's sub -- ErrDupRows + user555.OIDCUserMeta = ouDupSub + err = OnBoardOIDCUser(user555) + require.NotNil(t, err) + require.Equal(t, err, ErrDupRows) + +} diff --git a/src/common/models/base.go b/src/common/models/base.go index 2faeb0a1a..35ad3a97a 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -38,5 +38,6 @@ func init() { new(UserGroup), new(AdminJob), new(JobLog), - new(Robot)) + new(Robot), + new(OIDCUser)) } diff --git a/src/common/models/oidc_user.go b/src/common/models/oidc_user.go new file mode 100644 index 000000000..5d6c66c35 --- /dev/null +++ b/src/common/models/oidc_user.go @@ -0,0 +1,20 @@ +package models + +import ( + "time" +) + +// OIDCUser ... +type OIDCUser struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + UserID int `orm:"column(user_id)" json:"user_id"` + Secret string `orm:"column(secret)" json:"secret"` + SubIss string `orm:"column(subiss)" json:"subiss"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` +} + +// TableName ... +func (o *OIDCUser) TableName() string { + return "oidc_user" +} diff --git a/src/common/models/user.go b/src/common/models/user.go index c638cff8c..d10ae91a3 100644 --- a/src/common/models/user.go +++ b/src/common/models/user.go @@ -41,6 +41,7 @@ type User struct { CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` GroupList []*UserGroup `orm:"-" json:"-"` + OIDCUserMeta *OIDCUser `orm:"-" json:"oidc_user_meta"` } // UserQuery ... diff --git a/src/core/controllers/controllers_test.go b/src/core/controllers/controllers_test.go index 85cdeec7c..9e3950716 100644 --- a/src/core/controllers/controllers_test.go +++ b/src/core/controllers/controllers_test.go @@ -50,6 +50,7 @@ func init() { beego.Router("/c/reset", &CommonController{}, "post:ResetPassword") beego.Router("/c/userExists", &CommonController{}, "post:UserExists") beego.Router("/c/sendEmail", &CommonController{}, "get:SendResetEmail") + beego.Router("/c/oidc/onboard", &OIDCController{}, "post:Onboard") beego.Router("/v2/*", &RegistryProxy{}, "*:Handle") } @@ -138,4 +139,9 @@ func TestAll(t *testing.T) { w = httptest.NewRecorder() beego.BeeApp.Handlers.ServeHTTP(w, r) assert.Equal(int(404), w.Code, "GET v2/noproject/manifests/1.0 should get a 404 response") + + r, _ = http.NewRequest("POST", "/c/oidc/onboard", nil) + w = httptest.NewRecorder() + beego.BeeApp.Handlers.ServeHTTP(w, r) + assert.Equal(int(500), w.Code, "/c/oidc/onboard httpStatusCode should be 500") } diff --git a/src/core/controllers/oidc.go b/src/core/controllers/oidc.go index 7ba18580d..653240703 100644 --- a/src/core/controllers/oidc.go +++ b/src/core/controllers/oidc.go @@ -95,4 +95,4 @@ func (oc *OIDCController) Onboard() { oc.RenderError(http.StatusNotImplemented, "") return -} +} \ No newline at end of file