auth
auth/client.go
Move HTTP tests to http_test.go, rename the GetGrant test. Rename the GetGrant test to something that makes it more obvious, and indicate that we're only testing for successful responses in this function (e.g., responses that should be successfully handled). Another function will deal with failure modes. Move the function to a new http_test.go file. The model files are shouldn't have information about how the models are being represented.
1 package auth
3 import (
4 "errors"
5 "net/url"
6 "strings"
7 "time"
9 "code.secondbit.org/uuid"
10 )
12 var (
13 ErrNoClientStore = errors.New("no ClientStore was specified for the Context")
14 ErrClientNotFound = errors.New("client not found in ClientStore")
15 ErrClientAlreadyExists = errors.New("client already exists in ClientStore")
17 ErrEmptyChange = errors.New("change must have at least one change in it")
18 ErrClientNameTooShort = errors.New("client name must be at least 2 characters")
19 ErrClientNameTooLong = errors.New("client name must be at most 32 characters")
20 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters")
21 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL")
22 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters")
23 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL")
24 )
26 // Client represents a client that grants access
27 // to the auth server, exchanging grants for tokens,
28 // and tokens for access.
29 type Client struct {
30 ID uuid.ID
31 Secret string
32 OwnerID uuid.ID
33 Name string
34 Logo string
35 Website string
36 Type string
37 }
39 func (c *Client) ApplyChange(change ClientChange) {
40 if change.Secret != nil {
41 c.Secret = *change.Secret
42 }
43 if change.OwnerID != nil {
44 c.OwnerID = change.OwnerID
45 }
46 if change.Name != nil {
47 c.Name = *change.Name
48 }
49 if change.Logo != nil {
50 c.Logo = *change.Logo
51 }
52 if change.Website != nil {
53 c.Website = *change.Website
54 }
55 }
57 type ClientChange struct {
58 Secret *string
59 OwnerID uuid.ID
60 Name *string
61 Logo *string
62 Website *string
63 }
65 func (c ClientChange) Validate() error {
66 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
67 return ErrEmptyChange
68 }
69 if c.Name != nil && len(*c.Name) < 2 {
70 return ErrClientNameTooShort
71 }
72 if c.Name != nil && len(*c.Name) > 32 {
73 return ErrClientNameTooLong
74 }
75 if c.Logo != nil && *c.Logo != "" {
76 if len(*c.Logo) > 1024 {
77 return ErrClientLogoTooLong
78 }
79 u, err := url.Parse(*c.Logo)
80 if err != nil || !u.IsAbs() {
81 return ErrClientLogoNotURL
82 }
83 }
84 if c.Website != nil && *c.Website != "" {
85 if len(*c.Website) > 140 {
86 return ErrClientWebsiteTooLong
87 }
88 u, err := url.Parse(*c.Website)
89 if err != nil || !u.IsAbs() {
90 return ErrClientWebsiteNotURL
91 }
92 }
93 return nil
94 }
96 type Endpoint struct {
97 ID uuid.ID
98 ClientID uuid.ID
99 URI url.URL
100 Added time.Time
101 }
103 type sortedEndpoints []Endpoint
105 func (s sortedEndpoints) Len() int {
106 return len(s)
107 }
109 func (s sortedEndpoints) Less(i, j int) bool {
110 return s[i].Added.Before(s[j].Added)
111 }
113 func (s sortedEndpoints) Swap(i, j int) {
114 s[i], s[j] = s[j], s[i]
115 }
117 // ClientStore abstracts the storage interface for
118 // storing and retrieving Clients.
119 type ClientStore interface {
120 GetClient(id uuid.ID) (Client, error)
121 SaveClient(client Client) error
122 UpdateClient(id uuid.ID, change ClientChange) error
123 DeleteClient(id uuid.ID) error
124 ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
126 AddEndpoint(client uuid.ID, endpoint Endpoint) error
127 RemoveEndpoint(client, endpoint uuid.ID) error
128 CheckEndpoint(client uuid.ID, endpoint string) (bool, error)
129 ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
130 }
132 func (m *Memstore) GetClient(id uuid.ID) (Client, error) {
133 m.clientLock.RLock()
134 defer m.clientLock.RUnlock()
135 c, ok := m.clients[id.String()]
136 if !ok {
137 return Client{}, ErrClientNotFound
138 }
139 return c, nil
140 }
142 func (m *Memstore) SaveClient(client Client) error {
143 m.clientLock.Lock()
144 defer m.clientLock.Unlock()
145 if _, ok := m.clients[client.ID.String()]; ok {
146 return ErrClientAlreadyExists
147 }
148 m.clients[client.ID.String()] = client
149 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
150 return nil
151 }
153 func (m *Memstore) UpdateClient(id uuid.ID, change ClientChange) error {
154 m.clientLock.Lock()
155 defer m.clientLock.Unlock()
156 c, ok := m.clients[id.String()]
157 if !ok {
158 return ErrClientNotFound
159 }
160 c.ApplyChange(change)
161 m.clients[id.String()] = c
162 return nil
163 }
165 func (m *Memstore) DeleteClient(id uuid.ID) error {
166 client, err := m.GetClient(id)
167 if err != nil {
168 return err
169 }
170 m.clientLock.Lock()
171 defer m.clientLock.Unlock()
172 delete(m.clients, id.String())
173 pos := -1
174 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
175 if item.Equal(id) {
176 pos = p
177 break
178 }
179 }
180 if pos >= 0 {
181 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
182 }
183 return nil
184 }
186 func (m *Memstore) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
187 ids := m.lookupClientsByProfileID(ownerID.String())
188 if len(ids) > num+offset {
189 ids = ids[offset : num+offset]
190 } else if len(ids) > offset {
191 ids = ids[offset:]
192 } else {
193 return []Client{}, nil
194 }
195 clients := []Client{}
196 for _, id := range ids {
197 client, err := m.GetClient(id)
198 if err != nil {
199 return []Client{}, err
200 }
201 clients = append(clients, client)
202 }
203 return clients, nil
204 }
206 func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error {
207 m.endpointLock.Lock()
208 defer m.endpointLock.Unlock()
209 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
210 return nil
211 }
213 func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error {
214 m.endpointLock.Lock()
215 defer m.endpointLock.Unlock()
216 pos := -1
217 for p, item := range m.endpoints[client.String()] {
218 if item.ID.Equal(endpoint) {
219 pos = p
220 break
221 }
222 }
223 if pos >= 0 {
224 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
225 }
226 return nil
227 }
229 func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) {
230 m.endpointLock.RLock()
231 defer m.endpointLock.RUnlock()
232 for _, candidate := range m.endpoints[client.String()] {
233 if strings.HasPrefix(endpoint, candidate.URI.String()) {
234 return true, nil
235 }
236 }
237 return false, nil
238 }
240 func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
241 m.endpointLock.RLock()
242 defer m.endpointLock.RUnlock()
243 return m.endpoints[client.String()], nil
244 }