ldap tests (wip)

This commit is contained in:
Boris Rybalkin 2023-02-23 00:05:25 +00:00
parent de0baf4f8e
commit 623e55a871
6 changed files with 285 additions and 95 deletions

View file

@ -91,6 +91,11 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
openldap:
image: bitnami/openldap:2.5.14
volumes:
- ${{ github.workspace }}/test/openldap:/ldifs
steps:
- uses: actions/checkout@v3
- name: Setup go

View file

@ -0,0 +1,112 @@
package auth
import (
"github.com/go-ldap/ldap/v3"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/util"
"net/http"
"strings"
)
type LdapAuthenticator struct {
config config.Ldap
}
func NewLdapAuthenticator(config config.Ldap) *LdapAuthenticator {
return &LdapAuthenticator{
config: config,
}
}
func (l *LdapAuthenticator) Authenticate(username, password string) (bool, *util.JSONResponse) {
var conn *ldap.Conn
conn, err := ldap.DialURL(l.config.Uri)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to connect to ldap: " + err.Error()),
}
}
defer conn.Close()
if l.config.AdminBindEnabled {
err = conn.Bind(l.config.AdminBindDn, l.config.AdminBindPassword)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to bind to ldap: " + err.Error()),
}
}
filter := strings.ReplaceAll(l.config.SearchFilter, "{username}", username)
searchRequest := ldap.NewSearchRequest(
l.config.SearchBaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
0, 0, false, filter, []string{l.config.SearchAttribute}, nil,
)
result, err := conn.Search(searchRequest)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to bind to search ldap: " + err.Error()),
}
}
if len(result.Entries) > 1 {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.BadJSON("'user' must be duplicated."),
}
}
if len(result.Entries) < 1 {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.BadJSON("'user' not found."),
}
}
userDN := result.Entries[0].DN
err = conn.Bind(userDN, password)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
} else {
bindDn := strings.ReplaceAll(l.config.UserBindDn, "{username}", username)
err = conn.Bind(bindDn, password)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
}
isAdmin, err := l.isLdapAdmin(conn, username)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
return isAdmin, nil
}
func (l *LdapAuthenticator) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
searchRequest := ldap.NewSearchRequest(
l.config.AdminGroupDn,
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
strings.ReplaceAll(l.config.AdminGroupFilter, "{username}", username),
[]string{l.config.AdminGroupAttribute},
nil)
sr, err := conn.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) < 1 {
return false, nil
}
return true, nil
}

View file

@ -0,0 +1,95 @@
package auth
import (
"github.com/matrix-org/dendrite/setup/config"
"github.com/stretchr/testify/assert"
"testing"
)
func TestLdapAuthenticator_Authenticate_DirectBind_AdminUser(t *testing.T) {
authenticator := NewLdapAuthenticator(config.Ldap{
Uri: "ldap://openldap:1389",
BaseDn: "dc=example,dc=org",
AdminBindEnabled: false,
UserBindDn: "cn={username},ou=users,dc=example,dc=org",
AdminGroupDn: "cn=admin,ou=groups,dc=example,dc=org",
AdminGroupFilter: "(memberUid={username})",
AdminGroupAttribute: "memberUid",
})
isAdmin, err := authenticator.Authenticate("user1", "password")
assert.Nil(t, err)
assert.True(t, isAdmin)
}
func TestLdapAuthenticator_Authenticate_DirectBind_RegularUser(t *testing.T) {
authenticator := NewLdapAuthenticator(config.Ldap{
Uri: "ldap://openldap:1389",
BaseDn: "dc=example,dc=org",
AdminBindEnabled: false,
UserBindDn: "cn={username},ou=users,dc=example,dc=org",
AdminGroupDn: "cn=admin,ou=groups,dc=example,dc=org",
AdminGroupFilter: "(memberUid={username})",
AdminGroupAttribute: "memberUid",
})
isAdmin, err := authenticator.Authenticate("user2", "password")
assert.Nil(t, err)
assert.False(t, isAdmin)
}
func TestLdapAuthenticator_Authenticate_AdminBind(t *testing.T) {
authenticator := NewLdapAuthenticator(config.Ldap{
Uri: "ldap://openldap:1389",
BaseDn: "dc=example,dc=org",
AdminBindEnabled: true,
AdminBindDn: "cn=admin,dc=example,dc=org",
AdminBindPassword: "password",
AdminGroupDn: "cn=admin,ou=groups,dc=example,dc=org",
AdminGroupFilter: "(memberUid={username})",
AdminGroupAttribute: "memberUid",
SearchBaseDn: "ou=users,dc=example,dc=org",
SearchFilter: "(&(objectclass=inetOrgPerson)(cn={username}))",
SearchAttribute: "cn",
})
isAdmin, err := authenticator.Authenticate("user1", "password")
assert.Nil(t, err)
assert.True(t, isAdmin)
}
func TestLdapAuthenticator_Authenticate_AdminBind_UserNotFound(t *testing.T) {
authenticator := NewLdapAuthenticator(config.Ldap{
Uri: "ldap://openldap:1389",
BaseDn: "dc=example,dc=org",
AdminBindEnabled: true,
AdminBindDn: "cn=admin,dc=example,dc=org",
AdminBindPassword: "password",
AdminGroupDn: "cn=admin,ou=groups,dc=example,dc=org",
AdminGroupFilter: "(memberUid={username})",
AdminGroupAttribute: "memberUid",
SearchBaseDn: "ou=users,dc=example,dc=org",
SearchFilter: "(&(objectclass=inetOrgPerson)(cn={username}))",
SearchAttribute: "cn",
})
_, err := authenticator.Authenticate("user_not_found", "")
assert.NotNil(t, err)
}
func TestLdapAuthenticator_Authenticate_DirectBind_WrongPassword(t *testing.T) {
authenticator := NewLdapAuthenticator(config.Ldap{
Uri: "ldap://openldap:1389",
BaseDn: "dc=example,dc=org",
UserBindDn: "cn={username},ou=users,dc=example,dc=org",
AdminBindEnabled: false,
})
_, err := authenticator.Authenticate("user2", "password_wrong")
assert.NotNil(t, err)
}

View file

@ -17,7 +17,6 @@ package auth
import (
"context"
"database/sql"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/matrix-org/gomatrixserverlib"
"net/http"
@ -91,7 +90,8 @@ func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest)
var account *api.Account
if t.Config.Ldap.Enabled {
isAdmin, err := t.authenticateLdap(username, request.Password)
ldapAuthenticator := NewLdapAuthenticator(t.Config.Ldap)
isAdmin, err := ldapAuthenticator.Authenticate(username, request.Password)
if err != nil {
return nil, err
}
@ -149,97 +149,6 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
}
return res.Account, nil
}
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
var conn *ldap.Conn
conn, err := ldap.DialURL(t.Config.Ldap.Uri)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to connect to ldap: " + err.Error()),
}
}
defer conn.Close()
if t.Config.Ldap.AdminBindEnabled {
err = conn.Bind(t.Config.Ldap.AdminBindDn, t.Config.Ldap.AdminBindPassword)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to bind to ldap: " + err.Error()),
}
}
filter := strings.ReplaceAll(t.Config.Ldap.SearchFilter, "{username}", username)
searchRequest := ldap.NewSearchRequest(
t.Config.Ldap.BaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
0, 0, false, filter, []string{t.Config.Ldap.SearchAttribute}, nil,
)
result, err := conn.Search(searchRequest)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("unable to bind to search ldap: " + err.Error()),
}
}
if len(result.Entries) > 1 {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.BadJSON("'user' must be duplicated."),
}
}
if len(result.Entries) < 1 {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.BadJSON("'user' not found."),
}
}
userDN := result.Entries[0].DN
err = conn.Bind(userDN, password)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
} else {
bindDn := strings.ReplaceAll(t.Config.Ldap.UserBindDn, "{username}", username)
err = conn.Bind(bindDn, password)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
}
isAdmin, err := t.isLdapAdmin(conn, username)
if err != nil {
return false, &util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: jsonerror.InvalidUsername(err.Error()),
}
}
return isAdmin, nil
}
func (t *LoginTypePassword) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
searchRequest := ldap.NewSearchRequest(
t.Config.Ldap.AdminGroupDn,
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
strings.ReplaceAll(t.Config.Ldap.AdminGroupFilter, "{username}", username),
[]string{t.Config.Ldap.AdminGroupAttribute},
nil)
sr, err := conn.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) < 1 {
return false, nil
}
return true, nil
}
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain gomatrixserverlib.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
var existing api.QueryAccountByLocalpartResponse

View file

@ -60,11 +60,12 @@ type Ldap struct {
Enabled bool `yaml:"enabled"`
Uri string `yaml:"uri"`
BaseDn string `yaml:"base_dn"`
SearchFilter string `yaml:"search_filter"`
SearchAttribute string `yaml:"search_attribute"`
AdminBindEnabled bool `yaml:"admin_bind_enabled"`
AdminBindDn string `yaml:"admin_bind_dn"`
AdminBindPassword string `yaml:"admin_bind_password"`
SearchBaseDn string `yaml:"search_base_dn"`
SearchFilter string `yaml:"search_filter"`
SearchAttribute string `yaml:"search_attribute"`
UserBindDn string `yaml:"user_bind_dn"`
AdminGroupDn string `yaml:"admin_group_dn"`
AdminGroupFilter string `yaml:"admin_group_filter"`

View file

@ -0,0 +1,68 @@
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organizationalUnit
# administrator
dn: cn=admin,dc=example,dc=org
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: Administrator
userPassword: password
# Subtree for Users
dn: ou=users,dc=example,dc=org
ou: Users
description: Users
objectClass: organizationalUnit
objectClass: top
# admin user
dn: cn=user1,ou=users,dc=example,dc=org
objectClass: simpleSecurityObject
objectClass: Person
objectClass: inetOrgPerson
objectClass: posixAccount
uidNumber: 10
gidNumber: 10
homeDirectory: /home/user1
uid: user1
cn: user1
sn: 10
displayName: user1
description: user1
userPassword: user1
mail: user1@example.com
# regular user
dn: cn=user2,ou=users,dc=example,dc=org
objectClass: simpleSecurityObject
objectClass: Person
objectClass: inetOrgPerson
objectClass: posixAccount
uidNumber: 11
gidNumber: 11
homeDirectory: /home/user2
uid: user2
cn: user2
sn: 11
displayName: user2
description: user2
userPassword: user2
mail: user2@example.com
# Subtree for Groups
dn: ou=groups,dc=example,dc=org
ou: Groups
description: Groups
objectClass: organizationalUnit
objectClass: top
# Admin group
dn: cn=admin,ou=groups,dc=example,dc=org
objectClass: posixGroup
objectClass: top
gidNumber: 1
cn: admin
description: admin
memberUid: user1