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