diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acb6b1f7..a10a76b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,18 +9,19 @@ jobs: runs-on: ubuntu-latest services: mysql: - image: mysql:5.7 - env: - MYSQL_DATABASE: casdoor - MYSQL_ROOT_PASSWORD: 123456 - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + image: mysql:5.7 + env: + MYSQL_DATABASE: casdoor + MYSQL_ROOT_PASSWORD: 123456 + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: '^1.16.5' + cache-dependency-path: ./go.mod - name: Tests run: | go test -v $(go list ./...) -tags skipCi @@ -31,14 +32,12 @@ jobs: runs-on: ubuntu-latest needs: [ go-tests ] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: 16 - # cache - - uses: c-hive/gha-yarn-cache@v2 - with: - directory: ./web + cache: 'yarn' + cache-dependency-path: ./web/yarn.lock - run: yarn install && CI=false yarn run build working-directory: ./web @@ -47,10 +46,11 @@ jobs: runs-on: ubuntu-latest needs: [ go-tests ] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: '^1.16.5' + cache-dependency-path: ./go.mod - run: go version - name: Build run: | @@ -63,13 +63,14 @@ jobs: needs: [ go-tests ] steps: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: '^1.16.5' + cache-dependency-path: ./go.mod # gen a dummy config file - run: touch dummy.yml - + - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: @@ -82,35 +83,35 @@ jobs: needs: [ go-tests ] services: mysql: - image: mysql:5.7 - env: - MYSQL_DATABASE: casdoor - MYSQL_ROOT_PASSWORD: 123456 - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + image: mysql:5.7 + env: + MYSQL_DATABASE: casdoor + MYSQL_ROOT_PASSWORD: 123456 + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: '^1.16.5' - - uses: actions/setup-node@v2 - with: - node-version: 16 - - name: back start + cache-dependency-path: ./go.mod + - name: start backend run: nohup go run ./main.go & working-directory: ./ - - name: front install - run: yarn install - working-directory: ./web - - name: front start - run: nohup yarn start & - working-directory: ./web - - uses: cypress-io/github-action@v4 + - uses: actions/setup-node@v3 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-timeout: 180 + working-directory: ./web - uses: actions/upload-artifact@v3 if: failure() @@ -121,7 +122,7 @@ jobs: if: always() with: name: cypress-videos - path: ./web/cypress/videos + path: ./web/cypress/videos release-and-push: name: Release And Push @@ -130,11 +131,11 @@ jobs: needs: [ frontend, backend, linter, e2e ] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: -1 - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: 16 @@ -166,10 +167,10 @@ jobs: elif [ ${old_array[1]} != ${new_array[1]} ] then echo ::set-output name=push::'true' - + else echo ::set-output name=push::'false' - + fi - name: Set up QEMU diff --git a/controllers/ldapserver.go b/ldap/server.go similarity index 54% rename from controllers/ldapserver.go rename to ldap/server.go index daeb0295..fc488ae5 100644 --- a/controllers/ldapserver.go +++ b/ldap/server.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package controllers +package ldap import ( "fmt" @@ -20,76 +20,78 @@ import ( "github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/object" - "github.com/forestmgy/ldapserver" + ldap "github.com/forestmgy/ldapserver" "github.com/lor00x/goldap/message" ) func StartLdapServer() { - server := ldapserver.NewServer() - routes := ldapserver.NewRouteMux() + server := ldap.NewServer() + routes := ldap.NewRouteMux() routes.Bind(handleBind) routes.Search(handleSearch).Label(" SEARCH****") 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() - res := ldapserver.NewBindResponse(ldapserver.LDAPResultSuccess) + res := ldap.NewBindResponse(ldap.LDAPResultSuccess) if r.AuthenticationChoice() == "simple" { - bindusername, bindorg, err := object.GetNameAndOrgFromDN(string(r.Name())) + bindUsername, bindOrg, err := getNameAndOrgFromDN(string(r.Name())) if err != "" { log.Printf("Bind failed ,ErrMsg=%s", err) - res.SetResultCode(ldapserver.LDAPResultInvalidDNSyntax) + res.SetResultCode(ldap.LDAPResultInvalidDNSyntax) res.SetDiagnosticMessage("bind failed ErrMsg: " + err) w.Write(res) 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 != "" { 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) w.Write(res) return } - if bindorg == "built-in" { + + if bindOrg == "built-in" || bindUser.IsGlobalAdmin { m.Client.IsGlobalAdmin, m.Client.IsOrgAdmin = true, true - } else if binduser.IsAdmin { + } else if bindUser.IsAdmin { m.Client.IsOrgAdmin = true } + m.Client.IsAuthenticated = true - m.Client.UserName = bindusername - m.Client.OrgName = bindorg + m.Client.UserName = bindUsername + m.Client.OrgName = bindOrg } else { - res.SetResultCode(ldapserver.LDAPResultAuthMethodNotSupported) + res.SetResultCode(ldap.LDAPResultAuthMethodNotSupported) res.SetDiagnosticMessage("Authentication method not supported,Please use Simple Authentication") } w.Write(res) } -func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) { - res := ldapserver.NewSearchResultDoneResponse(ldapserver.LDAPResultSuccess) +func handleSearch(w ldap.ResponseWriter, m *ldap.Message) { + res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess) if !m.Client.IsAuthenticated { - res.SetResultCode(ldapserver.LDAPResultUnwillingToPerform) + res.SetResultCode(ldap.LDAPResultUnwillingToPerform) w.Write(res) return } + r := m.GetSearchRequest() if r.FilterString() == "(objectClass=*)" { w.Write(res) 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....) select { case <-m.Done: @@ -97,16 +99,17 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) { return default: } - users, errCode := object.GetFilteredUsers(m, name, org) - if errCode != ldapserver.LDAPResultSuccess { - res.SetResultCode(errCode) + + users, code := GetFilteredUsers(m) + if code != ldap.LDAPResultSuccess { + res.SetResultCode(code) w.Write(res) 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())) - e := ldapserver.NewSearchResultEntry(dn) + e := ldap.NewSearchResultEntry(dn) e.AddAttribute("cn", message.AttributeValue(user.Name)) e.AddAttribute("uid", message.AttributeValue(user.Name)) e.AddAttribute("email", message.AttributeValue(user.Email)) @@ -117,22 +120,3 @@ func handleSearch(w ldapserver.ResponseWriter, m *ldapserver.Message) { } 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) -} diff --git a/ldap/util.go b/ldap/util.go new file mode 100644 index 00000000..640bc929 --- /dev/null +++ b/ldap/util.go @@ -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) +} diff --git a/main.go b/main.go index 4fbf07e8..09b565f1 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( _ "github.com/beego/beego/session/redis" "github.com/casdoor/casdoor/authz" "github.com/casdoor/casdoor/conf" - "github.com/casdoor/casdoor/controllers" + "github.com/casdoor/casdoor/ldap" "github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/routers" @@ -81,7 +81,7 @@ func main() { // logs.SetLevel(logs.LevelInformational) logs.SetLogFuncCall(false) - go controllers.StartLdapServer() + go ldap.StartLdapServer() beego.Run(fmt.Sprintf(":%v", port)) } diff --git a/object/ldap.go b/object/ldap.go index 7c82e818..f7f441c5 100644 --- a/object/ldap.go +++ b/object/ldap.go @@ -115,7 +115,7 @@ func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser { } func isMicrosoftAD(Conn *goldap.Conn) (bool, error) { - SearchFilter := "(objectclass=*)" + SearchFilter := "(objectClass=*)" SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"} searchReq := goldap.NewSearchRequest("", @@ -126,7 +126,7 @@ func isMicrosoftAD(Conn *goldap.Conn) (bool, error) { return false, err } if len(searchResult.Entries) == 0 { - return false, errors.New("no result") + return false, nil } isMicrosoft := false var ldapServerType ldapServerType diff --git a/object/ldapserver.go b/object/ldapserver.go deleted file mode 100644 index 3156748f..00000000 --- a/object/ldapserver.go +++ /dev/null @@ -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 - } -}