mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: support user migration from Keycloak using syncer (#645)
* feat: support user migration from Keycloak using syncer Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com> * feat: add more Keycloak columns Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com> * fix: requested changes Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
This commit is contained in:
parent
91602d2b21
commit
801302c6e7
@ -28,6 +28,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewMd5UserSaltCredManager()
|
||||
} else if passwordType == "bcrypt" {
|
||||
return NewBcryptCredManager()
|
||||
} else if passwordType == "pbkdf2-salt" {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
39
cred/pbkdf2-salt.go
Normal file
39
cred/pbkdf2-salt.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 cred
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
type Pbkdf2SaltCredManager struct{}
|
||||
|
||||
func NewPbkdf2SaltCredManager() *Pbkdf2SaltCredManager {
|
||||
cm := &Pbkdf2SaltCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
// https://www.keycloak.org/docs/latest/server_admin/index.html#password-database-compromised
|
||||
decodedSalt, _ := base64.StdEncoding.DecodeString(userSalt)
|
||||
res := pbkdf2.Key([]byte(password), decodedSalt, 27500, 64, sha256.New)
|
||||
return base64.StdEncoding.EncodeToString(res)
|
||||
}
|
||||
|
||||
func (cm *Pbkdf2SaltCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
return hashedPwd == cm.GetHashedPassword(plainPwd, userSalt, organizationSalt)
|
||||
}
|
@ -25,6 +25,11 @@ import (
|
||||
|
||||
type OriginalUser = User
|
||||
|
||||
type Credential struct {
|
||||
Value string `json:"value"`
|
||||
Salt string `json:"salt"`
|
||||
}
|
||||
|
||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||
sql := fmt.Sprintf("select * from %s", syncer.getTable())
|
||||
results, err := syncer.Adapter.Engine.QueryString(sql)
|
||||
|
@ -15,9 +15,11 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
@ -99,12 +101,18 @@ func (syncer *Syncer) setUserByKeyValue(user *User, key string, value string) {
|
||||
user.PasswordSalt = value
|
||||
case "DisplayName":
|
||||
user.DisplayName = value
|
||||
case "FirstName":
|
||||
user.FirstName = value
|
||||
case "LastName":
|
||||
user.LastName = value
|
||||
case "Avatar":
|
||||
user.Avatar = syncer.getPartialAvatarUrl(value)
|
||||
case "PermanentAvatar":
|
||||
user.PermanentAvatar = value
|
||||
case "Email":
|
||||
user.Email = value
|
||||
case "EmailVerified":
|
||||
user.EmailVerified = util.ParseBool(value)
|
||||
case "Phone":
|
||||
user.Phone = value
|
||||
case "Location":
|
||||
@ -167,6 +175,32 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
for _, tableColumn := range syncer.TableColumns {
|
||||
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, result[tableColumn.Name])
|
||||
}
|
||||
|
||||
if syncer.Type == "Keycloak" {
|
||||
// query and set password and password salt from credential table
|
||||
sql := fmt.Sprintf("select * from credential where type = 'password' and user_id = '%s'", originalUser.Id)
|
||||
credentialResult, _ := syncer.Adapter.Engine.QueryString(sql)
|
||||
if len(credentialResult) > 0 {
|
||||
credential := Credential{}
|
||||
_ = json.Unmarshal([]byte(credentialResult[0]["SECRET_DATA"]), &credential)
|
||||
originalUser.Password = credential.Value
|
||||
originalUser.PasswordSalt = credential.Salt
|
||||
}
|
||||
// query and set signup application from user group table
|
||||
sql = fmt.Sprintf("select name from keycloak_group where id = " +
|
||||
"(select group_id as gid from user_group_membership where user_id = '%s')", originalUser.Id)
|
||||
groupResult, _ := syncer.Adapter.Engine.QueryString(sql)
|
||||
if len(groupResult) > 0 {
|
||||
originalUser.SignupApplication = groupResult[0]["name"]
|
||||
}
|
||||
// create time
|
||||
i, _ := strconv.ParseInt(originalUser.CreatedTime, 10, 64)
|
||||
tm := time.Unix(i/int64(1000), 0)
|
||||
originalUser.CreatedTime = tm.Format("2006-01-02T15:04:05+08:00")
|
||||
// enable
|
||||
originalUser.IsForbidden = !(result["ENABLED"] == "\x01")
|
||||
}
|
||||
|
||||
users = append(users, originalUser)
|
||||
}
|
||||
return users
|
||||
|
@ -39,6 +39,7 @@ type User struct {
|
||||
Avatar string `xorm:"varchar(500)" json:"avatar"`
|
||||
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
|
||||
Email string `xorm:"varchar(100) index" json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
Phone string `xorm:"varchar(100) index" json:"phone"`
|
||||
Location string `xorm:"varchar(100)" json:"location"`
|
||||
Address []string `json:"address"`
|
||||
|
@ -52,6 +52,10 @@ func ParseFloat(s string) float64 {
|
||||
}
|
||||
|
||||
func ParseBool(s string) bool {
|
||||
if s == "\x01" {
|
||||
return true
|
||||
}
|
||||
|
||||
i := ParseInt(s)
|
||||
return i != 0
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ class OrganizationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField('passwordType', value);})}>
|
||||
{
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt']
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt', 'pbkdf2-salt']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@ -654,3 +654,94 @@ export function getFromLink() {
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
export function getSyncerTableColumns(syncer) {
|
||||
switch (syncer.type) {
|
||||
case "Keycloak":
|
||||
return [
|
||||
{
|
||||
"name":"ID",
|
||||
"type":"string",
|
||||
"casdoorName":"Id",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"USERNAME",
|
||||
"type":"string",
|
||||
"casdoorName":"Name",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"USERNAME",
|
||||
"type":"string",
|
||||
"casdoorName":"DisplayName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"EMAIL",
|
||||
"type":"string",
|
||||
"casdoorName":"Email",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"EMAIL_VERIFIED",
|
||||
"type":"boolean",
|
||||
"casdoorName":"EmailVerified",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"FIRST_NAME",
|
||||
"type":"string",
|
||||
"casdoorName":"FirstName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"LAST_NAME",
|
||||
"type":"string",
|
||||
"casdoorName":"LastName",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"CREATED_TIMESTAMP",
|
||||
"type":"string",
|
||||
"casdoorName":"CreatedTime",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"ENABLED",
|
||||
"type":"boolean",
|
||||
"casdoorName":"IsForbidden",
|
||||
"isHashed":true,
|
||||
"values":[
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
@ -117,9 +117,13 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {this.updateSyncerField('type', value);})}>
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {
|
||||
this.updateSyncerField('type', value);
|
||||
this.state.syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer);
|
||||
this.state.syncer.table = value === "Keycloak" ? "user_entity" : this.state.syncer.table;
|
||||
})}>
|
||||
{
|
||||
['Database', 'LDAP']
|
||||
['Database', 'LDAP', 'Keycloak']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
@ -198,7 +202,8 @@ class SyncerEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.syncer.table} onChange={e => {
|
||||
<Input value={this.state.syncer.table}
|
||||
disabled={this.state.syncer.type === "Keycloak"} onChange={e => {
|
||||
this.updateSyncerField('table', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
|
@ -98,9 +98,9 @@ class SyncerTableColumnTable extends React.Component {
|
||||
return (
|
||||
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {this.updateField(table, index, 'casdoorName', value);})}>
|
||||
{
|
||||
['Name', 'CreatedTime', 'UpdatedTime', 'Id', 'Type', 'Password', 'PasswordSalt', 'DisplayName', 'Avatar', 'PermanentAvatar', 'Email', 'Phone',
|
||||
'Location', 'Address', 'Affiliation', 'Title', 'IdCardType', 'IdCard', 'Homepage', 'Bio', 'Tag', 'Region', 'Language', 'Gender', 'Birthday',
|
||||
'Education', 'Score', 'Ranking', 'IsDefaultAvatar', 'IsOnline', 'IsAdmin', 'IsGlobalAdmin', 'IsForbidden', 'IsDeleted', 'CreatedIp']
|
||||
['Name', 'CreatedTime', 'UpdatedTime', 'Id', 'Type', 'Password', 'PasswordSalt', 'DisplayName', 'FirstName', 'LastName', 'Avatar', 'PermanentAvatar',
|
||||
'Email', 'EmailVerified', 'Phone', 'Location', 'Address', 'Affiliation', 'Title', 'IdCardType', 'IdCard', 'Homepage', 'Bio', 'Tag', 'Region',
|
||||
'Language', 'Gender', 'Birthday', 'Education', 'Score', 'Ranking', 'IsDefaultAvatar', 'IsOnline', 'IsAdmin', 'IsGlobalAdmin', 'IsForbidden', 'IsDeleted', 'CreatedIp']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
Loading…
x
Reference in New Issue
Block a user