auth
auth/client.go
Added validation for clients, split endpoints out. Split endpoints out into their own type and added associated methods to the ClientStores, so now each client can have more than one redirect endpoint. Added unit testing for endpoint methods. Added validation code to validate client changes.
1 package auth
3 import (
4 "errors"
5 "net/url"
6 "time"
8 "strings"
9 "secondbit.org/uuid"
10 )
12 var (
13 ErrClientNotFound = errors.New("Client not found in ClientStore.")
14 ErrClientAlreadyExists = errors.New("Client already exists in ClientStore.")
16 ErrClientNameTooShort = errors.New("Client name must be at least 2 characters.")
17 ErrClientNameTooLong = errors.New("Client name must be at most 32 characters.")
18 ErrClientLogoTooShort = errors.New("Client logo URL must be at least 12 characters.")
19 ErrClientLogoTooLong = errors.New("Client logo must be at most 1024 characters.")
20 ErrClientWebsiteTooShort = errors.New("Client website URL must be at least 12 characters.")
21 ErrClientWebsiteTooLong = errors.New("Client website must be at most 1024 characters.")
22 )
24 // Client represents a client that grants access
25 // to the auth server, exchanging grants for tokens,
26 // and tokens for access.
27 type Client struct {
28 ID uuid.ID
29 Secret string
30 OwnerID uuid.ID
31 Name string
32 Logo string
33 Website string
34 Type string
35 }
37 func (c *Client) ApplyChange(change ClientChange) {
38 if change.Secret != nil {
39 c.Secret = *change.Secret
40 }
41 if change.OwnerID != nil {
42 c.OwnerID = change.OwnerID
43 }
44 if change.Name != nil {
45 c.Name = *change.Name
46 }
47 if change.Logo != nil {
48 c.Logo = *change.Logo
49 }
50 if change.Website != nil {
51 c.Website = *change.Website
52 }
53 }
55 type ClientChange struct {
56 Secret *string
57 OwnerID uuid.ID
58 Name *string
59 Logo *string
60 Website *string
61 }
63 func (c ClientChange) Validate() error {
64 if c.Name != nil && len(*c.Name) < 2 {
65 return ErrClientNameTooShort
66 }
67 if c.Name != nil && len(*c.Name) > 32 {
68 return ErrClientNameTooLong
69 }
70 if c.Logo != nil && len(*c.Logo) > 1024 {
71 return ErrClientLogoTooLong
72 }
73 if c.Logo != nil && len(*c.Logo) > 0 && len(*c.Logo) < 12 {
74 return ErrClientLogoTooShort
75 }
76 if c.Website != nil && len(*c.Website) > 140 {
77 return ErrClientWebsiteTooLong
78 }
79 if c.Website != nil && len(*c.Website) > 0 && len(*c.Website) < 12 {
80 return ErrClientWebsiteTooShort
81 }
82 return nil
83 }
85 type Endpoint struct {
86 ID uuid.ID
87 ClientID uuid.ID
88 URI url.URL
89 Added time.Time
90 }
92 type sortedEndpoints []Endpoint
94 func (s sortedEndpoints) Len() int {
95 return len(s)
96 }
98 func (s sortedEndpoints) Less(i, j int) bool {
99 return s[i].Added.Before(s[j].Added)
100 }
102 func (s sortedEndpoints) Swap(i, j int) {
103 s[i], s[j] = s[j], s[i]
104 }
106 // ClientStore abstracts the storage interface for
107 // storing and retrieving Clients.
108 type ClientStore interface {
109 GetClient(id uuid.ID) (Client, error)
110 SaveClient(client Client) error
111 UpdateClient(id uuid.ID, change ClientChange) error
112 DeleteClient(id uuid.ID) error
113 ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
115 AddEndpoint(client uuid.ID, endpoint Endpoint) error
116 RemoveEndpoint(client, endpoint uuid.ID) error
117 CheckEndpoint(client uuid.ID, endpoint string) (bool, error)
118 ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
119 }
121 func (m *Memstore) GetClient(id uuid.ID) (Client, error) {
122 m.clientLock.RLock()
123 defer m.clientLock.RUnlock()
124 c, ok := m.clients[id.String()]
125 if !ok {
126 return Client{}, ErrClientNotFound
127 }
128 return c, nil
129 }
131 func (m *Memstore) SaveClient(client Client) error {
132 m.clientLock.Lock()
133 defer m.clientLock.Unlock()
134 if _, ok := m.clients[client.ID.String()]; ok {
135 return ErrClientAlreadyExists
136 }
137 m.clients[client.ID.String()] = client
138 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
139 return nil
140 }
142 func (m *Memstore) UpdateClient(id uuid.ID, change ClientChange) error {
143 m.clientLock.Lock()
144 defer m.clientLock.Unlock()
145 c, ok := m.clients[id.String()]
146 if !ok {
147 return ErrClientNotFound
148 }
149 c.ApplyChange(change)
150 m.clients[id.String()] = c
151 return nil
152 }
154 func (m *Memstore) DeleteClient(id uuid.ID) error {
155 client, err := m.GetClient(id)
156 if err != nil {
157 return err
158 }
159 m.clientLock.Lock()
160 defer m.clientLock.Unlock()
161 delete(m.clients, id.String())
162 pos := -1
163 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
164 if item.Equal(id) {
165 pos = p
166 break
167 }
168 }
169 if pos >= 0 {
170 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
171 }
172 return nil
173 }
175 func (m *Memstore) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
176 ids := m.lookupClientsByProfileID(ownerID.String())
177 if len(ids) > num+offset {
178 ids = ids[offset : num+offset]
179 } else if len(ids) > offset {
180 ids = ids[offset:]
181 } else {
182 return []Client{}, nil
183 }
184 clients := []Client{}
185 for _, id := range ids {
186 client, err := m.GetClient(id)
187 if err != nil {
188 return []Client{}, err
189 }
190 clients = append(clients, client)
191 }
192 return clients, nil
193 }
195 func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error {
196 m.endpointLock.Lock()
197 defer m.endpointLock.Unlock()
198 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
199 return nil
200 }
202 func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error {
203 m.endpointLock.Lock()
204 defer m.endpointLock.Unlock()
205 pos := -1
206 for p, item := range m.endpoints[client.String()] {
207 if item.ID.Equal(endpoint) {
208 pos = p
209 break
210 }
211 }
212 if pos >= 0 {
213 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
214 }
215 return nil
216 }
218 func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) {
219 m.endpointLock.RLock()
220 defer m.endpointLock.RUnlock()
221 for _, candidate := range m.endpoints[client.String()] {
222 if strings.HasPrefix(endpoint, candidate.URI.String()) {
223 return true, nil
224 }
225 }
226 return false, nil
227 }
229 func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
230 m.endpointLock.RLock()
231 defer m.endpointLock.RUnlock()
232 return m.endpoints[client.String()], nil
233 }