mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-21 13:03:09 -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-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
|
openldap:
|
||||||
|
image: bitnami/openldap:2.5.14
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}/test/openldap:/ldifs
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup go
|
- 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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -91,7 +90,8 @@ func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest)
|
||||||
|
|
||||||
var account *api.Account
|
var account *api.Account
|
||||||
if t.Config.Ldap.Enabled {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -149,97 +149,6 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
|
||||||
}
|
}
|
||||||
return res.Account, nil
|
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) {
|
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain gomatrixserverlib.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
|
||||||
var existing api.QueryAccountByLocalpartResponse
|
var existing api.QueryAccountByLocalpartResponse
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,12 @@ type Ldap struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Uri string `yaml:"uri"`
|
Uri string `yaml:"uri"`
|
||||||
BaseDn string `yaml:"base_dn"`
|
BaseDn string `yaml:"base_dn"`
|
||||||
SearchFilter string `yaml:"search_filter"`
|
|
||||||
SearchAttribute string `yaml:"search_attribute"`
|
|
||||||
AdminBindEnabled bool `yaml:"admin_bind_enabled"`
|
AdminBindEnabled bool `yaml:"admin_bind_enabled"`
|
||||||
AdminBindDn string `yaml:"admin_bind_dn"`
|
AdminBindDn string `yaml:"admin_bind_dn"`
|
||||||
AdminBindPassword string `yaml:"admin_bind_password"`
|
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"`
|
UserBindDn string `yaml:"user_bind_dn"`
|
||||||
AdminGroupDn string `yaml:"admin_group_dn"`
|
AdminGroupDn string `yaml:"admin_group_dn"`
|
||||||
AdminGroupFilter string `yaml:"admin_group_filter"`
|
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