From df8a5243eb80bc0009e6c38faf0ced05b844a30a Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Sat, 30 Jun 2018 08:38:38 +0100 Subject: [PATCH] Add an overview of the organization of the code --- CONTRIBUTING.md | 177 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..97d65cb6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,177 @@ +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)) +} +``` -- GitLab