feat: fix LDAP server handle filter without CN field as * (#1705)

* fix: set ldap server default filter name as *

* fix: default use built-in organization to bind

* chore: use cache reduce the ci test time
This commit is contained in:
Yaodong Yu 2023-04-04 20:51:28 +08:00 committed by GitHub
parent 0781a3835d
commit e1842f6b80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 201 additions and 174 deletions

View File

@ -9,18 +9,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
mysql: mysql:
image: mysql:5.7 image: mysql:5.7
env: env:
MYSQL_DATABASE: casdoor MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456 MYSQL_ROOT_PASSWORD: 123456
ports: ports:
- 3306:3306 - 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v2 - uses: actions/setup-go@v4
with: with:
go-version: '^1.16.5' go-version: '^1.16.5'
cache-dependency-path: ./go.mod
- name: Tests - name: Tests
run: | run: |
go test -v $(go list ./...) -tags skipCi go test -v $(go list ./...) -tags skipCi
@ -31,14 +32,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ go-tests ] needs: [ go-tests ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
# cache cache: 'yarn'
- uses: c-hive/gha-yarn-cache@v2 cache-dependency-path: ./web/yarn.lock
with:
directory: ./web
- run: yarn install && CI=false yarn run build - run: yarn install && CI=false yarn run build
working-directory: ./web working-directory: ./web
@ -47,10 +46,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ go-tests ] needs: [ go-tests ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v2 - uses: actions/setup-go@v4
with: with:
go-version: '^1.16.5' go-version: '^1.16.5'
cache-dependency-path: ./go.mod
- run: go version - run: go version
- name: Build - name: Build
run: | run: |
@ -63,13 +63,14 @@ jobs:
needs: [ go-tests ] needs: [ go-tests ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: '^1.16.5' go-version: '^1.16.5'
cache-dependency-path: ./go.mod
# gen a dummy config file # gen a dummy config file
- run: touch dummy.yml - run: touch dummy.yml
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
@ -82,35 +83,35 @@ jobs:
needs: [ go-tests ] needs: [ go-tests ]
services: services:
mysql: mysql:
image: mysql:5.7 image: mysql:5.7
env: env:
MYSQL_DATABASE: casdoor MYSQL_DATABASE: casdoor
MYSQL_ROOT_PASSWORD: 123456 MYSQL_ROOT_PASSWORD: 123456
ports: ports:
- 3306:3306 - 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v2 - uses: actions/setup-go@v4
with: with:
go-version: '^1.16.5' go-version: '^1.16.5'
- uses: actions/setup-node@v2 cache-dependency-path: ./go.mod
with: - name: start backend
node-version: 16
- name: back start
run: nohup go run ./main.go & run: nohup go run ./main.go &
working-directory: ./ working-directory: ./
- name: front install - uses: actions/setup-node@v3
run: yarn install
working-directory: ./web
- name: front start
run: nohup yarn start &
working-directory: ./web
- uses: cypress-io/github-action@v4
with: with:
working-directory: ./web node-version: 16
cache: 'yarn'
cache-dependency-path: ./web/yarn.lock
- run: yarn install
working-directory: ./web
- uses: cypress-io/github-action@v5
with:
start: yarn start
wait-on: 'http://localhost:7001' wait-on: 'http://localhost:7001'
wait-on-timeout: 180 wait-on-timeout: 180
working-directory: ./web
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure() if: failure()
@ -121,7 +122,7 @@ jobs:
if: always() if: always()
with: with:
name: cypress-videos name: cypress-videos
path: ./web/cypress/videos path: ./web/cypress/videos
release-and-push: release-and-push:
name: Release And Push name: Release And Push
@ -130,11 +131,11 @@ jobs:
needs: [ frontend, backend, linter, e2e ] needs: [ frontend, backend, linter, e2e ]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: -1 fetch-depth: -1
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
@ -166,10 +167,10 @@ jobs:
elif [ ${old_array[1]} != ${new_array[1]} ] elif [ ${old_array[1]} != ${new_array[1]} ]
then then
echo ::set-output name=push::'true' echo ::set-output name=push::'true'
else else
echo ::set-output name=push::'false' echo ::set-output name=push::'false'
fi fi
- name: Set up QEMU - name: Set up QEMU

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package controllers package ldap
import ( import (
"fmt" "fmt"
@ -20,76 +20,78 @@ import (
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/forestmgy/ldapserver" ldap "github.com/forestmgy/ldapserver"
"github.com/lor00x/goldap/message" "github.com/lor00x/goldap/message"
) )
func StartLdapServer() { func StartLdapServer() {
server := ldapserver.NewServer() server := ldap.NewServer()
routes := ldapserver.NewRouteMux() routes := ldap.NewRouteMux()
routes.Bind(handleBind) routes.Bind(handleBind)
routes.Search(handleSearch).Label(" SEARCH****") routes.Search(handleSearch).Label(" SEARCH****")
server.Handle(routes) server.Handle(routes)
server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort")) err := server.ListenAndServe("0.0.0.0:" + conf.GetConfigString("ldapServerPort"))
if err != nil {
return
}
} }
func handleBind(w ldapserver.ResponseWriter, m *ldapserver.Message) { func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
r := m.GetBindRequest() r := m.GetBindRequest()
res := ldapserver.NewBindResponse(ldapserver.LDAPResultSuccess) res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
if r.AuthenticationChoice() == "simple" { if r.AuthenticationChoice() == "simple" {
bindusername, bindorg, err := object.GetNameAndOrgFromDN(string(r.Name())) bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name()))
if err != "" { if err != "" {
log.Printf("Bind failed ,ErrMsg=%s", err) log.Printf("Bind failed ,ErrMsg=%s", err)
res.SetResultCode(ldapserver.LDAPResultInvalidDNSyntax) res.SetResultCode(ldap.LDAPResultInvalidDNSyntax)
res.SetDiagnosticMessage("bind failed ErrMsg: " + err) res.SetDiagnosticMessage("bind failed ErrMsg: " + err)
w.Write(res) w.Write(res)
return return
} }
bindpassword := string(r.AuthenticationSimple())
binduser, err := object.CheckUserPassword(bindorg, bindusername, bindpassword, "en") bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(object.CasdoorOrganization, bindUsername, bindPassword, "en")
if err != "" { if err != "" {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err) log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldapserver.LDAPResultInvalidCredentials) res.SetResultCode(ldap.LDAPResultInvalidCredentials)
res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err) res.SetDiagnosticMessage("invalid credentials ErrMsg: " + err)
w.Write(res) w.Write(res)
return return
} }
if bindorg == "built-in" {
if bindOrg == "built-in" || bindUser.IsGlobalAdmin {
m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true
} else if binduser.IsAdmin { } else if bindUser.IsAdmin {
m.Client.IsOrgAdmin = true m.Client.IsOrgAdmin = true
} }
m.Client.IsAuthenticated = true m.Client.IsAuthenticated = true
m.Client.UserName = bindusername m.Client.UserName = bindUsername
m.Client.OrgName = bindorg m.Client.OrgName = bindOrg
} else { } else {
res.SetResultCode(ldapserver.LDAPResultAuthMethodNotSupported) res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported)
res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication") res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication")
} }
w.Write(res) w.Write(res)
} }
func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) { func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
res := ldapserver.NewSearchResultDoneResponse(ldapserver.LDAPResultSuccess) res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)
if !m.Client.IsAuthenticated { if !m.Client.IsAuthenticated {
res.SetResultCode(ldapserver.LDAPResultUnwillingToPerform) res.SetResultCode(ldap.LDAPResultUnwillingToPerform)
w.Write(res) w.Write(res)
return return
} }
r := m.GetSearchRequest() r := m.GetSearchRequest()
if r.FilterString() == "(objectClass=*)" { if r.FilterString() == "(objectClass=*)" {
w.Write(res) w.Write(res)
return return
} }
name, org, errCode := object.GetUserNameAndOrgFromBaseDnAndFilter(string(r.BaseObject()), r.FilterString())
if errCode != ldapserver.LDAPResultSuccess {
res.SetResultCode(errCode)
w.Write(res)
return
}
// Handle Stop Signal (server stop / client disconnected / Abandoned request....) // Handle Stop Signal (server stop / client disconnected / Abandoned request....)
select { select {
case <-m.Done: case <-m.Done:
@ -97,16 +99,17 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
return return
default: default:
} }
users, errCode := object.GetFilteredUsers(m, name, org)
if errCode != ldapserver.LDAPResultSuccess { users, code := GetFilteredUsers(m)
res.SetResultCode(errCode) if code != ldap.LDAPResultSuccess {
res.SetResultCode(code)
w.Write(res) w.Write(res)
return return
} }
for i := 0; i < len(users); i++ {
user := users[i] for _, user := range users {
dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject())) dn := fmt.Sprintf("cn=%s,%s", user.Name, string(r.BaseObject()))
e := ldapserver.NewSearchResultEntry(dn) e := ldap.NewSearchResultEntry(dn)
e.AddAttribute("cn", message.AttributeValue(user.Name)) e.AddAttribute("cn", message.AttributeValue(user.Name))
e.AddAttribute("uid", message.AttributeValue(user.Name)) e.AddAttribute("uid", message.AttributeValue(user.Name))
e.AddAttribute("email", message.AttributeValue(user.Email)) e.AddAttribute("email", message.AttributeValue(user.Email))
@ -117,22 +120,3 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) {
} }
w.Write(res) w.Write(res)
} }
// get user password with hash type prefix
// TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
func getUserPasswordWithType(user *object.User) string {
org := object.GetOrganizationByUser(user)
if org.PasswordType == "" || org.PasswordType == "plain" {
return user.Password
}
prefix := org.PasswordType
if prefix == "salt" {
prefix = "sha256"
} else if prefix == "md5-salt" {
prefix = "md5"
} else if prefix == "pbkdf2-salt" {
prefix = "pbkdf2"
}
return fmt.Sprintf("{%s}%s", prefix, user.Password)
}

116
ldap/util.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ldap
import (
"fmt"
"log"
"strings"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
ldap "github.com/forestmgy/ldapserver"
)
func getNameAndOrgFromDN(DN string) (string, string, string) {
DNFields := strings.Split(DN, ",")
params := make(map[string]string, len(DNFields))
for _, field := range DNFields {
if strings.Contains(field, "=") {
k := strings.Split(field, "=")
params[k[0]] = k[1]
}
}
if params["cn"] == "" {
return "", "", "please use Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
}
if params["ou"] == "" {
return params["cn"], object.CasdoorOrganization, ""
}
return params["cn"], params["ou"], ""
}
func getNameAndOrgFromFilter(baseDN, filter string) (string, string, int) {
if !strings.Contains(baseDN, "ou=") {
return "", "", ldap.LDAPResultInvalidDNSyntax
}
name, org, _ := getNameAndOrgFromDN(fmt.Sprintf("cn=%s,", getUsername(filter)) + baseDN)
return name, org, ldap.LDAPResultSuccess
}
func getUsername(filter string) string {
nameIndex := strings.Index(filter, "cn=")
if nameIndex == -1 {
return "*"
}
var name string
for i := nameIndex + 3; filter[i] != ')'; i++ {
name = name + string(filter[i])
}
return name
}
func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int) {
r := m.GetSearchRequest()
name, org, code := getNameAndOrgFromFilter(string(r.BaseObject()), r.FilterString())
if code != ldap.LDAPResultSuccess {
return nil, code
}
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if m.Client.IsGlobalAdmin && org == "*" {
filteredUsers = object.GetGlobalUsers()
return filteredUsers, ldap.LDAPResultSuccess
}
if m.Client.IsGlobalAdmin || org == m.Client.OrgName {
filteredUsers = object.GetUsers(org)
return filteredUsers, ldap.LDAPResultSuccess
} else {
return nil, ldap.LDAPResultInsufficientAccessRights
}
} else {
hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), org, true, "en")
if !hasPermission {
log.Printf("ErrMsg = %v", err.Error())
return nil, ldap.LDAPResultInsufficientAccessRights
}
user := object.GetUser(util.GetId(org, name))
filteredUsers = append(filteredUsers, user)
return filteredUsers, ldap.LDAPResultSuccess
}
}
// get user password with hash type prefix
// TODO not handle salt yet
// @return {md5}5f4dcc3b5aa765d61d8327deb882cf99
func getUserPasswordWithType(user *object.User) string {
org := object.GetOrganizationByUser(user)
if org.PasswordType == "" || org.PasswordType == "plain" {
return user.Password
}
prefix := org.PasswordType
if prefix == "salt" {
prefix = "sha256"
} else if prefix == "md5-salt" {
prefix = "md5"
} else if prefix == "pbkdf2-salt" {
prefix = "pbkdf2"
}
return fmt.Sprintf("{%s}%s", prefix, user.Password)
}

View File

@ -23,7 +23,7 @@ import (
_ "github.com/beego/beego/session/redis" _ "github.com/beego/beego/session/redis"
"github.com/casdoor/casdoor/authz" "github.com/casdoor/casdoor/authz"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/controllers" "github.com/casdoor/casdoor/ldap"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/routers" "github.com/casdoor/casdoor/routers"
@ -81,7 +81,7 @@ func main() {
// logs.SetLevel(logs.LevelInformational) // logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false) logs.SetLogFuncCall(false)
go controllers.StartLdapServer() go ldap.StartLdapServer()
beego.Run(fmt.Sprintf(":%v", port)) beego.Run(fmt.Sprintf(":%v", port))
} }

View File

@ -115,7 +115,7 @@ func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
} }
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) { func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectclass=*)" SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"} SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
searchReq := goldap.NewSearchRequest("", searchReq := goldap.NewSearchRequest("",
@ -126,7 +126,7 @@ func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
return false, err return false, err
} }
if len(searchResult.Entries) == 0 { if len(searchResult.Entries) == 0 {
return false, errors.New("no result") return false, nil
} }
isMicrosoft := false isMicrosoft := false
var ldapServerType ldapServerType var ldapServerType ldapServerType

View File

@ -1,74 +0,0 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"log"
"strings"
"github.com/forestmgy/ldapserver"
)
func GetNameAndOrgFromDN(DN string) (string, string, string) {
DNValue := strings.Split(DN, ",")
if len(DNValue) == 1 || strings.ToLower(DNValue[0])[0] != 'c' || strings.ToLower(DNValue[1])[0] != 'o' {
return "", "", "please use correct Admin Name format like cn=xxx,ou=xxx,dc=example,dc=com"
}
return DNValue[0][3:], DNValue[1][3:], ""
}
func GetUserNameAndOrgFromBaseDnAndFilter(baseDN, filter string) (string, string, int) {
if !strings.Contains(baseDN, "ou=") || !strings.Contains(filter, "cn=") {
return "", "", ldapserver.LDAPResultInvalidDNSyntax
}
name := getUserNameFromFilter(filter)
_, org, _ := GetNameAndOrgFromDN(fmt.Sprintf("cn=%s,", name) + baseDN)
errCode := ldapserver.LDAPResultSuccess
return name, org, errCode
}
func getUserNameFromFilter(filter string) string {
nameIndex := strings.Index(filter, "cn=")
var name string
for i := nameIndex + 3; filter[i] != ')'; i++ {
name = name + string(filter[i])
}
return name
}
func GetFilteredUsers(m *ldapserver.Message, name, org string) ([]*User, int) {
var filteredUsers []*User
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if m.Client.OrgName == "built-in" && org == "*" {
filteredUsers = GetGlobalUsers()
return filteredUsers, ldapserver.LDAPResultSuccess
} else if m.Client.OrgName == "built-in" || org == m.Client.OrgName {
filteredUsers = GetUsers(org)
return filteredUsers, ldapserver.LDAPResultSuccess
} else {
return nil, ldapserver.LDAPResultInsufficientAccessRights
}
} else {
hasPermission, err := CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), org, true, "en")
if !hasPermission {
log.Printf("ErrMsg = %v", err.Error())
return nil, ldapserver.LDAPResultInsufficientAccessRights
}
user := getUser(org, name)
filteredUsers = append(filteredUsers, user)
return filteredUsers, ldapserver.LDAPResultSuccess
}
}