auth
auth/client.go
Update uuid import path, test for multiple profile updates. Test updating multiple profiles in one request (e.g., when profiles become compromised.) Update the uuid import path to use the new code.secondbit.org/uuid import path.
| 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@45 | 8 "code.secondbit.org/uuid" |
| paddy@41 | 9 "strings" |
| 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 } |