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