mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-16 18:43:10 -06:00
ldap tests (wip)
This commit is contained in:
parent
de0baf4f8e
commit
623e55a871
5
.github/workflows/dendrite.yml
vendored
5
.github/workflows/dendrite.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
112
clientapi/auth/ldap_authenticator.go
Normal file
112
clientapi/auth/ldap_authenticator.go
Normal 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
|
||||
}
|
||||
95
clientapi/auth/ldap_authenticator_test.go
Normal file
95
clientapi/auth/ldap_authenticator_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
68
test/openldap/bootstrap.ldif
Normal file
68
test/openldap/bootstrap.ldif
Normal 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
|
||||
Loading…
Reference in a new issue