Commit df8a5243 authored by ale's avatar ale

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
consider:
## 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
too.
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 {
ResourceRequestBase
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))
}
```
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