auth
auth/client.go
Make sure client URLs are actually URLs. When updating a client website or logo, make sure that URL is actually a URL. Instead of returning an error for too short input, just return an error if the input isn't a URL.
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 ErrEmptyChange = errors.New("Change must have at least one change in it.")
17 ErrClientNameTooShort = errors.New("Client name must be at least 2 characters.")
18 ErrClientNameTooLong = errors.New("Client name must be at most 32 characters.")
19 ErrClientLogoTooLong = errors.New("Client logo must be at most 1024 characters.")
20 ErrClientLogoNotURL = errors.New("Client logo must be a valid absolute URL.")
21 ErrClientWebsiteTooLong = errors.New("Client website must be at most 1024 characters.")
22 ErrClientWebsiteNotURL = errors.New("Client website must be a valid absolute URL.")
23 )
25 // Client represents a client that grants access
26 // to the auth server, exchanging grants for tokens,
27 // and tokens for access.
28 type Client struct {
29 ID uuid.ID
30 Secret string
31 OwnerID uuid.ID
32 Name string
33 Logo string
34 Website string
35 Type string
36 }
38 func (c *Client) ApplyChange(change ClientChange) {
39 if change.Secret != nil {
40 c.Secret = *change.Secret
41 }
42 if change.OwnerID != nil {
43 c.OwnerID = change.OwnerID
44 }
45 if change.Name != nil {
46 c.Name = *change.Name
47 }
48 if change.Logo != nil {
49 c.Logo = *change.Logo
50 }
51 if change.Website != nil {
52 c.Website = *change.Website
53 }
54 }
56 type ClientChange struct {
57 Secret *string
58 OwnerID uuid.ID
59 Name *string
60 Logo *string
61 Website *string
62 }
64 func (c ClientChange) Validate() error {
65 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil {
66 return ErrEmptyChange
67 }
68 if c.Name != nil && len(*c.Name) < 2 {
69 return ErrClientNameTooShort
70 }
71 if c.Name != nil && len(*c.Name) > 32 {
72 return ErrClientNameTooLong
73 }
74 if c.Logo != nil && *c.Logo != "" {
75 if len(*c.Logo) > 1024 {
76 return ErrClientLogoTooLong
77 }
78 u, err := url.Parse(*c.Logo)
79 if err != nil || !u.IsAbs() {
80 return ErrClientLogoNotURL
81 }
82 }
83 if c.Website != nil && *c.Website != "" {
84 if len(*c.Website) > 140 {
85 return ErrClientWebsiteTooLong
86 }
87 u, err := url.Parse(*c.Website)
88 if err != nil || !u.IsAbs() {
89 return ErrClientWebsiteNotURL
90 }
91 }
92 return nil
93 }
95 type Endpoint struct {
96 ID uuid.ID
97 ClientID uuid.ID
98 URI url.URL
99 Added time.Time
100 }
102 type sortedEndpoints []Endpoint
104 func (s sortedEndpoints) Len() int {
105 return len(s)
106 }
108 func (s sortedEndpoints) Less(i, j int) bool {
109 return s[i].Added.Before(s[j].Added)
110 }
112 func (s sortedEndpoints) Swap(i, j int) {
113 s[i], s[j] = s[j], s[i]
114 }
116 // ClientStore abstracts the storage interface for
117 // storing and retrieving Clients.
118 type ClientStore interface {
119 GetClient(id uuid.ID) (Client, error)
120 SaveClient(client Client) error
121 UpdateClient(id uuid.ID, change ClientChange) error
122 DeleteClient(id uuid.ID) error
123 ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
125 AddEndpoint(client uuid.ID, endpoint Endpoint) error
126 RemoveEndpoint(client, endpoint uuid.ID) error
127 CheckEndpoint(client uuid.ID, endpoint string) (bool, error)
128 ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
129 }
131 func (m *Memstore) GetClient(id uuid.ID) (Client, error) {
132 m.clientLock.RLock()
133 defer m.clientLock.RUnlock()
134 c, ok := m.clients[id.String()]
135 if !ok {
136 return Client{}, ErrClientNotFound
137 }
138 return c, nil
139 }
141 func (m *Memstore) SaveClient(client Client) error {
142 m.clientLock.Lock()
143 defer m.clientLock.Unlock()
144 if _, ok := m.clients[client.ID.String()]; ok {
145 return ErrClientAlreadyExists
146 }
147 m.clients[client.ID.String()] = client
148 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID)
149 return nil
150 }
152 func (m *Memstore) UpdateClient(id uuid.ID, change ClientChange) error {
153 m.clientLock.Lock()
154 defer m.clientLock.Unlock()
155 c, ok := m.clients[id.String()]
156 if !ok {
157 return ErrClientNotFound
158 }
159 c.ApplyChange(change)
160 m.clients[id.String()] = c
161 return nil
162 }
164 func (m *Memstore) DeleteClient(id uuid.ID) error {
165 client, err := m.GetClient(id)
166 if err != nil {
167 return err
168 }
169 m.clientLock.Lock()
170 defer m.clientLock.Unlock()
171 delete(m.clients, id.String())
172 pos := -1
173 for p, item := range m.profileClientLookup[client.OwnerID.String()] {
174 if item.Equal(id) {
175 pos = p
176 break
177 }
178 }
179 if pos >= 0 {
180 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...)
181 }
182 return nil
183 }
185 func (m *Memstore) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) {
186 ids := m.lookupClientsByProfileID(ownerID.String())
187 if len(ids) > num+offset {
188 ids = ids[offset : num+offset]
189 } else if len(ids) > offset {
190 ids = ids[offset:]
191 } else {
192 return []Client{}, nil
193 }
194 clients := []Client{}
195 for _, id := range ids {
196 client, err := m.GetClient(id)
197 if err != nil {
198 return []Client{}, err
199 }
200 clients = append(clients, client)
201 }
202 return clients, nil
203 }
205 func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error {
206 m.endpointLock.Lock()
207 defer m.endpointLock.Unlock()
208 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
209 return nil
210 }
212 func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error {
213 m.endpointLock.Lock()
214 defer m.endpointLock.Unlock()
215 pos := -1
216 for p, item := range m.endpoints[client.String()] {
217 if item.ID.Equal(endpoint) {
218 pos = p
219 break
220 }
221 }
222 if pos >= 0 {
223 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
224 }
225 return nil
226 }
228 func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) {
229 m.endpointLock.RLock()
230 defer m.endpointLock.RUnlock()
231 for _, candidate := range m.endpoints[client.String()] {
232 if strings.HasPrefix(endpoint, candidate.URI.String()) {
233 return true, nil
234 }
235 }
236 return false, nil
237 }
239 func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
240 m.endpointLock.RLock()
241 defer m.endpointLock.RUnlock()
242 return m.endpoints[client.String()], nil
243 }