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