feat: support LDAP (#160)

Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
This commit is contained in:
WindSpiritSR 2021-07-17 14:13:00 +08:00 committed by Yang Luo
parent a22dfd8954
commit 705d2ede6b
24 changed files with 1821 additions and 3 deletions

209
controllers/ldap.go Normal file
View File

@ -0,0 +1,209 @@
// Copyright 2021 The casbin 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 controllers
import (
"encoding/json"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
type LdapServer struct {
Host string `json:"host"`
Port int `json:"port"`
Admin string `json:"admin"`
Passwd string `json:"passwd"`
BaseDn string `json:"baseDn"`
}
type LdapResp struct {
//Groups []LdapRespGroup `json:"groups"`
Users []object.LdapRespUser `json:"users"`
}
//type LdapRespGroup struct {
// GroupId string
// GroupName string
//}
type LdapSyncResp struct {
Exist []object.LdapRespUser `json:"exist"`
Failed []object.LdapRespUser `json:"failed"`
}
func (c *ApiController) GetLdapUser() {
ldapServer := LdapServer{}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldapServer)
if err != nil || util.IsStrsEmpty(ldapServer.Host, ldapServer.Admin, ldapServer.Passwd, ldapServer.BaseDn) {
c.ResponseError("Missing parameter")
return
}
var resp LdapResp
conn, err := object.GetLdapConn(ldapServer.Host, ldapServer.Port, ldapServer.Admin, ldapServer.Passwd)
if err != nil {
c.Data["json"] = Response{Status: "error", Msg: err.Error()}
c.ServeJSON()
return
}
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
//if err != nil {
// c.Data["json"] = Response{Status: "error", Msg: err.Error()}
// c.ServeJSON()
// return
//}
//for _, group := range groupsMap {
// resp.Groups = append(resp.Groups, LdapRespGroup{
// GroupId: group.GidNumber,
// GroupName: group.Cn,
// })
//}
users, err := conn.GetLdapUsers(ldapServer.BaseDn)
if err != nil {
c.Data["json"] = Response{Status: "error", Msg: err.Error()}
c.ServeJSON()
return
}
for _, user := range users {
resp.Users = append(resp.Users, object.LdapRespUser{
UidNumber: user.UidNumber,
Uid: user.Uid,
Cn: user.Cn,
GroupId: user.GidNumber,
//GroupName: groupsMap[user.GidNumber].Cn,
Uuid: user.Uuid,
Email: util.GetMaxLenStr(user.Mail, user.Email, user.EmailAddress),
Phone: util.GetMaxLenStr(user.TelephoneNumber, user.Mobile, user.MobileTelephoneNumber),
Address: util.GetMaxLenStr(user.RegisteredAddress, user.PostalAddress),
})
}
c.Data["json"] = Response{Status: "ok", Data: resp}
c.ServeJSON()
return
}
func (c *ApiController) GetLdaps() {
owner := c.Input().Get("owner")
c.Data["json"] = Response{Status: "ok", Data: object.GetLdaps(owner)}
c.ServeJSON()
}
func (c *ApiController) GetLdap() {
id := c.Input().Get("id")
if util.IsStrsEmpty(id) {
c.ResponseError("Missing parameter")
return
}
c.Data["json"] = Response{Status: "ok", Data: object.GetLdap(id)}
c.ServeJSON()
}
func (c *ApiController) AddLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil {
c.ResponseError("Missing parameter")
return
}
if util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) {
c.ResponseError("Missing parameter")
return
}
if object.CheckLdapExist(&ldap) {
c.ResponseError("Ldap server exist")
return
}
affected := object.AddLdap(&ldap)
resp := wrapActionResponse(affected)
if affected {
resp.Data2 = ldap
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) UpdateLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil || util.IsStrsEmpty(ldap.Owner, ldap.ServerName, ldap.Host, ldap.Admin, ldap.Passwd, ldap.BaseDn) {
c.ResponseError("Missing parameter")
return
}
affected := object.UpdateLdap(&ldap)
resp := wrapActionResponse(affected)
if affected {
resp.Data2 = ldap
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) DeleteLdap() {
var ldap object.Ldap
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ldap)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteLdap(&ldap))
c.ServeJSON()
}
func (c *ApiController) SyncLdapUsers() {
owner := c.Input().Get("owner")
ldapId := c.Input().Get("ldapId")
var users []object.LdapRespUser
err := json.Unmarshal(c.Ctx.Input.RequestBody, &users)
if err != nil {
panic(err)
}
object.UpdateLdapSyncTime(ldapId)
exist, failed := object.SyncLdapUsers(owner, users)
c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{
Exist: *exist,
Failed: *failed,
}}
c.ServeJSON()
}
func (c *ApiController) CheckLdapUsersExist() {
owner := c.Input().Get("owner")
var uuids []string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &uuids)
if err != nil {
panic(err)
}
exist := object.CheckLdapUuidExist(owner, uuids)
c.Data["json"] = &Response{Status: "ok", Data: exist}
c.ServeJSON()
}

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0
github.com/go-sql-driver/mysql v1.5.0
github.com/google/uuid v1.2.0
github.com/jinzhu/configor v1.2.1 // indirect

7
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -63,10 +65,14 @@ github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ=
github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
@ -220,6 +226,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -137,4 +137,9 @@ func (a *Adapter) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap))
if err != nil {
panic(err)
}
}

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 object
import (

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 object
import "github.com/casdoor/casdoor/util"
@ -6,6 +20,7 @@ func InitDb() {
initBuiltInOrganization()
initBuiltInUser()
initBuiltInApplication()
initBuiltInLdap()
}
func initBuiltInOrganization() {
@ -79,3 +94,24 @@ func initBuiltInApplication() {
}
AddApplication(application)
}
func initBuiltInLdap() {
ldap := GetLdap("ldap-built-in")
if ldap != nil {
return
}
ldap = &Ldap{
Id: "ldap-built-in",
Owner: "built-in",
ServerName: "BuildIn LDAP Server",
Host: "example.com",
Port: 389,
Admin: "cn=buildin,dc=example,dc=com",
Passwd: "123",
BaseDn: "ou=BuildIn,dc=example,dc=com",
AutoSync: 0,
LastSync: "",
}
AddLdap(ldap)
}

381
object/ldap.go Normal file
View File

@ -0,0 +1,381 @@
// Copyright 2021 The casbin 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 object
import (
"errors"
"fmt"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
"strings"
)
type Ldap struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Owner string `xorm:"varchar(100)" json:"owner"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
ServerName string `xorm:"varchar(100)" json:"serverName"`
Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"`
Admin string `xorm:"varchar(100)" json:"admin"`
Passwd string `xorm:"varchar(100)" json:"passwd"`
BaseDn string `xorm:"varchar(100)" json:"baseDn"`
AutoSync int `json:"autoSync"`
LastSync string `xorm:"varchar(100)" json:"lastSync"`
}
type ldapConn struct {
Conn *goldap.Conn
}
//type ldapGroup struct {
// GidNumber string
// Cn string
//}
type ldapUser struct {
UidNumber string
Uid string
Cn string
GidNumber string
//Gcn string
Uuid string
Mail string
Email string
EmailAddress string
TelephoneNumber string
Mobile string
MobileTelephoneNumber string
RegisteredAddress string
PostalAddress string
}
type LdapRespUser struct {
UidNumber string `json:"uidNumber"`
Uid string `json:"uid"`
Cn string `json:"cn"`
GroupId string `json:"groupId"`
//GroupName string `json:"groupName"`
Uuid string `json:"uuid"`
Email string `json:"email"`
Phone string `json:"phone"`
Address string `json:"address"`
}
func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*ldapConn, error) {
conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, err
}
err = conn.Bind(adminUser, adminPasswd)
if err != nil {
return nil, fmt.Errorf("fail to login Ldap server with [%s]", adminUser)
}
return &ldapConn{Conn: conn}, nil
}
//FIXME: The Base DN does not necessarily contain the Group
//func (l *ldapConn) GetLdapGroups(baseDn string) (map[string]ldapGroup, error) {
// SearchFilter := "(objectClass=posixGroup)"
// SearchAttributes := []string{"cn", "gidNumber"}
// groupMap := make(map[string]ldapGroup)
//
// searchReq := goldap.NewSearchRequest(baseDn,
// goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
// SearchFilter, SearchAttributes, nil)
// searchResult, err := l.Conn.Search(searchReq)
// if err != nil {
// return nil, err
// }
//
// if len(searchResult.Entries) == 0 {
// return nil, errors.New("no result")
// }
//
// for _, entry := range searchResult.Entries {
// var ldapGroupItem ldapGroup
// for _, attribute := range entry.Attributes {
// switch attribute.Name {
// case "gidNumber":
// ldapGroupItem.GidNumber = attribute.Values[0]
// break
// case "cn":
// ldapGroupItem.Cn = attribute.Values[0]
// break
// }
// }
// groupMap[ldapGroupItem.GidNumber] = ldapGroupItem
// }
//
// return groupMap, nil
//}
func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
SearchFilter := "(objectClass=posixAccount)"
SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
searchReq := goldap.NewSearchRequest(baseDn,
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
SearchFilter, SearchAttributes, nil)
searchResult, err := l.Conn.Search(searchReq)
if err != nil {
return nil, err
}
if len(searchResult.Entries) == 0 {
return nil, errors.New("no result")
}
var ldapUsers []ldapUser
for _, entry := range searchResult.Entries {
var ldapUserItem ldapUser
for _, attribute := range entry.Attributes {
switch attribute.Name {
case "uidNumber":
ldapUserItem.UidNumber = attribute.Values[0]
break
case "uid":
ldapUserItem.Uid = attribute.Values[0]
break
case "cn":
ldapUserItem.Cn = attribute.Values[0]
break
case "gidNumber":
ldapUserItem.GidNumber = attribute.Values[0]
break
case "entryUUID":
ldapUserItem.Uuid = attribute.Values[0]
break
case "mail":
ldapUserItem.Mail = attribute.Values[0]
break
case "email":
ldapUserItem.Email = attribute.Values[0]
break
case "emailAddress":
ldapUserItem.EmailAddress = attribute.Values[0]
break
case "telephoneNumber":
ldapUserItem.TelephoneNumber = attribute.Values[0]
break
case "mobile":
ldapUserItem.Mobile = attribute.Values[0]
break
case "mobileTelephoneNumber":
ldapUserItem.MobileTelephoneNumber = attribute.Values[0]
break
case "registeredAddress":
ldapUserItem.RegisteredAddress = attribute.Values[0]
break
case "postalAddress":
ldapUserItem.PostalAddress = attribute.Values[0]
break
}
}
ldapUsers = append(ldapUsers, ldapUserItem)
}
return ldapUsers, nil
}
func AddLdap(ldap *Ldap) bool {
if len(ldap.Id) == 0 {
ldap.Id = util.GenerateId()
}
if len(ldap.CreatedTime) == 0 {
ldap.CreatedTime = util.GetCurrentTime()
}
affected, err := adapter.Engine.Insert(ldap)
if err != nil {
panic(err)
}
return affected != 0
}
func CheckLdapExist(ldap *Ldap) bool {
var result []*Ldap
err := adapter.Engine.Find(&result, &Ldap{
Owner: ldap.Owner,
Host: ldap.Host,
Port: ldap.Port,
Admin: ldap.Admin,
Passwd: ldap.Passwd,
BaseDn: ldap.BaseDn,
})
if err != nil {
panic(err)
}
if len(result) > 0 {
return true
}
return false
}
func GetLdaps(owner string) []*Ldap {
var ldaps []*Ldap
err := adapter.Engine.Desc("created_time").Find(&ldaps, &Ldap{Owner: owner})
if err != nil {
panic(err)
}
return ldaps
}
func GetLdap(id string) *Ldap {
if util.IsStrsEmpty(id) {
return nil
}
ldap := Ldap{Id: id}
existed, err := adapter.Engine.Get(&ldap)
if err != nil {
panic(err)
}
if existed {
return &ldap
} else {
return nil
}
}
func UpdateLdap(ldap *Ldap) bool {
if GetLdap(ldap.Id) == nil {
return false
}
affected, err := adapter.Engine.ID(ldap.Id).Cols("owner", "server_name", "host",
"port", "admin", "passwd", "base_dn", "auto_sync").Update(ldap)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteLdap(ldap *Ldap) bool {
affected, err := adapter.Engine.ID(ldap.Id).Delete(&Ldap{})
if err != nil {
panic(err)
}
return affected != 0
}
func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]LdapRespUser) {
var existUsers []LdapRespUser
var failedUsers []LdapRespUser
var uuids []string
for _, user := range users {
uuids = append(uuids, user.Uuid)
}
existUuids := CheckLdapUuidExist(owner, uuids)
for _, user := range users {
if len(existUuids) > 0 {
for index, existUuid := range existUuids {
if user.Uuid == existUuid {
existUsers = append(existUsers, user)
existUuids = append(existUuids[:index], existUuids[index+1:]...)
}
}
}
if !AddUser(&User{
Owner: owner,
Name: buildLdapUserName(user.Uid, user.UidNumber),
CreatedTime: util.GetCurrentTime(),
Password: "123",
DisplayName: user.Cn,
Avatar: "https://casbin.org/img/casbin.svg",
Email: user.Email,
Phone: user.Phone,
Address: []string{user.Address},
Affiliation: "Example Inc.",
Tag: "staff",
Ldap: user.Uuid,
}) {
failedUsers = append(failedUsers, user)
continue
}
}
return &existUsers, &failedUsers
}
func UpdateLdapSyncTime(ldapId string) {
_, err := adapter.Engine.ID(ldapId).Update(&Ldap{LastSync: util.GetCurrentTime()})
if err != nil {
panic(err)
}
}
func CheckLdapUuidExist(owner string, uuids []string) []string {
var results []User
var existUuids []string
//whereStr := ""
//for i, uuid := range uuids {
// if i == 0 {
// whereStr = fmt.Sprintf("'%s'", uuid)
// } else {
// whereStr = fmt.Sprintf(",'%s'", uuid)
// }
//}
err := adapter.Engine.Where(fmt.Sprintf("ldap IN (%s) AND owner = ?", "'" + strings.Join(uuids, "','") + "'"), owner).Find(&results)
if err != nil {
panic(err)
}
if len(results) > 0 {
for _, result := range results {
existUuids = append(existUuids, result.Ldap)
}
}
return existUuids
}
func buildLdapUserName(uid, uidNum string) string {
var result User
uidWithNumber := fmt.Sprintf("%s_%s", uid, uidNum)
has, err := adapter.Engine.Where("name = ? or name = ?", uid, uidWithNumber).Get(&result)
if err != nil {
panic(err)
}
if has {
if result.Name == uid {
return uidWithNumber
}
return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6))
}
return uid
}

View File

@ -56,6 +56,7 @@ type User struct {
Gitee string `xorm:"gitee varchar(100)" json:"gitee"`
LinkedIn string `xorm:"linkedin varchar(100)" json:"linkedin"`
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"`
}
@ -142,7 +143,9 @@ func UpdateUser(id string, user *User) bool {
user.UpdateUserHash()
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar", "address","language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden", "hash", "properties").Update(user)
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols("owner", "display_name", "avatar",
"address", "language", "affiliation", "score", "tag", "is_admin", "is_global_admin", "is_forbidden",
"hash", "properties").Update(user)
if err != nil {
panic(err)
}

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 object
import (

View File

@ -64,6 +64,14 @@ func initAPI() {
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck")
beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps")
beego.Router("/api/get-ldap", &controllers.ApiController{}, "POST:GetLdap")
beego.Router("/api/add-ldap", &controllers.ApiController{}, "POST:AddLdap")
beego.Router("/api/update-ldap", &controllers.ApiController{}, "POST:UpdateLdap")
beego.Router("/api/delete-ldap", &controllers.ApiController{}, "POST:DeleteLdap")
beego.Router("/api/check-ldap-users-exist", &controllers.ApiController{}, "POST:CheckLdapUsersExist")
beego.Router("/api/sync-ldap-users", &controllers.ApiController{}, "POST:SyncLdapUsers")
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 util
import "encoding/json"

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 util
import (

View File

@ -55,3 +55,39 @@ func GetMd5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
func IsStrsEmpty(strs ...string) bool {
r := false
for _, str := range strs {
if len(str) == 0 {
r = true
}
}
return r
}
func GetMaxLenStr(strs ...string) string {
m := 0
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}
func GetMinLenStr(strs ...string) string {
m := int(^uint(0) >> 1)
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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 util
import (

View File

@ -27,6 +27,8 @@ import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage";
import TokenEditPage from "./TokenEditPage";
import RecordListPage from "./RecordListPage";
@ -327,7 +329,7 @@ class App extends Component {
);
}
res.push(
<Menu.Item key="6" onClick={() => window.location.href = "/swagger"}>
<Menu.Item key="7" onClick={() => window.location.href = "/swagger"}>
{i18next.t("general:Swagger")}
</Menu.Item>
);
@ -399,6 +401,8 @@ class App extends Component {
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)}/>
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)}/>
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>

237
web/src/LdapEditPage.js Normal file
View File

@ -0,0 +1,237 @@
// Copyright 2021 The casbin 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.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons";
import * as LddpBackend from "./backend/LdapBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import i18n from "i18next";
const {Option} = Select;
class LdapEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldapId: props.match.params.ldapId,
ldap: null,
organizations: [],
};
}
UNSAFE_componentWillMount() {
this.getLdap();
this.getOrganizations();
}
getLdap() {
LddpBackend.getLdap(this.state.ldapId)
.then((res) => {
if (res.status === "ok") {
this.setState({
ldap: res.data
})
} else {
Setting.showMessage("error", res.msg);
}
})
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
updateLdapField(key, value) {
this.setState((prevState) => {
prevState.ldap[key] = value;
return prevState;
});
}
renderAutoSyncWarn() {
if (this.state.ldap.autoSync > 0) {
return (
<span style={{
color: "#faad14",
marginLeft: "20px"
}}>{i18next.t("ldap:The Auto Sync option will sync all users to specify organization")}</span>
)
}
}
renderLdap() {
return (
<Card size="small" title={
<div>
{i18next.t("ldap:Edit LDAP")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
</div>
} style={{marginLeft: "5px"}} type="inner">
<Row style={{marginTop: "10px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={21}>
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)}
value={this.state.ldap.owner} onChange={(value => {
this.updateLdapField("owner", value);
})}>
{
this.state.organizations.map((organization, index) => <Option key={index}
value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:ID"), i18next.t("general:ID - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.id} disabled={true}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Name"), i18next.t("ldap:Server Name - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.serverName} onChange={e => {
this.updateLdapField("serverName", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Host"), i18next.t("ldap:Server Host - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.host} onChange={e => {
this.updateLdapField("host", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Server Port"), i18next.t("ldap:Server Port - Tooltip"))} :
</Col>
<Col span={21}>
<InputNumber min={0} max={65535} formatter={value => value.replace(/\$\s?|(,*)/g, "")}
value={this.state.ldap.port} onChange={value => {
this.updateLdapField("port", value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.baseDn} onChange={e => {
this.updateLdapField("baseDn", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} :
</Col>
<Col span={21}>
<Input value={this.state.ldap.admin} onChange={e => {
this.updateLdapField("admin", e.target.value);
}}/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Admin Password"), i18next.t("ldap:Admin Password - Tooltip"))} :
</Col>
<Col span={21}>
<Input.Password
iconRender={visible => (visible ? <EyeTwoTone/> : <EyeInvisibleOutlined/>)} value={this.state.ldap.passwd}
onChange={e => {
this.updateLdapField("passwd", e.target.value);
}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{lineHeight: "32px", textAlign: "right", paddingRight: "25px"}} span={3}>
{Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} :
</Col>
<Col span={21}>
<InputNumber min={0} formatter={value => value.replace(/\$\s?|(,*)/g, "")} disabled={true}
value={this.state.ldap.autoSync} onChange={value => {
this.updateLdapField("autoSync", value);
}}/><span>&nbsp;mins</span>
{this.renderAutoSyncWarn()}
</Col>
</Row>
</Card>
)
}
submitLdapEdit() {
LddpBackend.updateLdap(this.state.ldap)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Update LDAP server success`);
this.setState((prevState) => {
prevState.ldap = res.data2;
})
} else {
Setting.showMessage("error", res.msg);
}
})
.catch(error => {
Setting.showMessage("error", `Update LDAP server failed: ${error}`);
});
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.state.ldap !== null ? this.renderLdap() : null
}
</Col>
<Col span={1}>
</Col>
</Row>
<Row style={{margin: 10}}>
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large"
onClick={() => this.submitLdapEdit()}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>
);
}
}
export default LdapEditPage;

193
web/src/LdapListPage.js Normal file
View File

@ -0,0 +1,193 @@
// Copyright 2021 The casbin 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.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Col, Popconfirm, Row, Table} from "antd";
import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next";
class LdapListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldaps: null
};
}
UNSAFE_componentWillMount() {
this.getLdaps()
}
getLdaps() {
LdapBackend.getLdaps("")
.then((res) => {
let ldapsData = [];
if (res.status === "ok") {
ldapsData = res.data;
} else {
Setting.showMessage("error", res.msg);
}
this.setState((prevState) => {
prevState.ldaps = ldapsData;
return prevState;
})
});
}
deleteLdap(index) {
}
renderTable(ldaps) {
const columns = [
{
title: i18next.t("ldap:Server Name"),
dataIndex: "serverName",
key: "serverName",
width: "200px",
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
render: (text, record, index) => {
return (
<Link to={`/ldaps/${record.id}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "140px",
sorter: (a, b) => a.owner.localeCompare(b.owner),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("ldap:Server"),
dataIndex: "host",
key: "host",
ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => {
return `${text}:${record.port}`
}
},
{
title: i18next.t("ldap:Base DN"),
dataIndex: "baseDn",
key: "baseDn",
ellipsis: true,
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
},
{
title: i18next.t("ldap:Admin"),
dataIndex: "admin",
key: "admin",
ellipsis: true,
sorter: (a, b) => a.admin.localeCompare(b.admin),
},
{
title: i18next.t("ldap:Auto Sync"),
dataIndex: "autoSync",
key: "autoSync",
width: "100px",
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
}
},
{
title: i18next.t("ldap:Last Sync"),
dataIndex: "lastSync",
key: "lastSync",
ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => {
return text
}
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
onConfirm={() => this.deleteLdap(index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
return (
<div>
<Table columns={columns} dataSource={ldaps} rowKey="id" size="middle" bordered
pagination={{pageSize: 100}}
title={() => (
<div>
<span>{i18next.t("general:LDAPs")}</span>
<Button type="primary" size="small" style={{marginLeft: "10px"}}
onClick={() => {
this.addLdap()
}}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={ldaps === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.ldaps)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default LdapListPage;

253
web/src/LdapSyncPage.js Normal file
View File

@ -0,0 +1,253 @@
// Copyright 2021 The casbin 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.
import React from "react";
import {Button, Col, Row, Table, Popconfirm} from "antd";
import * as Setting from "./Setting";
import * as LdapBackend from "./backend/LdapBackend";
import i18next from "i18next";
class LdapSyncPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ldapId: props.match.params.ldapId,
ldap: null,
users: [],
existUuids: [],
selectedUsers: []
};
}
UNSAFE_componentWillMount() {
this.getLdap()
}
syncUsers() {
let selectedUsers = this.state.selectedUsers;
if (selectedUsers === null || selectedUsers.length === 0) {
Setting.showMessage("error", "Please select al least 1 user first");
return
}
LdapBackend.syncUsers(this.state.ldap.owner, this.state.ldap.id, selectedUsers)
.then((res => {
if (res.status === "ok") {
let exist = res.data.exist;
let failed = res.data.failed;
let existUser = [];
let failedUser = [];
if ((!exist || exist.length === 0) && (!failed || failed.length === 0)) {
Setting.goToLink(`/organizations/${this.state.ldap.owner}/users`);
} else {
if (exist && exist.length > 0) {
exist.forEach(elem => {
existUser.push(elem.cn);
});
Setting.showMessage("error", `User [${existUser}] is already exist`);
}
if (failed && failed.length > 0) {
failed.forEach(elem => {
failedUser.push(elem.cn);
})
Setting.showMessage("error", `Sync [${failedUser}] failed`)
}
}
} else {
Setting.showMessage("error", res.msg);
}
}))
}
getLdap() {
LdapBackend.getLdap(this.state.ldapId)
.then((res) => {
if (res.status === "ok") {
this.setState((prevState) => {
prevState.ldap = res.data;
return prevState;
})
this.getLdapUser(res.data);
} else {
Setting.showMessage("error", res.msg);
}
});
}
getLdapUser(ldap) {
LdapBackend.getLdapUser(ldap)
.then((res) => {
if (res.status === "ok") {
this.setState((prevState) => {
prevState.users = res.data.users;
return prevState;
})
this.getExistUsers(ldap.owner, res.data.users);
} else {
Setting.showMessage("error", res.msg);
}
})
}
getExistUsers(owner, users) {
let uuidArray = [];
users.forEach(elem => {
uuidArray.push(elem.uuid);
})
LdapBackend.checkLdapUsersExist(owner, uuidArray)
.then((res) => {
if (res.status === "ok") {
this.setState(prevState => {
prevState.existUuids = res.data?.length > 0 ? res.data : [];
return prevState;
})
}
})
}
buildValArray(data, key) {
let valTypesArray = [];
if (data !== null && data.length > 0) {
data.forEach(elem => {
let val = elem[key];
if (!valTypesArray.includes(val)) {
valTypesArray.push(val);
}
});
}
return valTypesArray;
}
buildFilter(data, key) {
let filterArray = [];
if (data !== null && data.length > 0) {
let valArray = this.buildValArray(data, key)
valArray.forEach(elem => {
filterArray.push({
text: elem,
value: elem,
});
});
}
return filterArray;
}
renderTable(users) {
const columns = [
{
title: i18next.t("ldap:CN"),
dataIndex: "cn",
key: "cn",
sorter: (a, b) => a.cn.localeCompare(b.cn),
},
{
title: i18next.t("ldap:UidNumber / Uid"),
dataIndex: "uidNumber",
key: "uidNumber",
width: "200px",
sorter: (a, b) => a.uidNumber.localeCompare(b.uidNumber),
render: (text, record, index) => {
return `${text} / ${record.uid}`
},
},
{
title: i18next.t("ldap:Group Id"),
dataIndex: "groupId",
key: "groupId",
width: "140px",
sorter: (a, b) => a.groupId.localeCompare(b.groupId),
filters: this.buildFilter(this.state.users, "groupId"),
onFilter: (value, record) => record.groupId.indexOf(value) === 0,
},
{
title: i18next.t("ldap:Email"),
dataIndex: "email",
key: "email",
width: "240px",
sorter: (a, b) => a.email.localeCompare(b.email),
},
{
title: i18next.t("ldap:Phone"),
dataIndex: "phone",
key: "phone",
width: "160px",
sorter: (a, b) => a.phone.localeCompare(b.phone),
},
{
title: i18next.t("ldap:Address"),
dataIndex: "address",
key: "address",
sorter: (a, b) => a.address.localeCompare(b.address),
},
];
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState(prevState => {
prevState.selectedUsers = selectedRows;
return prevState;
})
},
getCheckboxProps: record => ({
disabled: this.state.existUuids.indexOf(record.uuid) !== -1,
}),
};
return (
<div>
<Table rowSelection={rowSelection} columns={columns} dataSource={users} rowKey="uuid" bordered
pagination={{pageSize: 100}}
title={() => (
<div>
<span>{this.state.ldap?.serverName}</span>
<Popconfirm placement={"right"}
title={`Please confirm to sync selected users`}
onConfirm={() => this.syncUsers()}
>
<Button type="primary" size="small"
style={{marginLeft: "10px"}}>{i18next.t("ldap:Sync")}</Button>
</Popconfirm>
</div>
)}
loading={users === null}
/>
</div>
);
}
render() {
return (
<div>
<Row style={{width: "100%"}}>
<Col span={1}>
</Col>
<Col span={22}>
{
this.renderTable(this.state.users)
}
</Col>
<Col span={1}>
</Col>
</Row>
</div>
);
}
}
export default LdapSyncPage;

201
web/src/LdapTable.js Normal file
View File

@ -0,0 +1,201 @@
// Copyright 2021 The casbin 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.
import React from "react";
import {Button, Col, Popconfirm, Row, Table} from 'antd';
import * as Setting from "./Setting";
import i18next from "i18next";
import * as LdapBackend from "./backend/LdapBackend";
import {Link} from "react-router-dom";
class LdapTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
newLdap() {
return {
id: "",
owner: this.props.organizationName,
createdTime: "",
serverName: "Example LDAP Server",
host: "example.com",
port: 389,
admin: "cn=admin,dc=example,dc=com",
passwd: "123",
baseDn: "ou=People,dc=example,dc=com",
autosync: 0,
lastSync: ""
}
}
addRow(table) {
const newLdap = this.newLdap();
LdapBackend.addLdap(newLdap)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Add LDAP server success`);
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, res.data2);
this.updateTable(table);
} else {
Setting.showMessage("error", res.msg);
}
}
)
.catch(error => {
Setting.showMessage("error", `Add LDAP server failed: ${error}`);
});
}
deleteRow(table, i) {
LdapBackend.deleteLdap(table[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", `Delete LDAP server success`);
table = Setting.deleteRow(table, i);
this.updateTable(table);
} else {
Setting.showMessage("error", res.msg);
}
}
)
.catch(error => {
Setting.showMessage("error", `Delete LDAP server failed: ${error}`);
});
}
renderTable(table) {
const columns = [
{
title: i18next.t("ldap:Server Name"),
dataIndex: "serverName",
key: "serverName",
width: "160px",
sorter: (a, b) => a.serverName.localeCompare(b.serverName),
render: (text, record, index) => {
return (
<Link to={`/ldaps/${record.id}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("ldap:Server"),
dataIndex: "host",
key: "host",
ellipsis: true,
sorter: (a, b) => a.host.localeCompare(b.host),
render: (text, record, index) => {
return `${text}:${record.port}`
}
},
{
title: i18next.t("ldap:Base DN"),
dataIndex: "baseDn",
key: "baseDn",
ellipsis: true,
sorter: (a, b) => a.baseDn.localeCompare(b.baseDn),
},
{
title: i18next.t("ldap:Auto Sync"),
dataIndex: "autoSync",
key: "autoSync",
width: "120px",
sorter: (a, b) => a.autoSync.localeCompare(b.autoSync),
render: (text, record, index) => {
return text === 0 ? (<span style={{color: "#faad14"}}>Disable</span>) : (
<span style={{color: "#52c41a"}}>{text + " mins"}</span>)
}
},
{
title: i18next.t("ldap:Last Sync"),
dataIndex: "lastSync",
key: "lastSync",
ellipsis: true,
sorter: (a, b) => a.lastSync.localeCompare(b.lastSync),
render: (text, record, index) => {
return text
}
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "240px",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
type="primary"
onClick={() => Setting.goToLink(`/ldap/sync/${record.id}`)}>{i18next.t("ldap:Sync")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}}
onClick={() => Setting.goToLink(`/ldap/${record.id}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete LDAP Config: ${record.serverName} ?`}
onConfirm={() => this.deleteRow(table, index)}
>
<Button style={{marginBottom: "10px"}}
type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
return (
<Table rowKey="id" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small"
onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: '20px'}}>
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
)
}
}
export default LdapTable;

View File

@ -15,9 +15,11 @@
import React from "react";
import {Button, Card, Col, Input, Row, Select} from 'antd';
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable";
const { Option } = Select;
@ -28,11 +30,13 @@ class OrganizationEditPage extends React.Component {
classes: props,
organizationName: props.match.params.organizationName,
organization: null,
ldaps: null,
};
}
UNSAFE_componentWillMount() {
this.getOrganization();
this.getLdaps();
}
getOrganization() {
@ -44,6 +48,21 @@ class OrganizationEditPage extends React.Component {
});
}
getLdaps() {
LdapBackend.getLdaps(this.state.organizationName)
.then(res => {
let resdata = []
if (res.status === "ok") {
if (res.data !== null) {
resdata = res.data;
}
}
this.setState({
ldaps: resdata
})
})
}
parseOrganizationField(key, value) {
// if ([].includes(key)) {
// value = Setting.myParseInt(value);
@ -186,6 +205,20 @@ class OrganizationEditPage extends React.Component {
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}}>
<Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
</Col>
<Col span={22}>
<LdapTable
title={i18next.t("general:LDAPs")}
table={this.state.ldaps}
organizationName={this.state.organizationName}
onUpdateTable={(value) => {
this.setState({ldaps: value}) }}
/>
</Col>
</Row>
</Card>
)
}
@ -228,7 +261,8 @@ class OrganizationEditPage extends React.Component {
<Col span={2}>
</Col>
<Col span={18}>
<Button type="primary" size="large" onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
<Button type="primary" size="large"
onClick={this.submitOrganizationEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</Col>
</Row>
</div>

View File

@ -0,0 +1,77 @@
// Copyright 2021 The casbin 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.
import * as Setting from "../Setting";
export function getLdaps(owner) {
return fetch(`${Setting.ServerUrl}/api/get-ldaps?owner=${owner}`, {
method: "POST",
credentials: "include",
}).then(res => res.json());
}
export function getLdap(id) {
return fetch(`${Setting.ServerUrl}/api/get-ldap?id=${id}`, {
method: "POST",
credentials: "include",
}).then(res => res.json());
}
export function addLdap(body) {
return fetch(`${Setting.ServerUrl}/api/add-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function deleteLdap(body) {
return fetch(`${Setting.ServerUrl}/api/delete-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function updateLdap(body) {
return fetch(`${Setting.ServerUrl}/api/update-ldap`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function getLdapUser(body) {
return fetch(`${Setting.ServerUrl}/api/get-ldap-user`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function syncUsers(owner, ldapId, body) {
return fetch(`${Setting.ServerUrl}/api/sync-ldap-users?owner=${owner}&ldapId=${ldapId}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}
export function checkLdapUsersExist(owner, body) {
return fetch(`${Setting.ServerUrl}/api/check-ldap-users-exist?owner=${owner}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(body),
}).then(res => res.json());
}

View File

@ -1,3 +1,17 @@
// Copyright 2021 The casbin 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.
import React from "react";
import {Cascader, Col, Input, Row, Select} from 'antd';
import i18next from "i18next";

View File

@ -18,6 +18,7 @@
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "Save",
"Add": "Add",
"Action": "Action",
@ -282,5 +283,29 @@
"Email/Phone": "Email/Phone",
"Change Password": "Change Password",
"Choose email verification or mobile verification": "Choose email verification or mobile verification"
},
"ldap":
{
"Server Name": "Server Name",
"Host": "Host",
"Server": "Server",
"Base DN": "Base DN",
"Admin": "Admin",
"Admin Password": "Admin Password",
"Auto Sync": "Auto Sync",
"Last Sync": "Last Sync",
"Sync": "Sync",
"ID": "ID",
"Server Host": "Server Host",
"Server Port": "Server Port",
"Edit LDAP": "Edit LDAP",
"Sync users": "Sync users",
"Server Name - Tooltip": "LDAP server config display name",
"Server Host - Tooltip": "LDAP server host",
"Server Port - Tooltip": "LDAP server port",
"Base DN - Tooltip": "LDAP search base DN",
"Admin - Tooltip": "LDAP server admin CN or ID",
"Admin Password - Tooltip": "LDAP server admin password",
"Auto Sync - Tooltip": "Auto sync config, disable if is 0"
}
}

View File

@ -17,6 +17,7 @@
"Timestamp": "Timestamp",
"Username": "Username",
"Request uri": "Request uri",
"LDAPs": "LDAPs",
"Save": "保存",
"Add": "添加",
"Action": "操作",
@ -272,5 +273,28 @@
"Email/Phone": "邮箱/手机号",
"Change Password": "修改密码",
"Choose email verification or mobile verification": "选择邮箱验证或手机验证"
},
"ldap": {
"Server Name": "LDAP 服务器",
"Host": "域名",
"Server": "服务器",
"Base DN": "基本 DN",
"Admin": "管理员",
"Admin Password": "密码",
"Auto Sync": "自动同步",
"Last Sync": "最近同步",
"Sync": "同步",
"ID": "ID",
"Server Host": "域名",
"Server Port": "端口",
"Edit LDAP": "编辑 LDAP",
"Sync users": "同步用户",
"Server Name - Tooltip": "LDAP 服务器配置显示名称",
"Server Host - Tooltip": "LDAP 服务器地址",
"Server Port - Tooltip": "LDAP 服务器端口号",
"Base DN - Tooltip": "LDAP 搜索时的基本 DN",
"Admin - Tooltip": "LDAP 服务器管理员的 CN 或 ID",
"Admin Password - Tooltip": "LDAP 服务器管理员密码",
"Auto Sync - Tooltip": "自动同步配置,为 0 时禁用"
}
}