From 705d2ede6b2e7affcb9dba803fee6ddaba2ec247 Mon Sep 17 00:00:00 2001 From: WindSpiritSR Date: Sat, 17 Jul 2021 14:13:00 +0800 Subject: [PATCH] feat: support LDAP (#160) Signed-off-by: WindSpiritSR --- controllers/ldap.go | 209 +++++++++++++++ go.mod | 1 + go.sum | 7 + object/adapter.go | 5 + object/captcha.go | 14 + object/init.go | 36 +++ object/ldap.go | 381 ++++++++++++++++++++++++++++ object/user.go | 5 +- object/user_test.go | 14 + routers/router.go | 8 + util/json.go | 14 + util/log.go | 14 + util/string.go | 36 +++ util/time.go | 14 + web/src/App.js | 6 +- web/src/LdapEditPage.js | 237 +++++++++++++++++ web/src/LdapListPage.js | 193 ++++++++++++++ web/src/LdapSyncPage.js | 253 ++++++++++++++++++ web/src/LdapTable.js | 201 +++++++++++++++ web/src/OrganizationEditPage.js | 36 ++- web/src/backend/LdapBackend.js | 77 ++++++ web/src/common/AffiliationSelect.js | 14 + web/src/locales/en/data.json | 25 ++ web/src/locales/zh/data.json | 24 ++ 24 files changed, 1821 insertions(+), 3 deletions(-) create mode 100644 controllers/ldap.go create mode 100644 object/ldap.go create mode 100644 web/src/LdapEditPage.js create mode 100644 web/src/LdapListPage.js create mode 100644 web/src/LdapSyncPage.js create mode 100644 web/src/LdapTable.js create mode 100644 web/src/backend/LdapBackend.js diff --git a/controllers/ldap.go b/controllers/ldap.go new file mode 100644 index 00000000..334e3ca8 --- /dev/null +++ b/controllers/ldap.go @@ -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() +} diff --git a/go.mod b/go.mod index 454a9b73..3eaf865d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ec73a2ed..75dd984a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/object/adapter.go b/object/adapter.go index 2ac7a111..05c8ac8e 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -137,4 +137,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.Engine.Sync2(new(Ldap)) + if err != nil { + panic(err) + } } diff --git a/object/captcha.go b/object/captcha.go index dbc03e3b..11ef9255 100644 --- a/object/captcha.go +++ b/object/captcha.go @@ -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 ( diff --git a/object/init.go b/object/init.go index 49c31fde..55f1c3ce 100644 --- a/object/init.go +++ b/object/init.go @@ -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) +} diff --git a/object/ldap.go b/object/ldap.go new file mode 100644 index 00000000..31d68fc2 --- /dev/null +++ b/object/ldap.go @@ -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 +} diff --git a/object/user.go b/object/user.go index 4800702f..1804abb8 100644 --- a/object/user.go +++ b/object/user.go @@ -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) } diff --git a/object/user_test.go b/object/user_test.go index fc8716d6..4f939d47 100644 --- a/object/user_test.go +++ b/object/user_test.go @@ -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 ( diff --git a/routers/router.go b/routers/router.go index 4c663882..1de18e39 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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") diff --git a/util/json.go b/util/json.go index 6adb06f1..8e71e42e 100644 --- a/util/json.go +++ b/util/json.go @@ -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" diff --git a/util/log.go b/util/log.go index 08e16330..4a8ff8ed 100644 --- a/util/log.go +++ b/util/log.go @@ -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 ( diff --git a/util/string.go b/util/string.go index c17c2476..227b920c 100644 --- a/util/string.go +++ b/util/string.go @@ -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] +} diff --git a/util/time.go b/util/time.go index 4bc22091..d489497b 100644 --- a/util/time.go +++ b/util/time.go @@ -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 ( diff --git a/web/src/App.js b/web/src/App.js index 4cb78bd5..473b4bb5 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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( - window.location.href = "/swagger"}> + window.location.href = "/swagger"}> {i18next.t("general:Swagger")} ); @@ -399,6 +401,8 @@ class App extends Component { this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> + this.renderLoginIfNotLoggedIn()}/> + this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> diff --git a/web/src/LdapEditPage.js b/web/src/LdapEditPage.js new file mode 100644 index 00000000..3adc54c5 --- /dev/null +++ b/web/src/LdapEditPage.js @@ -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 ( + {i18next.t("ldap:The Auto Sync option will sync all users to specify organization")} + ) + } + } + + renderLdap() { + return ( + + {i18next.t("ldap:Edit LDAP")}     + + + } style={{marginLeft: "5px"}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("ldap:ID"), i18next.t("general:ID - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("ldap:Server Name"), i18next.t("ldap:Server Name - Tooltip"))} : + + + { + this.updateLdapField("serverName", e.target.value); + }}/> + + + + + {Setting.getLabel(i18next.t("ldap:Server Host"), i18next.t("ldap:Server Host - Tooltip"))} : + + + { + this.updateLdapField("host", e.target.value); + }}/> + + + + + {Setting.getLabel(i18next.t("ldap:Server Port"), i18next.t("ldap:Server Port - Tooltip"))} : + + + value.replace(/\$\s?|(,*)/g, "")} + value={this.state.ldap.port} onChange={value => { + this.updateLdapField("port", value); + }}/> + + + + + {Setting.getLabel(i18next.t("ldap:Base DN"), i18next.t("ldap:Base DN - Tooltip"))} : + + + { + this.updateLdapField("baseDn", e.target.value); + }}/> + + + + + {Setting.getLabel(i18next.t("ldap:Admin"), i18next.t("ldap:Admin - Tooltip"))} : + + + { + this.updateLdapField("admin", e.target.value); + }}/> + + + + + {Setting.getLabel(i18next.t("ldap:Admin Password"), i18next.t("ldap:Admin Password - Tooltip"))} : + + + (visible ? : )} value={this.state.ldap.passwd} + onChange={e => { + this.updateLdapField("passwd", e.target.value); + }} + /> + + + + + {Setting.getLabel(i18next.t("ldap:Auto Sync"), i18next.t("ldap:Auto Sync - Tooltip"))} : + + + value.replace(/\$\s?|(,*)/g, "")} disabled={true} + value={this.state.ldap.autoSync} onChange={value => { + this.updateLdapField("autoSync", value); + }}/> mins + {this.renderAutoSyncWarn()} + + + + ) + } + + 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 ( +
+ + + + + { + this.state.ldap !== null ? this.renderLdap() : null + } + + + + + + + + + + + +
+ ); + } +} + +export default LdapEditPage; diff --git a/web/src/LdapListPage.js b/web/src/LdapListPage.js new file mode 100644 index 00000000..314e76d7 --- /dev/null +++ b/web/src/LdapListPage.js @@ -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 ( + + {text} + + ) + } + }, + { + title: i18next.t("general:Organization"), + dataIndex: "owner", + key: "owner", + width: "140px", + sorter: (a, b) => a.owner.localeCompare(b.owner), + render: (text, record, index) => { + return ( + + {text} + + ) + } + }, + { + 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 ? (Disable) : ( + {text + " mins"}) + } + }, + { + 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 ( +
+ + + this.deleteLdap(index)} + > + + +
+ ) + } + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:LDAPs")} + +
+ )} + loading={ldaps === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.ldaps) + } + + + + + + ); + } +} + +export default LdapListPage; diff --git a/web/src/LdapSyncPage.js b/web/src/LdapSyncPage.js new file mode 100644 index 00000000..32cf4483 --- /dev/null +++ b/web/src/LdapSyncPage.js @@ -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 ( +
+
( +
+ {this.state.ldap?.serverName} + this.syncUsers()} + > + + +
+ )} + loading={users === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.users) + } + + + + + + ); + } +} + +export default LdapSyncPage; diff --git a/web/src/LdapTable.js b/web/src/LdapTable.js new file mode 100644 index 00000000..589ee959 --- /dev/null +++ b/web/src/LdapTable.js @@ -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 ( + + {text} + + ) + } + }, + { + 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 ? (Disable) : ( + {text + " mins"}) + } + }, + { + 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 ( +
+ + + this.deleteRow(table, index)} + > + + +
+ ) + } + }, + ]; + + return ( +
( +
+ {this.props.title}     + +
+ )} + /> + ); + } + + render() { + return ( +
+ +
+ { + this.renderTable(this.props.table) + } + + + + ) + } +} + +export default LdapTable; diff --git a/web/src/OrganizationEditPage.js b/web/src/OrganizationEditPage.js index f2d06738..77639638 100644 --- a/web/src/OrganizationEditPage.js +++ b/web/src/OrganizationEditPage.js @@ -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 { + + + {Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} : + + + { + this.setState({ldaps: value}) }} + /> + + ) } @@ -228,7 +261,8 @@ class OrganizationEditPage extends React.Component { - + diff --git a/web/src/backend/LdapBackend.js b/web/src/backend/LdapBackend.js new file mode 100644 index 00000000..c47a470f --- /dev/null +++ b/web/src/backend/LdapBackend.js @@ -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()); +} diff --git a/web/src/common/AffiliationSelect.js b/web/src/common/AffiliationSelect.js index f11f93f2..4c21f667 100644 --- a/web/src/common/AffiliationSelect.js +++ b/web/src/common/AffiliationSelect.js @@ -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"; diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index 92a5833b..d256647e 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -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" } } diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index 785b790c..57c7b0f0 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -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 时禁用" } }