Commit 11cab73f authored by ale's avatar ale
Browse files

Minor refactoring of LDAP query templates

Rename queryConfig to queryTemplate to better reflect its purpose, and
drop all cruft that had to do with config deserialization.
parent 7cf5e143
......@@ -31,8 +31,8 @@ const (
type backend struct {
conn ldapConn
baseDN string
userQuery *queryConfig
userResourceQueries []*queryConfig
userQuery *queryTemplate
userResourceQueries []*queryTemplate
resources *resourceRegistry
}
......@@ -74,22 +74,23 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
return &backend{
conn: conn,
baseDN: base,
userQuery: mustCompileQueryConfig(&queryConfig{
Base: "uid=${user},ou=People," + base,
Scope: "base",
}),
userResourceQueries: []*queryConfig{
userQuery: &queryTemplate{
Base: joinDN("uid=${user}", "ou=People", base),
Filter: "(objectClass=*)",
Scope: ldap.ScopeBaseObject,
},
userResourceQueries: []*queryTemplate{
// Find all resources that are children of the main uid object.
mustCompileQueryConfig(&queryConfig{
Base: "uid=${user},ou=People," + base,
Scope: "sub",
}),
&queryTemplate{
Base: joinDN("uid=${user}", "ou=People", base),
Scope: ldap.ScopeWholeSubtree,
},
// Find mailing lists, which are nested under a different root.
mustCompileQueryConfig(&queryConfig{
Base: "ou=Lists," + base,
&queryTemplate{
Base: joinDN("ou=Lists", base),
Filter: "(&(objectClass=mailingList)(listOwner=${user}))",
Scope: "one",
}),
Scope: ldap.ScopeSingleLevel,
},
},
resources: rsrc,
}, nil
......@@ -155,13 +156,16 @@ func (tx *backendTX) CreateUser(ctx context.Context, user *accountserver.User) e
func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserver.User, error) {
// First of all, find the main user object, and just that one.
vars := map[string]string{"user": username}
result, err := tx.search(ctx, tx.backend.userQuery.searchRequest(vars, nil))
result, err := tx.search(ctx, tx.backend.userQuery.query(vars))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return nil, nil
}
return nil, err
}
if len(result.Entries) == 0 {
return nil, nil
}
user, err := newUser(result.Entries[0])
if err != nil {
......@@ -172,8 +176,8 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserv
// object we just created.
// TODO: parallelize.
// TODO: add support for non-LDAP resource queries.
for _, query := range tx.backend.userResourceQueries {
result, err = tx.search(ctx, query.searchRequest(vars, nil))
for _, tpl := range tx.backend.userResourceQueries {
result, err = tx.search(ctx, tpl.query(vars))
if err != nil {
continue
}
......@@ -300,16 +304,17 @@ func (tx *backendTX) SetResourcePassword(ctx context.Context, r *accountserver.R
}
func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName string) (bool, error) {
query, err := tx.backend.resources.SearchQuery(resourceType)
tpl, err := tx.backend.resources.SearchQuery(resourceType)
if err != nil {
return false, err
}
// Make a quick LDAP search that only fetches the DN attribute.
result, err := tx.search(ctx, query.searchRequest(map[string]string{
tpl.Attrs = []string{"dn"}
result, err := tx.search(ctx, tpl.query(map[string]string{
"resource": resourceName,
"type": resourceType,
}, []string{"dn"}))
}))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return false, nil
......
......@@ -25,7 +25,7 @@ func startServerAndGetUser2(t testing.TB) (func(), accountserver.Backend, *accou
return startServerAndGetUserWithName(t, testUser2)
}
func startServerAndGetUserWithName(t testing.TB, username string) (func(), accountserver.Backend, *accountserver.User) {
func startServer(t testing.TB) (func(), accountserver.Backend) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../ldaptest",
Port: testLDAPPort,
......@@ -42,6 +42,12 @@ func startServerAndGetUserWithName(t testing.TB, username string) (func(), accou
t.Fatal("NewLDAPBackend", err)
}
return stop, b
}
func startServerAndGetUserWithName(t testing.TB, username string) (func(), accountserver.Backend, *accountserver.User) {
stop, b := startServer(t)
tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), username)
if err != nil {
......@@ -54,6 +60,20 @@ func startServerAndGetUserWithName(t testing.TB, username string) (func(), accou
return stop, b, user
}
func TestModel_GetUser_NotFound(t *testing.T) {
stop, b := startServer(t)
defer stop()
tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), "wrong_user")
if err != nil {
t.Fatalf("GetUser(wrong_user) should have returned no error, got: %v", err)
}
if user != nil {
t.Fatal("GetUser(wrong_user) returned non-nil user")
}
}
func TestModel_GetUser(t *testing.T) {
stop, _, user := startServerAndGetUser(t)
defer stop()
......@@ -217,7 +237,7 @@ func TestModel_HasAnyResource(t *testing.T) {
t.Fatal("HasAnyResource", err)
}
if !ok {
t.Fatal("could not find test resource")
t.Fatal("could not find test email resource")
}
// Request that should fail (bad resource type).
......
......@@ -16,7 +16,7 @@ type resourceHandler interface {
GetDN(accountserver.ResourceID) (string, error)
ToLDAP(*accountserver.Resource) []ldap.PartialAttribute
FromLDAP(*ldap.Entry) (*accountserver.Resource, error)
SearchQuery() *queryConfig
SearchQuery() *queryTemplate
}
// Registry for demultiplexing resource handling. Has a similar
......@@ -101,9 +101,9 @@ func (reg *resourceRegistry) FromLDAPWithType(rsrcType string, entry *ldap.Entry
return
}
func (reg *resourceRegistry) SearchQuery(rsrcType string) (c *queryConfig, err error) {
func (reg *resourceRegistry) SearchQuery(rsrcType string) (q *queryTemplate, err error) {
err = reg.dispatch(rsrcType, func(h resourceHandler) error {
c = h.SearchQuery()
q = h.SearchQuery()
return nil
})
return
......@@ -177,12 +177,12 @@ func (h *emailResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.Parti
}
}
func (h *emailResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *emailResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=People", h.baseDN),
Filter: "(&(objectClass=virtualMailUser)(mail=${resource}))",
Scope: "sub",
})
Scope: ldap.ScopeWholeSubtree,
}
}
// Mailing list resource.
......@@ -222,12 +222,12 @@ func (h *mailingListResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap
}
}
func (h *mailingListResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *mailingListResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=Lists", h.baseDN),
Filter: "(&(objectClass=mailingList)(listName=${resource}))",
Scope: "one",
})
Scope: ldap.ScopeSingleLevel,
}
}
// Website (subsite) resource.
......@@ -289,12 +289,12 @@ func (h *websiteResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.Par
}
}
func (h *websiteResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *websiteResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=People", h.baseDN),
Filter: "(&(objectClass=subSite)(alias=${resource}))",
Scope: "sub",
})
Scope: ldap.ScopeWholeSubtree,
}
}
// Domain (virtual host) resource.
......@@ -350,12 +350,12 @@ func (h *domainResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.Part
}
}
func (h *domainResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *domainResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=People", h.baseDN),
Filter: "(&(objectClass=virtualHost)(cn=${resource}))",
Scope: "sub",
})
Scope: ldap.ScopeWholeSubtree,
}
}
// WebDAV (a.k.a. "ftp account") resource.
......@@ -406,12 +406,12 @@ func (h *webdavResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.Part
}
}
func (h *webdavResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *webdavResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=People", h.baseDN),
Filter: "(&(objectClass=ftpAccount)(ftpname=${resource}))",
Scope: "sub",
})
Scope: ldap.ScopeWholeSubtree,
}
}
// Databases are special: in LDAP, they encode their relation with a
......@@ -514,12 +514,12 @@ func (h *databaseResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.Pa
}
}
func (h *databaseResourceHandler) SearchQuery() *queryConfig {
return mustCompileQueryConfig(&queryConfig{
func (h *databaseResourceHandler) SearchQuery() *queryTemplate {
return &queryTemplate{
Base: joinDN("ou=People", h.baseDN),
Filter: "(&(objectClass=dbMysql)(dbname=${resource}))",
Scope: "sub",
})
Scope: ldap.ScopeWholeSubtree,
}
}
func joinDN(parts ...string) string {
......
package backend
import (
"errors"
"os"
ldaputil "git.autistici.org/ai3/go-common/ldap"
"gopkg.in/ldap.v2"
)
// queryConfig holds the parameters for a single LDAP query.
type queryConfig struct {
Base string
Filter string
Scope string
parsedScope int
// queryTemplate is the template for a single parametrized LDAP query.
type queryTemplate struct {
Base string
Filter string
Scope int
Attrs []string
}
func (q *queryConfig) validate() error {
if q.Base == "" {
return errors.New("empty search base")
func (q *queryTemplate) query(vars map[string]string) *ldap.SearchRequest {
filter := q.Filter
if filter == "" {
filter = "(objectClass=*)"
}
// An empty filter is equivalent to objectClass=*.
if q.Filter == "" {
q.Filter = "(objectClass=*)"
}
q.parsedScope = ldap.ScopeWholeSubtree
if q.Scope != "" {
s, err := ldaputil.ParseScope(q.Scope)
if err != nil {
return err
}
q.parsedScope = s
}
return nil
}
func (q *queryConfig) searchRequest(vars map[string]string, attrs []string) *ldap.SearchRequest {
return ldap.NewSearchRequest(
replaceVars(q.Base, vars),
q.parsedScope,
q.Scope,
ldap.NeverDerefAliases,
0,
0,
false,
replaceVars(q.Filter, vars),
attrs,
replaceVars(filter, vars),
q.Attrs,
nil,
)
}
func mustCompileQueryConfig(q *queryConfig) *queryConfig {
if err := q.validate(); err != nil {
panic(err)
}
return q
}
func replaceVars(s string, vars map[string]string) string {
return os.Expand(s, func(k string) string {
return ldap.EscapeFilter(vars[k])
})
}
// LDAP string to boolean value. There is no schema defined for this,
// we just match a bunch of plausible text truth values ("yes", "on",
// "true", etc).
func s2b(s string) bool {
switch s {
case "yes", "y", "on", "enabled", "true":
......@@ -71,6 +51,7 @@ func s2b(s string) bool {
}
}
// Bool to LDAP value. Encoded as yes/no.
func b2s(b bool) string {
if b {
return "yes"
......@@ -87,6 +68,7 @@ func s2l(s string) []string {
return []string{s}
}
// Returns true if a LDAP object has the specified objectClass.
func isObjectClass(entry *ldap.Entry, class string) bool {
classes := entry.GetAttributeValues("objectClass")
for _, c := range classes {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment