diff --git a/.erda/migrations/kratos/20210914-kratos-uc-id.sql b/.erda/migrations/kratos/20210914-kratos-uc-id.sql new file mode 100644 index 00000000000..9eaf735f8ad --- /dev/null +++ b/.erda/migrations/kratos/20210914-kratos-uc-id.sql @@ -0,0 +1,8 @@ +CREATE TABLE `kratos_uc_userid_mapping` ( + `id` varchar(50) NOT NULL COMMENT 'uc userid', + `user_id` varchar(191) NOT NULL COMMENT 'kratos user uuid', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='id mapping'; diff --git a/apistructs/user.go b/apistructs/user.go index 4d6241e59ce..2381b6c79d9 100644 --- a/apistructs/user.go +++ b/apistructs/user.go @@ -131,6 +131,11 @@ type UserListResponse struct { Data UserListResponseData `json:"data"` } +type UserIDResponse struct { + Header + Data string `json:"data"` +} + // UserListResponseData 用户批量查询响应数据 type UserListResponseData struct { Users []UserInfo `json:"users"` diff --git a/bundle/user.go b/bundle/user.go index f6a0f62e53d..cfcc74122dd 100644 --- a/bundle/user.go +++ b/bundle/user.go @@ -67,6 +67,27 @@ func (b *Bundle) ListUsers(req apistructs.UserListRequest) (*apistructs.UserList return &userResp.Data, nil } +func (b *Bundle) GetUcUserID(uuid string) (string, error) { + host, err := b.urls.CoreServices() + if err != nil { + return "", err + } + hc := b.hc + + var userResp apistructs.UserIDResponse + resp, err := hc.Get(host).Path("/api/users/actions/get-uc-user-id"). + Header(httputil.InternalHeader, "bundle"). + Param("id", uuid). + Do().JSON(&userResp) + if err != nil { + return "", apierrors.ErrInvoke.InternalError(err) + } + if !resp.IsOK() || !userResp.Success { + return "", toAPIError(resp.StatusCode(), userResp.Error) + } + return userResp.Data, nil +} + func (b *Bundle) SearchUser(params url.Values) (*apistructs.UserListResponseData, error) { host, err := b.urls.CoreServices() if err != nil { diff --git a/conf/openapi/openapi.yaml b/conf/openapi/openapi.yaml index 8575619ec85..82b337411fc 100644 --- a/conf/openapi/openapi.yaml +++ b/conf/openapi/openapi.yaml @@ -42,7 +42,7 @@ openapi-auth: openapi-auth-ory-kratos: _enable: ${ORY_ENABLED:false} weight: 100 - ory_kratos_addr: "${ORY_KRATOS_ADDR:kratos:4433}" + ory_kratos_addr: "${ORY_KRATOS_ADDR:kratos-public}" openapi-auth-uc: _enable: ${UC_ENABLED:true} weight: 100 diff --git a/modules/cmp/conf/conf.go b/modules/cmp/conf/conf.go index 5b87a46a947..aea3e3b8ddc 100644 --- a/modules/cmp/conf/conf.go +++ b/modules/cmp/conf/conf.go @@ -39,8 +39,7 @@ type Conf struct { UCClientSecret string `env:"UC_CLIENT_SECRET"` // ory/kratos config OryEnabled bool `default:"false" env:"ORY_ENABLED"` - OryKratosAddr string `default:"kratos:4433" env:"KRATOS_ADDR"` - OryKratosPrivateAddr string `default:"kratos:4434" env:"KRATOS_PRIVATE_ADDR"` + OryKratosPrivateAddr string `default:"kratos-admin" env:"ORY_KRATOS_ADMIN_ADDR"` // size of steve server cache, default 1Gi CacheSize int64 `default:"1073741824" env:"CMP_CACHE_SIZE"` diff --git a/modules/cmp/initialize.go b/modules/cmp/initialize.go index 92a47c98205..c161c64f41f 100644 --- a/modules/cmp/initialize.go +++ b/modules/cmp/initialize.go @@ -112,6 +112,7 @@ func do(ctx context.Context) (*httpserver.Server, error) { uc := ucauth.NewUCClient(discover.UC(), conf.UCClientID(), conf.UCClientSecret()) if conf.OryEnabled() { uc = ucauth.NewUCClient(conf.OryKratosPrivateAddr(), conf.OryCompatibleClientID(), conf.OryCompatibleClientSecret()) + uc.SetDBClient(db.DB) } // init Bundle diff --git a/modules/core-services/conf/conf.go b/modules/core-services/conf/conf.go index 2ce60d0ec12..6bf535d03de 100644 --- a/modules/core-services/conf/conf.go +++ b/modules/core-services/conf/conf.go @@ -63,8 +63,7 @@ type Conf struct { // ory/kratos config OryEnabled bool `default:"false" env:"ORY_ENABLED"` - OryKratosAddr string `default:"kratos:4433" env:"KRATOS_ADDR"` - OryKratosPrivateAddr string `default:"kratos:4434" env:"KRATOS_PRIVATE_ADDR"` + OryKratosPrivateAddr string `default:"kratos-admin" env:"ORY_KRATOS_ADMIN_ADDR"` // Allow people who are not admin to create org CreateOrgEnabled bool `default:"false" env:"CREATE_ORG_ENABLED"` @@ -112,7 +111,6 @@ var ( func initPermissions() { permissions = getAllFiles("erda-configs/permission", permissions) } - func initAuditTemplate() { auditsTemplate = genTempFromFiles("erda-configs/audit/template.json") } diff --git a/modules/core-services/dao/migration.go b/modules/core-services/dao/migration.go new file mode 100644 index 00000000000..c67e8783958 --- /dev/null +++ b/modules/core-services/dao/migration.go @@ -0,0 +1,58 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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" + "strconv" + + "github.com/jinzhu/gorm" + + "github.com/erda-project/erda/modules/core-services/model" +) + +var joinMap = "LEFT OUTER JOIN kratos_uc_userid_mapping on kratos_uc_userid_mapping.id = uc_user.id" + +const noPass = "no pass" + +func (client *DBClient) GetUcUserList() ([]model.User, error) { + var users []model.User + sql := client.Table("uc_user").Joins(joinMap) + if err := sql.Where("password != ? AND password IS NOT NULL AND kratos_uc_userid_mapping.id IS NULL", noPass).Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +func (client *DBClient) InsertMapping(userID, uuid, hash string) error { + return client.Transaction(func(tx *gorm.DB) error { + sql := fmt.Sprintf("UPDATE identity_credentials SET config = JSON_SET(config, '$.hashed_password', ?) WHERE identity_id = ?") + if err := tx.Exec(sql, hash, uuid).Error; err != nil { + return err + } + return client.Table("kratos_uc_userid_mapping").Create(&model.UserIDMapping{ID: userID, UserID: uuid}).Error + }) +} + +func (client *DBClient) GetUcUserID(uuid string) (string, error) { + var users []model.User + if err := client.Table("kratos_uc_userid_mapping").Select("id").Where("user_id = ?", uuid).Find(&users).Error; err != nil { + return "", err + } + if len(users) == 0 { + return "", nil + } + return strconv.FormatInt(users[0].ID, 10), nil +} diff --git a/modules/core-services/dao/mysql.go b/modules/core-services/dao/mysql.go index d49d2b55623..5655ed42502 100644 --- a/modules/core-services/dao/mysql.go +++ b/modules/core-services/dao/mysql.go @@ -79,3 +79,18 @@ func (client *DBClient) BulkInsert(objects interface{}, excludeColumns ...string } return gormbulk.BulkInsert(client.DB, structSlice, BULK_INSERT_CHUNK_SIZE, excludeColumns...) } + +func (db *DBClient) Transaction(f func(tx *gorm.DB) error) error { + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := f(tx); err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} diff --git a/modules/core-services/endpoints/endpoints.go b/modules/core-services/endpoints/endpoints.go index 5a3035a92bf..a9dcc177fa3 100644 --- a/modules/core-services/endpoints/endpoints.go +++ b/modules/core-services/endpoints/endpoints.go @@ -38,6 +38,7 @@ import ( "github.com/erda-project/erda/modules/core-services/services/org" "github.com/erda-project/erda/modules/core-services/services/permission" "github.com/erda-project/erda/modules/core-services/services/project" + "github.com/erda-project/erda/modules/core-services/services/user" "github.com/erda-project/erda/pkg/http/httpserver" "github.com/erda-project/erda/pkg/i18n" "github.com/erda-project/erda/pkg/jsonstore" @@ -71,6 +72,7 @@ type Endpoints struct { audit *audit.Audit errorbox *errorbox.ErrorBox fileSvc *filesvc.FileService + user *user.User } type Option func(*Endpoints) @@ -244,6 +246,12 @@ func WithFileSvc(svc *filesvc.FileService) Option { } } +func WithUserSvc(svc *user.User) Option { + return func(e *Endpoints) { + e.user = svc + } +} + // DBClient 获取db client func (e *Endpoints) DBClient() *dao.DBClient { return e.db @@ -254,6 +262,10 @@ func (e *Endpoints) GetLocale(request *http.Request) *i18n.LocaleResource { return e.bdl.GetLocaleByRequest(request) } +func (e *Endpoints) UserSvc() *user.User { + return e.user +} + // Routes 返回 endpoints 的所有 endpoint 方法,也就是 route. func (e *Endpoints) Routes() []httpserver.Endpoint { return []httpserver.Endpoint{ @@ -433,5 +445,6 @@ func (e *Endpoints) Routes() []httpserver.Endpoint { {Path: "/api/users", Method: http.MethodGet, Handler: e.ListUser}, {Path: "/api/users/current", Method: http.MethodGet, Handler: e.GetCurrentUser}, {Path: "/api/users/actions/search", Method: http.MethodGet, Handler: e.SearchUser}, + {Path: "/api/users/actions/get-uc-user-id", Method: http.MethodGet, Handler: e.GetUcUserID}, } } diff --git a/modules/core-services/endpoints/user.go b/modules/core-services/endpoints/user.go index 79775ba718f..ee1b3800226 100644 --- a/modules/core-services/endpoints/user.go +++ b/modules/core-services/endpoints/user.go @@ -128,6 +128,16 @@ func (e *Endpoints) GetCurrentUser(ctx context.Context, r *http.Request, vars ma return httpserver.OkResp(*convertToUserInfo(user, false)) } +func (e *Endpoints) GetUcUserID(ctx context.Context, r *http.Request, vars map[string]string) ( + httpserver.Responser, error) { + id := r.URL.Query().Get("id") + userID, err := e.db.GetUcUserID(id) + if err != nil { + return apierrors.ErrGetUser.InternalError(err).ToResp(), nil + } + return httpserver.OkResp(userID) +} + func convertToUserInfo(user *ucauth.User, plaintext bool) *apistructs.UserInfo { if !plaintext { user.Phone = desensitize.Mobile(user.Phone) diff --git a/modules/core-services/initialize.go b/modules/core-services/initialize.go index 5a01b307e58..b79187c8123 100644 --- a/modules/core-services/initialize.go +++ b/modules/core-services/initialize.go @@ -45,6 +45,7 @@ import ( "github.com/erda-project/erda/modules/core-services/services/org" "github.com/erda-project/erda/modules/core-services/services/permission" "github.com/erda-project/erda/modules/core-services/services/project" + "github.com/erda-project/erda/modules/core-services/services/user" "github.com/erda-project/erda/modules/core-services/utils" "github.com/erda-project/erda/pkg/discover" "github.com/erda-project/erda/pkg/http/httpclient" @@ -74,6 +75,8 @@ func (p *provider) Initialize() error { bundle.WithCollector(), ) + go ep.UserSvc().UcUserMigration() + server := httpserver.New(conf.ListenAddr()) server.RegisterEndpoint(ep.Routes()) server.WithLocaleLoader(bdl.GetLocaleLoader()) @@ -143,6 +146,7 @@ func (p *provider) initEndpoints() (*endpoints.Endpoints, error) { uc := ucauth.NewUCClient(discover.UC(), conf.UCClientID(), conf.UCClientSecret()) if conf.OryEnabled() { uc = ucauth.NewUCClient(conf.OryKratosPrivateAddr(), conf.OryCompatibleClientID(), conf.OryCompatibleClientSecret()) + uc.SetDBClient(db.DB) } // init bundle @@ -253,6 +257,11 @@ func (p *provider) initEndpoints() (*endpoints.Endpoints, error) { filesvc.WithEtcdClient(etcdStore), ) + user := user.New( + user.WithDBClient(db), + user.WithUCClient(uc), + ) + // queryStringDecoder queryStringDecoder := schema.NewDecoder() queryStringDecoder.IgnoreUnknownKeys(true) @@ -282,6 +291,7 @@ func (p *provider) initEndpoints() (*endpoints.Endpoints, error) { endpoints.WithAudit(audit), endpoints.WithErrorBox(errorBox), endpoints.WithFileSvc(fileSvc), + endpoints.WithUserSvc(user), ) return ep, nil diff --git a/modules/core-services/model/uc_migration.go b/modules/core-services/model/uc_migration.go new file mode 100644 index 00000000000..b270093ffc0 --- /dev/null +++ b/modules/core-services/model/uc_migration.go @@ -0,0 +1,34 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 model + +type User struct { + BaseModel + Avatar string + Username string + Nickname string + Mobile string + Email string + Password string +} + +type Config struct { + HashedPassword string `json:"hashed_password"` +} + +type UserIDMapping struct { + ID string + UserID string +} diff --git a/modules/core-services/services/user/user.go b/modules/core-services/services/user/user.go new file mode 100644 index 00000000000..0796ca51cc2 --- /dev/null +++ b/modules/core-services/services/user/user.go @@ -0,0 +1,100 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 user + +import ( + "strconv" + "time" + + "github.com/sirupsen/logrus" + + "github.com/erda-project/erda/modules/core-services/conf" + "github.com/erda-project/erda/modules/core-services/dao" + "github.com/erda-project/erda/pkg/ucauth" +) + +type User struct { + db *dao.DBClient + uc *ucauth.UCClient +} + +type Option func(*User) + +func New(options ...Option) *User { + o := &User{} + for _, op := range options { + op(o) + } + return o +} + +func WithDBClient(db *dao.DBClient) Option { + return func(o *User) { + o.db = db + } +} + +func WithUCClient(uc *ucauth.UCClient) Option { + return func(o *User) { + o.uc = uc + } +} + +func (m *User) MigrateUser() error { + users, err := m.db.GetUcUserList() + if err != nil { + return err + } + for _, u := range users { + req := ucauth.OryKratosCreateIdentitiyRequest{ + SchemaID: "default", + Traits: ucauth.OryKratosIdentityTraits{ + Email: u.Email, + Name: u.Username, + Nick: u.Nickname, + Phone: u.Mobile, + Avatar: u.Avatar, + }, + } + uuid, err := m.uc.UserMigration(req) + if err != nil { + logrus.Errorf("fail to migrate user: %v, err: %v", u.ID, err) + continue + } + if err := m.db.InsertMapping(strconv.FormatInt(u.ID, 10), uuid, u.Password); err != nil { + return err + } + logrus.Infof("migrate user %v to krataos user %v successfully", u.ID, uuid) + } + return nil +} + +func (m *User) UcUserMigration() { + if !conf.OryEnabled() { + return + } + ticker := time.NewTicker(time.Second * 10) + for { + select { + case <-ticker.C: + if m.uc.MigrationReady() { + if err := m.MigrateUser(); err != nil { + logrus.Errorf("fail to migrate user, %v", err) + } + return + } + } + } +} diff --git a/modules/core/openapi-ng/auth/ory-kratos/login.go b/modules/core/openapi-ng/auth/ory-kratos/login.go index 032faaf77e9..bffaf68884d 100644 --- a/modules/core/openapi-ng/auth/ory-kratos/login.go +++ b/modules/core/openapi-ng/auth/ory-kratos/login.go @@ -24,7 +24,7 @@ func (p *provider) LoginURL(rw http.ResponseWriter, r *http.Request) { common.ResponseJSON(rw, &struct { URL string `json:"url"` }{ - URL: "/uc/auth/login", + URL: "/uc/login", }) } @@ -32,6 +32,6 @@ func (p *provider) Logout(rw http.ResponseWriter, r *http.Request) { common.ResponseJSON(rw, &struct { URL string `json:"url"` }{ - URL: "/.ory/kratos/public/self-service/browser/flows/logout", + URL: "/uc/login", }) } diff --git a/modules/core/openapi-ng/auth/ory-kratos/user.go b/modules/core/openapi-ng/auth/ory-kratos/user.go index 40c426c1330..330b64fea62 100644 --- a/modules/core/openapi-ng/auth/ory-kratos/user.go +++ b/modules/core/openapi-ng/auth/ory-kratos/user.go @@ -15,11 +15,11 @@ package orykratos import ( - "bytes" - "encoding/json" - "fmt" + "context" "net/http" + "github.com/pkg/errors" + "github.com/erda-project/erda/modules/core/openapi-ng" "github.com/erda-project/erda/modules/core/openapi-ng/common" "github.com/erda-project/erda/pkg/http/httpclient" @@ -35,25 +35,29 @@ func (p *provider) addUserInfoAPI(router openapi.Interface) { func (p *provider) GetUserInfo(rw http.ResponseWriter, r *http.Request) { sessionID := p.getSession(r) - + if len(sessionID) > 0 { + r = r.WithContext(context.WithValue(r.Context(), "session", sessionID)) + } info, err := p.getUserInfo(sessionID) if err != nil { - http.Error(rw, err.Error(), http.StatusBadGateway) + http.Error(rw, err.Error(), http.StatusUnauthorized) return } + common.ResponseJSON(rw, &struct { Success bool `json:"success"` Data interface{} `json:"data"` }{ Success: true, Data: map[string]interface{}{ - "id": info.ID, - "name": info.UserName, - "nick": info.NickName, - "avatar": info.AvatarUrl, - "phone": info.Phone, - "email": info.Email, - "token": info.Token, + "id": info.ID, + "name": info.UserName, + "nick": info.NickName, + "avatar": info.AvatarUrl, + "phone": info.Phone, + "email": info.Email, + "token": info.Token, + "userType": "new", }, }) } @@ -67,7 +71,7 @@ func (p *provider) getSession(r *http.Request) string { } func (p *provider) getUserInfo(sessionID string) (*ucauth.UserInfo, error) { - var buf bytes.Buffer + var s OryKratosSession r, err := httpclient.New(httpclient.WithCompleteRedirect()). Get(p.Cfg.OryKratosAddr). Cookie(&http.Cookie{ @@ -75,18 +79,23 @@ func (p *provider) getUserInfo(sessionID string) (*ucauth.UserInfo, error) { Value: sessionID, }). Path("/sessions/whoami"). - Do().Body(&buf) + Do().JSON(&s) if err != nil { return nil, err } if !r.IsOK() { - return nil, fmt.Errorf("bad session") + return nil, errors.Errorf("get kratos user info error, statusCode: %d", r.StatusCode()) } - var i OryKratosSession - if err := json.Unmarshal(buf.Bytes(), &i); err != nil { + + info := identityToUserInfo(s.Identity) + ucUserID, err := p.bundle.GetUcUserID(string(info.ID)) + if err != nil { return nil, err } - return identityToUserInfo(i.Identity), nil + if ucUserID != "" { + info.ID = ucauth.USERID(ucUserID) + } + return info, nil } type OryKratosSession struct { @@ -102,8 +111,10 @@ type OryKratosIdentity struct { } type OryKratosIdentityTraits struct { - Email string `json:"email"` - Name OryKratosIdentityTraitsName `json:"name"` + Email string `json:"email"` + Name string `json:"username"` + Nick string `json:"nickname"` + Phone string `json:"phone"` } type OryKratosIdentityTraitsName struct { @@ -119,8 +130,10 @@ func nameConversion(name OryKratosIdentityTraitsName) string { func identityToUser(i OryKratosIdentity) ucauth.User { return ucauth.User{ ID: string(i.ID), - Nick: nameConversion(i.Traits.Name), + Name: i.Traits.Name, + Nick: i.Traits.Nick, Email: i.Traits.Email, + Phone: i.Traits.Phone, } } diff --git a/modules/core/openapi-ng/interceptors/user-info/user_info.go b/modules/core/openapi-ng/interceptors/user-info/user_info.go index e6ba2e7b5fe..cbdb61d9236 100644 --- a/modules/core/openapi-ng/interceptors/user-info/user_info.go +++ b/modules/core/openapi-ng/interceptors/user-info/user_info.go @@ -49,6 +49,11 @@ func (p *provider) Init(ctx servicehub.Context) (err error) { p.uc = ucauth.NewUCClient(discover.UC(), conf.UCClientID(), conf.UCClientSecret()) if conf.OryEnabled() { p.uc = ucauth.NewUCClient(conf.OryKratosPrivateAddr(), conf.OryCompatibleClientID(), conf.OryCompatibleClientSecret()) + db, err := ucauth.NewDB() + if err != nil { + return err + } + p.uc.SetDBClient(db) } return nil } diff --git a/modules/dop/conf/conf.go b/modules/dop/conf/conf.go index 3d6a881c08e..8ffa6027dfe 100644 --- a/modules/dop/conf/conf.go +++ b/modules/dop/conf/conf.go @@ -49,8 +49,7 @@ type Conf struct { // ory/kratos config OryEnabled bool `default:"false" env:"ORY_ENABLED"` - OryKratosAddr string `default:"kratos:4433" env:"KRATOS_ADDR"` - OryKratosPrivateAddr string `default:"kratos:4434" env:"KRATOS_PRIVATE_ADDR"` + OryKratosPrivateAddr string `default:"kratos-admin" env:"ORY_KRATOS_ADMIN_ADDR"` CentralNexusPublicURL string `env:"NEXUS_PUBLIC_URL" required:"true"` CentralNexusAddr string `env:"NEXUS_ADDR" required:"true"` diff --git a/modules/dop/initialize.go b/modules/dop/initialize.go index 3577044c56d..93c9a4607c1 100644 --- a/modules/dop/initialize.go +++ b/modules/dop/initialize.go @@ -326,6 +326,7 @@ func (p *provider) initEndpoints(db *dao.DBClient) (*endpoints.Endpoints, error) uc := ucauth.NewUCClient(discover.UC(), conf.UCClientID(), conf.UCClientSecret()) if conf.OryEnabled() { uc = ucauth.NewUCClient(conf.OryKratosPrivateAddr(), conf.OryCompatibleClientID(), conf.OryCompatibleClientSecret()) + uc.SetDBClient(db.DB) } // init ticket service diff --git a/modules/gittar/conf/conf.go b/modules/gittar/conf/conf.go index 4f1de1b0477..9329691a54b 100644 --- a/modules/gittar/conf/conf.go +++ b/modules/gittar/conf/conf.go @@ -51,8 +51,8 @@ type Conf struct { // ory/kratos config OryEnabled bool `default:"false" env:"ORY_ENABLED"` - OryKratosAddr string `default:"kratos:4433" env:"KRATOS_ADDR"` - OryKratosPrivateAddr string `default:"kratos:4434" env:"KRATOS_PRIVATE_ADDR"` + OryKratosAddr string `default:"kratos-public" env:"ORY_KRATOS_ADDR"` + OryKratosPrivateAddr string `default:"kratos-admin" env:"ORY_KRATOS_ADMIN_ADDR"` } var cfg Conf diff --git a/modules/openapi/api/apis/uc/uc_user_create.go b/modules/openapi/api/apis/uc/uc_user_create.go index f6806fc293f..48b0a0eb87d 100644 --- a/modules/openapi/api/apis/uc/uc_user_create.go +++ b/modules/openapi/api/apis/uc/uc_user_create.go @@ -97,6 +97,23 @@ func createUsers(w http.ResponseWriter, r *http.Request) { } func handleCreateUsers(req *apistructs.UserCreateRequest, operatorID string, token ucauth.OAuthToken) error { + if token.TokenType == ucauth.OryCompatibleClientId { + for _, user := range req.Users { + if err := ucauth.CreateUser(ucauth.OryKratosRegistrationRequest{ + Method: "password", + Password: user.Password, + Traits: ucauth.OryKratosIdentityTraits{ + Email: user.Email, + Name: user.Name, + Nick: user.Nick, + Phone: user.Phone, + }, + }); err != nil { + return err + } + } + return nil + } var resp struct { Success bool `json:"success"` Error string `json:"error"` diff --git a/modules/openapi/api/apis/uc/uc_user_freeze.go b/modules/openapi/api/apis/uc/uc_user_freeze.go index b9a163a12a5..6a7db931a27 100644 --- a/modules/openapi/api/apis/uc/uc_user_freeze.go +++ b/modules/openapi/api/apis/uc/uc_user_freeze.go @@ -86,6 +86,10 @@ func freezeUser(w http.ResponseWriter, r *http.Request) { } func handleFreezeUser(userID, operatorID string, token ucauth.OAuthToken) error { + if token.TokenType == ucauth.OryCompatibleClientId { + return ucauth.ChangeUserState(token.AccessToken, userID, ucauth.UserInActive) + } + var resp struct { Success bool `json:"success"` Result bool `json:"result"` diff --git a/modules/openapi/api/apis/uc/uc_user_list_login_method.go b/modules/openapi/api/apis/uc/uc_user_list_login_method.go index 3b14a752ca6..9a6c5e35555 100644 --- a/modules/openapi/api/apis/uc/uc_user_list_login_method.go +++ b/modules/openapi/api/apis/uc/uc_user_list_login_method.go @@ -123,6 +123,12 @@ type listLoginTypeResult struct { } func handleListLoginMethod(token ucauth.OAuthToken) (*listLoginTypeResult, error) { + // TODO: password oidc + if token.TokenType == ucauth.OryCompatibleClientId { + return &listLoginTypeResult{ + RegistryType: []string{"email"}, + }, nil + } var resp struct { Success bool `json:"success"` Result *listLoginTypeResult `json:"result"` diff --git a/modules/openapi/api/apis/uc/uc_user_unfreeze.go b/modules/openapi/api/apis/uc/uc_user_unfreeze.go index 4f0a86fbb9a..4f2ee9dfa1a 100644 --- a/modules/openapi/api/apis/uc/uc_user_unfreeze.go +++ b/modules/openapi/api/apis/uc/uc_user_unfreeze.go @@ -87,6 +87,10 @@ func unfreezeUser(w http.ResponseWriter, r *http.Request) { } func handleUnfreezeUser(userID, operatorID string, token ucauth.OAuthToken) error { + if token.TokenType == ucauth.OryCompatibleClientId { + return ucauth.ChangeUserState(token.AccessToken, userID, ucauth.UserActive) + } + var resp struct { Success bool `json:"success"` Result bool `json:"result"` diff --git a/modules/openapi/api/apis/uc/uc_user_update_userinfo.go b/modules/openapi/api/apis/uc/uc_user_update_userinfo.go index c4f1654cdb9..0d99e89611e 100644 --- a/modules/openapi/api/apis/uc/uc_user_update_userinfo.go +++ b/modules/openapi/api/apis/uc/uc_user_update_userinfo.go @@ -128,6 +128,22 @@ type ucUpdateUserInfoReq struct { } func handleUpdateUserInfo(req *apistructs.UserUpdateInfoRequset, operatorID string, token ucauth.OAuthToken) error { + if token.TokenType == ucauth.OryCompatibleClientId { + updateReq := ucauth.OryKratosUpdateIdentitiyRequest{ + State: "active", + Traits: ucauth.OryKratosIdentityTraits{ + Email: req.Email, + Nick: req.Nick, + Name: req.Name, + Phone: req.Mobile, + }, + } + if err := ucauth.UpdateIdentity(token.AccessToken, req.UserID, updateReq); err != nil { + return err + } + return nil + } + var resp struct { Success bool `json:"success"` Error string `json:"error"` diff --git a/modules/openapi/auth/token.go b/modules/openapi/auth/token.go index bca66e4ea98..4801e999a9d 100644 --- a/modules/openapi/auth/token.go +++ b/modules/openapi/auth/token.go @@ -42,6 +42,7 @@ var once sync.Once // 获取 dice 自己的token func GetDiceClientToken() (ucauth.OAuthToken, error) { + // TODO kratos will not use it if conf.OryEnabled() { return ucauth.OAuthToken{ AccessToken: conf.OryKratosPrivateAddr(), diff --git a/modules/openapi/conf/conf.go b/modules/openapi/conf/conf.go index a0e67cb71dc..ccb2d3fcfc9 100644 --- a/modules/openapi/conf/conf.go +++ b/modules/openapi/conf/conf.go @@ -58,11 +58,19 @@ type Conf struct { // ory/kratos config OryEnabled bool `default:"false" env:"ORY_ENABLED"` - OryKratosAddr string `default:"kratos:4433" env:"KRATOS_ADDR"` - OryKratosPrivateAddr string `default:"kratos:4434" env:"KRATOS_PRIVATE_ADDR"` + OryKratosAddr string `default:"kratos-public" env:"ORY_KRATOS_ADDR"` + OryKratosPrivateAddr string `default:"kratos-admin" env:"ORY_KRATOS_ADMIN_ADDR"` // Allow people who are not admin to create org CreateOrgEnabled bool `default:"false" env:"CREATE_ORG_ENABLED"` + + MySQLHost string `env:"MYSQL_HOST"` + MySQLPort string `env:"MYSQL_PORT"` + MySQLUsername string `env:"MYSQL_USERNAME"` + MySQLPassword string `env:"MYSQL_PASSWORD"` + MySQLDatabase string `env:"MYSQL_DATABASE"` + MySQLLoc string `env:"MYSQL_LOC" default:"Local"` + Debug bool `env:"DEBUG" default:"false"` } var cfg Conf @@ -168,7 +176,7 @@ func OryKratosPrivateAddr() string { } func OryLoginURL() string { - return "/uc/auth/login" + return "/uc/login" } func OryLogoutURL() string { @@ -203,7 +211,35 @@ func CreateOrgEnabled() bool { return cfg.CreateOrgEnabled } -// GetDomain get a domian by request host +func MySQLHost() string { + return cfg.MySQLHost +} + +func MySQLPort() string { + return cfg.MySQLPort +} + +func MySQLUsername() string { + return cfg.MySQLUsername +} + +func MySQLPassword() string { + return cfg.MySQLPassword +} + +func MySQLDatabase() string { + return cfg.MySQLDatabase +} + +func MySQLLoc() string { + return cfg.MySQLLoc +} + +func Debug() bool { + return cfg.Debug +} + +// GetDomain get a domain by request host func GetDomain(host, confDomain string) (string, error) { if strings.Contains(host, ":") { host = strings.SplitN(host, ":", -1)[0] diff --git a/modules/openapi/hooks/posthandle/injectuserinfo.go b/modules/openapi/hooks/posthandle/injectuserinfo.go index b63de188c78..f56e5040194 100644 --- a/modules/openapi/hooks/posthandle/injectuserinfo.go +++ b/modules/openapi/hooks/posthandle/injectuserinfo.go @@ -22,6 +22,8 @@ import ( "net/http" "sync" + "github.com/sirupsen/logrus" + "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/modules/openapi/conf" "github.com/erda-project/erda/pkg/desensitize" @@ -92,6 +94,12 @@ func GetUsers(IDs []string, needDesensitize bool) (map[string]apistructs.UserInf uc = ucauth.NewUCClient(discover.UC(), conf.UCClientID(), conf.UCClientSecret()) if conf.OryEnabled() { uc = ucauth.NewUCClient(conf.OryKratosPrivateAddr(), conf.OryCompatibleClientID(), conf.OryCompatibleClientSecret()) + db, err := ucauth.NewDB() + if err != nil { + logrus.Errorf("fail to initial db err: %v", err) + return + } + uc.SetDBClient(db) } }) diff --git a/pkg/strutil/strutil.go b/pkg/strutil/strutil.go index 6400bd15098..9aadd2793fa 100644 --- a/pkg/strutil/strutil.go +++ b/pkg/strutil/strutil.go @@ -620,3 +620,7 @@ func FlatErrors(errs []error, sep string) error { } return fmt.Errorf("%s", Join(errMsgs, sep, true)) } + +func ContainsOrEmpty(source, target string) bool { + return target == "" || strings.Contains(source, target) +} diff --git a/pkg/strutil/strutil_test.go b/pkg/strutil/strutil_test.go index 6c40626edb2..90ecd968a62 100644 --- a/pkg/strutil/strutil_test.go +++ b/pkg/strutil/strutil_test.go @@ -173,3 +173,39 @@ func TestReverseSlice(t *testing.T) { assert.Equal(t, "s2", ss[1]) assert.Equal(t, "s1", ss[2]) } + +func TestContainsOrEmpty(t *testing.T) { + type args struct { + source string + target string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "empty", + args: args{ + source: "user", + target: "", + }, + want: true, + }, + { + name: "match", + args: args{ + source: "user", + target: "to", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsOrEmpty(tt.args.source, tt.args.target); got != tt.want { + t.Errorf("ContainsOrEmpty() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ucauth/identity.go b/pkg/ucauth/identity.go new file mode 100644 index 00000000000..0d5a995e39b --- /dev/null +++ b/pkg/ucauth/identity.go @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/erda-project/erda/pkg/http/httpclient" +) + +func getIdentity(kratosPrivateAddr string, userID string) (*OryKratosIdentity, error) { + var body bytes.Buffer + r, err := httpclient.New(httpclient.WithCompleteRedirect()). + Get(kratosPrivateAddr). + Path("/identities/" + userID). + Do().Body(&body) + if err != nil { + return nil, err + } + if !r.IsOK() { + return nil, fmt.Errorf("get identity: statuscode: %d, body: %v", r.StatusCode(), body.String()) + } + var i OryKratosIdentity + if err := json.Unmarshal(body.Bytes(), &i); err != nil { + return nil, err + } + return &i, nil +} diff --git a/pkg/ucauth/identity_paging.go b/pkg/ucauth/identity_paging.go new file mode 100644 index 00000000000..3f9737d41fe --- /dev/null +++ b/pkg/ucauth/identity_paging.go @@ -0,0 +1,97 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/erda-project/erda/apistructs" + "github.com/erda-project/erda/pkg/http/httpclient" + "github.com/erda-project/erda/pkg/strutil" +) + +func getIdentityPage(kratosPrivateAddr string, page, perPage int) ([]*OryKratosIdentity, error) { + var body bytes.Buffer + r, err := httpclient.New(httpclient.WithCompleteRedirect()). + Get(kratosPrivateAddr). + Path("/identities"). + Param("page", fmt.Sprintf("%d", page)). + Param("per_page", fmt.Sprintf("%d", perPage)). + Do().Body(&body) + if err != nil { + return nil, err + } + if !r.IsOK() { + return nil, fmt.Errorf("get identity page: statuscode: %d, body: %v", r.StatusCode(), body.String()) + } + + var i []*OryKratosIdentity + if err := json.Unmarshal(body.Bytes(), &i); err != nil { + return nil, err + } + return i, nil +} + +func getUserList(kratosPrivateAddr string, req *apistructs.UserPagingRequest) ([]User, int, error) { + if req.PageNo < 1 || req.PageSize < 1 { + return nil, 0, fmt.Errorf("invalid pagination parameter") + } + var identities []*OryKratosIdentity + cnt := 0 + p := 1 + size := 100 + for { + ul, err := getIdentityPage(kratosPrivateAddr, p, size) + if err != nil { + return nil, 0, err + } + if len(ul) == 0 { + break + } + for _, u := range ul { + if strutil.ContainsOrEmpty(u.Traits.Name, req.Name) && strutil.ContainsOrEmpty(u.Traits.Nick, req.Nick) && + strutil.ContainsOrEmpty(u.Traits.Email, req.Email) && strutil.ContainsOrEmpty(u.Traits.Phone, req.Phone) && + (req.Locked == nil || req.Locked != nil && u.State == oryKratosStateMap[*req.Locked]) { + identities = append(identities, u) + cnt++ + } + } + p++ + if p > 100 { + break + } + } + + var users []User + for _, u := range paginate(identities, req.PageNo, req.PageSize) { + users = append(users, identityToUser(*u)) + } + + return users, len(identities), nil +} + +func paginate(i []*OryKratosIdentity, pageNo int, pageSize int) []*OryKratosIdentity { + start := (pageNo - 1) * pageSize + if start > len(i) { + return nil + } + end := start + pageSize + if end > len(i) { + return i[start:] + } + return i[start:end] +} diff --git a/pkg/ucauth/identity_paging_test.go b/pkg/ucauth/identity_paging_test.go new file mode 100644 index 00000000000..7055db25c99 --- /dev/null +++ b/pkg/ucauth/identity_paging_test.go @@ -0,0 +1,224 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "reflect" + "testing" + + "bou.ke/monkey" + + "github.com/erda-project/erda/apistructs" +) + +var isLock = 0 +var data = []*OryKratosIdentity{ + { + ID: "1", + Traits: OryKratosIdentityTraits{ + Name: "a1", + }, + State: UserActive, + }, + { + ID: "2", + Traits: OryKratosIdentityTraits{ + Name: "b2", + }, + State: UserActive, + }, + { + ID: "3", + Traits: OryKratosIdentityTraits{ + Name: "a3", + }, + State: UserInActive, + }, +} + +func Test_getUserList(t *testing.T) { + type args struct { + kratosPrivateAddr string + req *apistructs.UserPagingRequest + } + tests := []struct { + name string + args args + want []User + want1 int + wantErr bool + }{ + { + name: "invalid input", + args: args{ + req: &apistructs.UserPagingRequest{ + PageNo: 0, + }, + }, + want: nil, + want1: 0, + wantErr: true, + }, + { + name: "search", + args: args{ + req: &apistructs.UserPagingRequest{ + PageNo: 1, + PageSize: 10, + Name: "a", + }, + }, + want: []User{ + { + ID: "1", + Name: "a1", + State: UserActive, + }, + { + ID: "3", + Name: "a3", + State: UserInActive, + }, + }, + want1: 2, + wantErr: false, + }, + { + name: "search active", + args: args{ + req: &apistructs.UserPagingRequest{ + PageNo: 1, + PageSize: 10, + Name: "a", + Locked: &isLock, + }, + }, + want: []User{ + { + ID: "1", + Name: "a1", + State: UserActive, + }, + }, + want1: 1, + wantErr: false, + }, + } + + monkey.Patch(getIdentityPage, + func(kratosPrivateAddr string, page, perPage int) ([]*OryKratosIdentity, error) { + if page == 2 { + return nil, nil + } + return data, nil + }) + defer monkey.UnpatchAll() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := getUserList(tt.args.kratosPrivateAddr, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("getUserList() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getUserList() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("getUserList() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_paginate(t *testing.T) { + type args struct { + i []*OryKratosIdentity + pageNo int + pageSize int + } + tests := []struct { + name string + args args + want []*OryKratosIdentity + }{ + { + name: "test", + args: args{ + i: data, + pageNo: 1, + pageSize: 15, + }, + want: data, + }, + { + name: "empty page", + args: args{ + i: data, + pageNo: 3, + pageSize: 2, + }, + want: nil, + }, + { + name: "first page", + args: args{ + i: data, + pageNo: 1, + pageSize: 2, + }, + want: []*OryKratosIdentity{ + { + ID: "1", + Traits: OryKratosIdentityTraits{ + Name: "a1", + }, + State: UserActive, + }, + { + ID: "2", + Traits: OryKratosIdentityTraits{ + Name: "b2", + }, + State: UserActive, + }, + }, + }, + { + name: "second page", + args: args{ + i: data, + pageNo: 2, + pageSize: 2, + }, + want: []*OryKratosIdentity{ + { + ID: "3", + Traits: OryKratosIdentityTraits{ + Name: "a3", + }, + State: UserInActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := paginate(tt.args.i, tt.args.pageNo, tt.args.pageSize); !reflect.DeepEqual(got, tt.want) { + t.Errorf("paginate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ucauth/identity_update.go b/pkg/ucauth/identity_update.go new file mode 100644 index 00000000000..11ba0229d73 --- /dev/null +++ b/pkg/ucauth/identity_update.go @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "bytes" + "fmt" + + "github.com/erda-project/erda/pkg/http/httpclient" +) + +func UpdateIdentity(kratosPrivateAddr string, userID string, req OryKratosUpdateIdentitiyRequest) error { + var body bytes.Buffer + r, err := httpclient.New(httpclient.WithCompleteRedirect()). + Put(kratosPrivateAddr). + Path("/identities/" + userID). + JSONBody(req). + Do().Body(&body) + if err != nil { + return err + } + if !r.IsOK() { + return fmt.Errorf("update identity: statuscode: %d, body: %v", r.StatusCode(), body.String()) + } + return nil +} + +func ChangeUserState(kratosPrivateAddr string, userID string, state string) error { + i, err := getIdentity(kratosPrivateAddr, userID) + if err != nil { + return err + } + return UpdateIdentity(kratosPrivateAddr, userID, OryKratosUpdateIdentitiyRequest{ + State: state, + Traits: i.Traits, + }) +} diff --git a/pkg/ucauth/kratos.go b/pkg/ucauth/kratos.go index 6f34fa9de10..a018436d87e 100644 --- a/pkg/ucauth/kratos.go +++ b/pkg/ucauth/kratos.go @@ -15,80 +15,16 @@ package ucauth import ( - "bytes" - "encoding/json" "fmt" "net/http" "strings" + "github.com/erda-project/erda/modules/openapi/conf" "github.com/erda-project/erda/pkg/http/httpclient" ) -type OryKratosSession struct { - ID string `json:"id"` - Active bool `json:"active"` - Identity OryKratosIdentity `json:"identity"` -} - -type OryKratosIdentity struct { - ID USERID `json:"id"` - SchemaID string `json:"schema_id"` - Traits OryKratosIdentityTraits `json:"traits"` -} - -type OryKratosIdentityTraits struct { - Email string `json:"email"` - Name OryKratosIdentityTraitsName `json:"name"` -} - -type OryKratosIdentityTraitsName struct { - First string `json:"first"` - Last string `json:"last"` -} - -func nameConversion(name OryKratosIdentityTraitsName) string { - // TODO: eastern name vs western name - return name.Last + name.First -} - -func identityToUser(i OryKratosIdentity) User { - return User{ - ID: string(i.ID), - Nick: nameConversion(i.Traits.Name), - Email: i.Traits.Email, - } -} - -func identityToUserInfo(i OryKratosIdentity) UserInfo { - return userToUserInfo(identityToUser(i)) -} - -func userToUserInfo(u User) UserInfo { - return UserInfo{ - ID: USERID(u.ID), - Email: u.Email, - Phone: u.Phone, - AvatarUrl: u.AvatarURL, - UserName: u.Name, - NickName: u.Nick, - Enabled: true, - } -} - -func userToUserInPaging(u User) userInPaging { - return userInPaging{ - Id: u.ID, - Avatar: u.AvatarURL, - Username: u.Name, - Nickname: u.Nick, - Mobile: u.Phone, - Email: u.Email, - Enabled: true, - } -} - func whoami(kratosPublicAddr string, sessionID string) (UserInfo, error) { - var buf bytes.Buffer + var s OryKratosSession r, err := httpclient.New(httpclient.WithCompleteRedirect()). Get(kratosPublicAddr). Cookie(&http.Cookie{ @@ -96,38 +32,22 @@ func whoami(kratosPublicAddr string, sessionID string) (UserInfo, error) { Value: sessionID, }). Path("/sessions/whoami"). - Do().Body(&buf) + Do().JSON(&s) if err != nil { return UserInfo{}, err } if !r.IsOK() { return UserInfo{}, fmt.Errorf("bad session") } - var i OryKratosSession - if err := json.Unmarshal(buf.Bytes(), &i); err != nil { - return UserInfo{}, err - } - //return r.ResponseHeader("X-Kratos-Authenticated-Identity-Id"), nil - return identityToUserInfo(i.Identity), nil + return identityToUserInfo(s.Identity), nil } func getUserByID(kratosPrivateAddr string, userID string) (*User, error) { - var buf bytes.Buffer - r, err := httpclient.New(httpclient.WithCompleteRedirect()). - Get(kratosPrivateAddr). - Path("/identities/" + userID). - Do().Body(&buf) + i, err := getIdentity(kratosPrivateAddr, userID) if err != nil { return nil, err } - if !r.IsOK() { - return nil, fmt.Errorf("bad session") - } - var i OryKratosIdentity - if err := json.Unmarshal(buf.Bytes(), &i); err != nil { - return nil, err - } - u := identityToUser(i) + u := identityToUser(*i) return &u, nil } @@ -143,9 +63,21 @@ func getUserByIDs(kratosPrivateAddr string, userIDs []string) ([]User, error) { return users, nil } +func getUserPage(kratosPrivateAddr string, page, perPage int) ([]User, error) { + i, err := getIdentityPage(kratosPrivateAddr, page, perPage) + if err != nil { + return nil, err + } + var users []User + for _, u := range i { + users = append(users, identityToUser(*u)) + } + return users, nil +} + func getUserByKey(kratosPrivateAddr string, key string) ([]User, error) { p := 1 - size := 1000 + size := 100 cnt := 0 var users []User for { @@ -153,44 +85,47 @@ func getUserByKey(kratosPrivateAddr string, key string) ([]User, error) { if err != nil { return nil, err } + if len(ul) == 0 { + return users, nil + } for _, u := range ul { - if strings.Contains(u.Name, key) || strings.Contains(u.Email, key) { + if u.State == UserActive && (strings.Contains(u.Name, key) || strings.Contains(u.Nick, key) || strings.Contains(u.Email, key)) { users = append(users, u) cnt++ } } - if cnt >= 10 { - return users, nil - } p++ if p > 100 { return users, nil } } - return nil, nil } -func getUserPage(kratosPrivateAddr string, page, perPage int) ([]User, error) { - var buf bytes.Buffer +func CreateUser(req OryKratosRegistrationRequest) error { + var rsp OryKratosFlowResponse r, err := httpclient.New(httpclient.WithCompleteRedirect()). - Get(kratosPrivateAddr). - Path("/identities"). - Param("page", fmt.Sprintf("%d", page)). - Param("per_page", fmt.Sprintf("%d", perPage)). - Do().Body(&buf) + Get(conf.OryKratosAddr()). + Path("/self-service/registration/api"). + Do().JSON(&rsp) if err != nil { - return nil, err + return err } if !r.IsOK() { - return nil, fmt.Errorf("bad session") + return fmt.Errorf("bad session") } - var i []OryKratosIdentity - if err := json.Unmarshal(buf.Bytes(), &i); err != nil { - return nil, err + + var register OryKratosRegistrationResponse + r, err = httpclient.New(httpclient.WithCompleteRedirect()). + Post(conf.OryKratosAddr()). + Path("/self-service/registration"). + Param("flow", rsp.ID). + JSONBody(req). + Do().JSON(®ister) + if err != nil { + return err } - var users []User - for _, u := range i { - users = append(users, identityToUser(u)) + if !r.IsOK() { + return fmt.Errorf("bad session") } - return users, nil + return nil } diff --git a/pkg/ucauth/kratos/identity.schema.json b/pkg/ucauth/kratos/identity.schema.json new file mode 100644 index 00000000000..33804047fdb --- /dev/null +++ b/pkg/ucauth/kratos/identity.schema.json @@ -0,0 +1,55 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/pkg/ucauth/kratos/kratos.yml b/pkg/ucauth/kratos/kratos.yml new file mode 100644 index 00000000000..814fc4d8f05 --- /dev/null +++ b/pkg/ucauth/kratos/kratos.yml @@ -0,0 +1,81 @@ +version: v0.7.1-alpha.1 + +dsn: memory + +serve: + public: + base_url: /api/uc + cors: + enabled: true + admin: + base_url: http://kratos-admin:4434/ + +selfservice: + # we are not using it, default value for config source validation, can be replaced by env SELFSERVICE_DEFAULT_BROWSER_RETURN_URL. + default_browser_return_url: http://one.erda.local + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://one.erda.local/uc/error + + settings: + ui_url: http://one.erda.local/uc/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://one.erda.local/uc/recovery + + verification: + enabled: true + ui_url: http://one.erda.local/uc/verify + after: + default_browser_return_url: / + + logout: + after: + default_browser_return_url: / + + login: + ui_url: http://one.erda.local/uc/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://one.erda.local/uc/registration + # issue session after registration + after: + password: + hooks: + - + hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 128MB + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&legacy_ssl=true diff --git a/pkg/ucauth/kratos_test.go b/pkg/ucauth/kratos_test.go new file mode 100644 index 00000000000..a826094d598 --- /dev/null +++ b/pkg/ucauth/kratos_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "reflect" + "testing" + + "bou.ke/monkey" +) + +func Test_getUserByKey(t *testing.T) { + type args struct { + kratosPrivateAddr string + key string + } + tests := []struct { + name string + args args + want []User + wantErr bool + }{ + { + name: "test", + args: args{ + key: "a", + }, + want: []User{ + { + ID: "1", + Name: "a1", + State: UserActive, + }, + { + ID: "2", + Nick: "a2", + State: UserActive, + }, + }, + wantErr: false, + }, + } + + monkey.Patch(getUserPage, + func(kratosPrivateAddr string, page, perPage int) ([]User, error) { + if page == 2 { + return nil, nil + } + return []User{ + { + ID: "1", + Name: "a1", + State: UserActive, + }, + { + ID: "2", + Nick: "a2", + State: UserActive, + }, + { + ID: "3", + Email: "abc@gmail.com", + State: UserInActive, + }, + }, nil + }) + defer monkey.UnpatchAll() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getUserByKey(tt.args.kratosPrivateAddr, tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("getUserByKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getUserByKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/ucauth/model.go b/pkg/ucauth/model.go new file mode 100644 index 00000000000..fdd3cfdbca7 --- /dev/null +++ b/pkg/ucauth/model.go @@ -0,0 +1,121 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +type OryKratosSession struct { + ID string `json:"id"` + Active bool `json:"active"` + Identity OryKratosIdentity `json:"identity"` +} + +type OryKratosIdentity struct { + ID USERID `json:"id"` + SchemaID string `json:"schema_id"` + State string `json:"state"` + Traits OryKratosIdentityTraits `json:"traits"` +} + +type OryKratosIdentityTraits struct { + Email string `json:"email"` + Name string `json:"username"` + Nick string `json:"nickname"` + Phone string `json:"phone"` + Avatar string `json:"avatar"` +} + +type OryKratosFlowResponse struct { + ID string `json:"id"` + UI OryKratosFlowResponseUI `json:"ui"` +} + +type OryKratosReadyResponse struct { + Status string `json:"status"` +} + +type OryKratosFlowResponseUI struct { + Action string `json:"action"` +} + +type OryKratosRegistrationRequest struct { + Traits OryKratosIdentityTraits `json:"traits"` + Password string `json:"password"` + Method string `json:"method"` +} + +type OryKratosRegistrationResponse struct { + Identity OryKratosIdentity `json:"identity"` +} + +type OryKratosUpdateIdentitiyRequest struct { + State string `json:"state"` + Traits OryKratosIdentityTraits `json:"traits"` +} + +type OryKratosCreateIdentitiyRequest struct { + SchemaID string `json:"schema_id"` + Traits OryKratosIdentityTraits `json:"traits"` +} + +const ( + UserActive = "active" + UserInActive = "inactive" +) + +var oryKratosStateMap = map[int]string{ + 0: UserActive, + 1: UserInActive, +} + +func identityToUser(i OryKratosIdentity) User { + return User{ + ID: string(i.ID), + Name: i.Traits.Name, + Nick: i.Traits.Nick, + Email: i.Traits.Email, + Phone: i.Traits.Phone, + AvatarURL: i.Traits.Avatar, + State: i.State, + } +} + +func identityToUserInfo(i OryKratosIdentity) UserInfo { + return userToUserInfo(identityToUser(i)) +} + +func userToUserInfo(u User) UserInfo { + return UserInfo{ + ID: USERID(u.ID), + Email: u.Email, + Phone: u.Phone, + AvatarUrl: u.AvatarURL, + UserName: u.Name, + NickName: u.Nick, + Enabled: true, + } +} + +func userToUserInPaging(u User) userInPaging { + return userInPaging{ + Id: u.ID, + Avatar: u.AvatarURL, + Username: u.Name, + Nickname: u.Nick, + Mobile: u.Phone, + Email: u.Email, + Enabled: true, + Locked: u.State == UserInActive, + // TODO: LastLoginAt PwdExpireAt + } +} diff --git a/pkg/ucauth/user.go b/pkg/ucauth/user.go index e8e2a22babb..ac86c7b20a6 100644 --- a/pkg/ucauth/user.go +++ b/pkg/ucauth/user.go @@ -26,6 +26,7 @@ import ( "github.com/pkg/errors" + "github.com/erda-project/erda/bundle" "github.com/erda-project/erda/pkg/http/httpclient" ) @@ -79,6 +80,7 @@ type UCUserAuth struct { RedirectURI string ClientID string ClientSecret string + bdl *bundle.Bundle } const OryCompatibleClientId = "kratos" @@ -93,7 +95,8 @@ func (a *UCUserAuth) oryKratosAddr() string { } func NewUCUserAuth(UCHostFront, UCHost, RedirectURI, ClientID, ClientSecret string) *UCUserAuth { - return &UCUserAuth{UCHostFront, UCHost, RedirectURI, ClientID, ClientSecret} + bdl := bundle.New(bundle.WithCoreServices(), bundle.WithDOP()) + return &UCUserAuth{UCHostFront, UCHost, RedirectURI, ClientID, ClientSecret, bdl} } // 返回用户中心的登陆URL, 也就是浏览器请求的地址 @@ -177,7 +180,18 @@ func (a *UCUserAuth) PwdAuth(username, password string) (OAuthToken, error) { func (a *UCUserAuth) GetUserInfo(oauthToken OAuthToken) (UserInfo, error) { if a.oryEnabled() { // sessionID as token - return whoami(a.oryKratosAddr(), oauthToken.AccessToken) + userInfo, err := whoami(a.oryKratosAddr(), oauthToken.AccessToken) + if err != nil { + return userInfo, err + } + ucUserID, err := a.bdl.GetUcUserID(string(userInfo.ID)) + if err != nil { + return userInfo, err + } + if ucUserID != "" { + userInfo.ID = USERID(ucUserID) + } + return userInfo, err } bearer := "Bearer " + oauthToken.AccessToken var me bytes.Buffer diff --git a/pkg/ucauth/user_admin.go b/pkg/ucauth/user_admin.go index 5bcbb563f07..27cce1de8be 100644 --- a/pkg/ucauth/user_admin.go +++ b/pkg/ucauth/user_admin.go @@ -21,9 +21,13 @@ import ( "net/http" "strconv" "strings" + "time" + "github.com/jinzhu/gorm" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/erda-project/erda/modules/openapi/conf" "github.com/erda-project/erda/pkg/http/httpclient" "github.com/erda-project/erda/pkg/strutil" ) @@ -36,6 +40,7 @@ type User struct { AvatarURL string `json:"avatar_url"` Phone string `json:"phone_number"` Email string `json:"email"` + State string `json:"state"` } type UcUser struct { @@ -47,12 +52,22 @@ type UcUser struct { Email string `json:"email"` } -// UCClient UC客户端 +type UserIDModel struct { + ID string + UserID string +} + +// UCClient UC客户端\ type UCClient struct { baseURL string isOry bool client *httpclient.HTTPClient ucTokenAuth *UCTokenAuth + db *gorm.DB +} + +func (c *UCClient) SetDBClient(db *gorm.DB) { + c.db = db } // NewUCClient 初始化UC客户端 @@ -95,7 +110,11 @@ func (c *UCClient) FindUsers(ids []string) ([]User, error) { return nil, nil } if c.oryEnabled() { - return getUserByIDs(c.oryKratosPrivateAddr(), ids) + userIDs, err := c.ConvertUserIDs(ids) + if err != nil { + return nil, err + } + return getUserByIDs(c.oryKratosPrivateAddr(), userIDs) } parts := make([]string, len(ids)) for _, id := range ids { @@ -108,6 +127,52 @@ func (c *UCClient) FindUsers(ids []string) ([]User, error) { return c.findUsersByQuery(query, ids...) } +const DIALECT = "mysql" + +const BULK_INSERT_CHUNK_SIZE = 3000 + +func NewDB() (*gorm.DB, error) { + url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=%s", + conf.MySQLUsername(), conf.MySQLPassword(), conf.MySQLHost(), conf.MySQLPort(), conf.MySQLDatabase(), conf.MySQLLoc()) + + logrus.Infof("Initialize db with %s, url: %s", DIALECT, url) + + db, err := gorm.Open(DIALECT, url) + if err != nil { + return nil, err + } + if conf.Debug() { + db.LogMode(true) + } + // connection pool + db.DB().SetMaxIdleConns(10) + db.DB().SetMaxOpenConns(50) + db.DB().SetConnMaxLifetime(time.Hour) + + return db, nil +} + +func (c *UCClient) ConvertUserIDs(ids []string) ([]string, error) { + var users []UserIDModel + if err := c.db.Table("kratos_uc_userid_mapping").Select("user_id").Where("id in (?)", ids).Find(&users).Error; err != nil { + return nil, err + } + return filterUserIDs(ids, users), nil +} + +func filterUserIDs(ids []string, users []UserIDModel) []string { + userIDs := make([]string, 0, len(ids)) + for _, id := range ids { + if len(id) > 11 { + userIDs = append(userIDs, id) + } + } + for _, u := range users { + userIDs = append(userIDs, u.UserID) + } + return userIDs +} + // FindUsersByKey 根据key查找用户,key可匹配用户名/邮箱/手机号 func (c *UCClient) FindUsersByKey(key string) ([]User, error) { if key == "" { diff --git a/pkg/ucauth/user_migration.go b/pkg/ucauth/user_migration.go new file mode 100644 index 00000000000..b92113312ba --- /dev/null +++ b/pkg/ucauth/user_migration.go @@ -0,0 +1,56 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 ucauth + +import ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/erda-project/erda/modules/openapi/conf" + "github.com/erda-project/erda/pkg/http/httpclient" +) + +func (c *UCClient) UserMigration(req OryKratosCreateIdentitiyRequest) (string, error) { + var rsp OryKratosFlowResponse + r, err := httpclient.New(httpclient.WithCompleteRedirect()). + Post(conf.OryKratosPrivateAddr()). + Path("/identities"). + JSONBody(req). + Do().JSON(&rsp) + if err != nil { + return "", err + } + if !r.IsOK() { + return "", errors.Errorf("get kratos user info error, statusCode: %d, err: %s", r.StatusCode(), r.Body()) + } + return rsp.ID, nil +} + +func (c *UCClient) MigrationReady() bool { + var rsp OryKratosReadyResponse + r, err := httpclient.New(httpclient.WithCompleteRedirect()). + Get(conf.OryKratosAddr()). + Path("/health/ready"). + Do().JSON(&rsp) + if err != nil { + logrus.Errorf("get kratos user info error: %v", err) + return false + } + if !r.IsOK() { + logrus.Errorf("get kratos user info error, statusCode: %d, err: %s", r.StatusCode(), r.Body()) + return false + } + return rsp.Status == "ok" +} diff --git a/pkg/ucauth/user_paging.go b/pkg/ucauth/user_paging.go index a361f013541..09403ef3a41 100644 --- a/pkg/ucauth/user_paging.go +++ b/pkg/ucauth/user_paging.go @@ -30,12 +30,12 @@ import ( func HandlePagingUsers(req *apistructs.UserPagingRequest, token OAuthToken) (*userPaging, error) { if token.TokenType == OryCompatibleClientId { - users, err := getUserPage(token.AccessToken, req.PageNo, req.PageSize) + users, total, err := getUserList(token.AccessToken, req) if err != nil { return nil, err } var p userPaging - p.Total = 1000 + p.Total = total for _, u := range users { p.Data = append(p.Data, userToUserInPaging(u)) } diff --git a/quick-start/kratos/kratos.yml b/quick-start/kratos/kratos.yml index 0ce75973820..cbc05fb5413 100644 --- a/quick-start/kratos/kratos.yml +++ b/quick-start/kratos/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: / + default_browser_return_url: http://one.erda.local whitelisted_return_urls: - http://127.0.0.1:4455 @@ -42,12 +42,13 @@ selfservice: default_browser_return_url: / login: - ui_url: /uc/auth/login + ui_url: http://one.erda.local/uc/auth/login lifespan: 10m registration: lifespan: 10m - ui_url: /uc/auth/registration + # not using it, default value for config source validation, can be replaced by env + ui_url: http://one.erda.local/uc/auth/registration after: password: hooks: @@ -66,7 +67,7 @@ secrets: hashers: argon2: parallelism: 1 - memory: 131072 + memory: 128MB iterations: 2 salt_length: 16 key_length: 16 @@ -76,4 +77,4 @@ identity: courier: smtp: - connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&legacy_ssl=true