Commit df8a5243 authored by ale's avatar ale
Browse files

Add an overview of the organization of the code

parent 96cf6388
Code organization and how to add functionality
The codebase is still fairly messy: we just haven't found (yet?) a
good way to abstract functions and interfaces that minimizes the
effort of writing business logic.
Anyway, in case you want to add new functionality to the
accountserver, here's a quick overview of what to do and what to
## Adding functionality to the backend
The backend interface is in [service.go](service.go) (the *Backend*
and *TX* interfaces), and has methods to manipulate objects in the
user database. The data types used by this interface (most importantly
*User* and *Resource*) are defined in [types.go](types.go) instead.
In some cases, your new functionality may require changes in the data
model as well. There are two major options available, depending on the
desired visibility of the new data:
### Adding public data
In this case you need to add *publicly-visible* data to the User or to
a Resource. This is relatively straightforward as it requires no
changes to the TX interface.
Suppose you are adding a field called *color* (whatever, doesn't
matter) to email resources.
* Add the *color* field to the *Email* type in types.go:
type Email struct {
Color string `json:"color"`
* The LDAP backend implementation needs to know which LDAP attribute
this field corresponds to. This is done
in [backend/resources.go](backend/resources.go), in the
FromLDAP/ToLDAP methods of the *emailResourceHandler* type:
func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*accountserver.Resource, error) {
return &accountserver.Resource{
Email: &accountserver.Email{
Color: entry.GetAttributeValue("color"),
func (h *emailResourceHandler) ToLDAP(rsrc *accountserver.Resource) []ldap.PartialAttribute {
return []ldap.PartialAttribute{
{Type: "color", Vals: s2l(rsrc.Email.Color)},
* Validation code for the new field must be added
to [validators.go](validators.go) in the
validationContext.validEmailResource function:
func (v *validationContext) validEmailResource() ResourceValidatorFunc {
return func(ctx context.Context, r *Resource, user *User) error {
if r.Email.Color != "blue" {
return errors.New("oh no, the color is wrong")
* If a newly created email resource should have a default value for
the *color* field, it needs to be added to the
templateContext.emailResourceTemplate function in the same
validators.go file:
func (c *templateContext) emailResourceTemplate(ctx context.Context, r *Resource, user *User) {
r.Email.Color = "blue"
### Adding private data
When adding new private data, more complex changes are required: private
data should not be stored in the public data types, but one should use
dedicated methods on the TX interface instead.
Suppose for instance you want to add a new authentication mechanism,
*third-factor authentication* or *3FA*. The authentication handler
(which is not part of accountserver!) requires a secret *magic word*
to authenticate a user, so we need a dedicated TX method to set it. We
would also like to be able to tell whether a user has 3FA enabled or
not (it's nice to show it in the user management panel, for instance),
so we are going to add a boolean *Has3FA* field to the *User* type
This might result in something like:
type TX interface {
SetUser3FAMagicWord(context.Context, *User, string) error
which would then need to be implemented by the LDAP backend.
## Adding functionality to the API
Once the backend changes, where necessary, have been made, you may
want to add a public method to the API, to allow clients to manipulate
the new data. The public API is defined by the *AccountService*
type. This type is defined in [service.go](service.go), which contains
basic private functionality, but all "business logic" methods are
actually in [actions.go](actions.go). The API is then exported as an
HTTP service by [server/server.go](server/server.go).
As an example, let's add a new *SetEmailColor* API method to set the
*color* attribute of email resources from an earlier example
above. We're going to need a request type, but the action returns no
data so we can avoid defining a response type.
There's a bit of boilerplate involved. In brief, this is going to be a
*resource*-level method (since it operates on a specific resource, not
on the user itself), and we're going to allow users to set their own
"email color".
So, in actions.go:
type SetEmailColorRequest struct {
Color string `json:"color"`
func (s *AccountService) SetEmailColor(ctx context.Context, tx TX, req *SetEmailColorRequest) error {
return s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error {
r.Email.Color = req.Color
return tx.UpdateResource(ctx, r)
In reality we will want to do more things, like validating the *color*
field in the incoming request.
Finally, the new API method must be exposed on the HTTP service. In
server/server.go we should create a new handler wrapper function:
func (s *AccountServer) handleSetEmailColor(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.SetEmailColorRequest
return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) {
return s.service.SetEmailColor(ctx, tx, &req)
func (s *AccountServer) Handler() http.Handler {
h.HandleFunc("/api/resource/email/set_color", s.withTx(s.handleSetEmailColor))
Supports Markdown
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