Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
ai3
accountserver
Commits
496a4c6f
Commit
496a4c6f
authored
Jun 22, 2018
by
ale
Browse files
Move some functions out of model.go to make it more readable
parent
66204a81
Changes
4
Hide whitespace changes
Inline
Side-by-side
DATAMODEL.md
0 → 100644
View file @
496a4c6f
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.
backend/model.go
View file @
496a4c6f
...
...
@@ -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
...
...
backend/tx.go
View file @
496a4c6f
...
...
@@ -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
...
...
backend/util.go
0 → 100644
View file @
496a4c6f
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
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment