CONTRIBUTING.md 5.86 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 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))
}
```