Commit 496a4c6f authored by ale's avatar ale

Move some functions out of model.go to make it more readable

parent 66204a81
A few notes on the data model
======
The data model used by the accountserver tries to be straightforward
and simple, but there are numerous aspects about the assumed semantics
that require further discussion.
At its core, it represents *users* and their associated
*resources*. Resources can represent accounts on services of different
type, and can be nested if there is a direct and relevant hierarchical
relation.
## Authentication
The major issue with the LDAP backend is that it's not easy to do
joins (you need to do two separate queries, and most open source
clients aren't even coded to do that). This makes it hard to go, for
example, from the email resource object to the main user account
object.
The issue of authentication is made complex by the choice of specific
services and desired UX that we have adopted. Specifically, the
decision of having the e-mail coincide with the primary user identity
(along with the availability of web-based single sign-on), and the UX
decision of presenting a single "authentication control surface" to
the user (so that, for instance, you don't have to separately set up
2FA for the main user account and your email) determine the need to
unify authentication details for the main user object and the email
resource(s).
In practice, this means that the API offers its authentication-related
methods on the user object rather than on the email resource. While
the implementation on a SQL backend would be straightforward
(credentials would simply be fields, or subtables, of the *user*
table), in the LDAP backend we are going to rely on denormalization
(i.e. copying the same attribute to multiple objects) and careful
splitting of some attributes to where they are used. For an example of
the latter, consider 2FA, where the split between interactive and
non-interactive clients allows us to:
* put the TOTP secret on the user object, because that's used by the
interactive authentication service (the single sign-on login server
application);
* put application-specific passwords (used by non-interactive clients)
on the email resource LDAP objects, which is where the email service
authenticates its users. Same for the storage encryption keys.
......@@ -2,8 +2,6 @@ package backend
import (
"context"
"errors"
"os"
"strings"
ldaputil "git.autistici.org/ai3/go-common/ldap"
......@@ -23,15 +21,6 @@ const (
passwordLDAPAttr = "userPassword"
)
// Generic interface to LDAP - allows us to stub out the LDAP client while
// testing.
type ldapConn interface {
Search(context.Context, *ldap.SearchRequest) (*ldap.SearchResult, error)
Add(context.Context, *ldap.AddRequest) error
Modify(context.Context, *ldap.ModifyRequest) error
Close()
}
// backend is the interface to an LDAP-backed user database.
//
// We keep a set of LDAP queries for each resource type, each having a
......@@ -105,85 +94,6 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
}, nil
}
func replaceVars(s string, vars map[string]string) string {
return os.Expand(s, func(k string) string {
return ldap.EscapeFilter(vars[k])
})
}
// queryConfig holds the parameters for a single LDAP query.
type queryConfig struct {
Base string
Filter string
Scope string
parsedScope int
}
func (q *queryConfig) validate() error {
if q.Base == "" {
return errors.New("empty search base")
}
// 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,
ldap.NeverDerefAliases,
0,
0,
false,
replaceVars(q.Filter, vars),
attrs,
nil,
)
}
func mustCompileQueryConfig(q *queryConfig) *queryConfig {
if err := q.validate(); err != nil {
panic(err)
}
return q
}
func s2b(s string) bool {
switch s {
case "yes", "y", "on", "enabled", "true":
return true
default:
return false
}
}
func b2s(b bool) string {
if b {
return "yes"
}
return "no"
}
// Convert a string to a []string with a single item, or nil if the
// string is empty. Useful for optional single-valued LDAP attributes.
func s2l(s string) []string {
if s == "" {
return nil
}
return []string{s}
}
func newUser(entry *ldap.Entry) (*accountserver.User, error) {
user := &accountserver.User{
Name: entry.GetAttributeValue("uid"),
......@@ -427,16 +337,6 @@ func (tx *backendTX) UpdateResource(ctx context.Context, r *accountserver.Resour
return nil
}
func isObjectClass(entry *ldap.Entry, class string) bool {
classes := entry.GetAttributeValues("objectClass")
for _, c := range classes {
if c == class {
return true
}
}
return false
}
var siteRoot = "/home/users/investici.org/"
// The hosting directory for a website is the path component immediately after
......
......@@ -8,6 +8,15 @@ import (
"gopkg.in/ldap.v2"
)
// Generic interface to LDAP - allows us to stub out the LDAP client while
// testing.
type ldapConn interface {
Search(context.Context, *ldap.SearchRequest) (*ldap.SearchResult, error)
Add(context.Context, *ldap.AddRequest) error
Modify(context.Context, *ldap.ModifyRequest) error
Close()
}
type ldapAttr struct {
dn, attr string
values []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
}
func (q *queryConfig) validate() error {
if q.Base == "" {
return errors.New("empty search base")
}
// 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,
ldap.NeverDerefAliases,
0,
0,
false,
replaceVars(q.Filter, vars),
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])
})
}
func s2b(s string) bool {
switch s {
case "yes", "y", "on", "enabled", "true":
return true
default:
return false
}
}
func b2s(b bool) string {
if b {
return "yes"
}
return "no"
}
// Convert a string to a []string with a single item, or nil if the
// string is empty. Useful for optional single-valued LDAP attributes.
func s2l(s string) []string {
if s == "" {
return nil
}
return []string{s}
}
func isObjectClass(entry *ldap.Entry, class string) bool {
classes := entry.GetAttributeValues("objectClass")
for _, c := range classes {
if c == class {
return true
}
}
return false
}
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