auth
auth/client.go
Move HTTP tests to http_test.go, rename the GetGrant test. Rename the GetGrant test to something that makes it more obvious, and indicate that we're only testing for successful responses in this function (e.g., responses that should be successfully handled). Another function will deal with failure modes. Move the function to a new http_test.go file. The model files are shouldn't have information about how the models are being represented.
| 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@41 | 128 CheckEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@41 | 129 ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@0 | 130 } |
| paddy@31 | 131 |
| paddy@31 | 132 func (m *Memstore) GetClient(id uuid.ID) (Client, error) { |
| paddy@31 | 133 m.clientLock.RLock() |
| paddy@31 | 134 defer m.clientLock.RUnlock() |
| paddy@31 | 135 c, ok := m.clients[id.String()] |
| paddy@31 | 136 if !ok { |
| paddy@31 | 137 return Client{}, ErrClientNotFound |
| paddy@31 | 138 } |
| paddy@31 | 139 return c, nil |
| paddy@31 | 140 } |
| paddy@31 | 141 |
| paddy@31 | 142 func (m *Memstore) SaveClient(client Client) error { |
| paddy@31 | 143 m.clientLock.Lock() |
| paddy@31 | 144 defer m.clientLock.Unlock() |
| paddy@31 | 145 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 146 return ErrClientAlreadyExists |
| paddy@31 | 147 } |
| paddy@31 | 148 m.clients[client.ID.String()] = client |
| paddy@31 | 149 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 150 return nil |
| paddy@31 | 151 } |
| paddy@31 | 152 |
| paddy@31 | 153 func (m *Memstore) UpdateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 154 m.clientLock.Lock() |
| paddy@39 | 155 defer m.clientLock.Unlock() |
| paddy@39 | 156 c, ok := m.clients[id.String()] |
| paddy@39 | 157 if !ok { |
| paddy@39 | 158 return ErrClientNotFound |
| paddy@39 | 159 } |
| paddy@39 | 160 c.ApplyChange(change) |
| paddy@39 | 161 m.clients[id.String()] = c |
| paddy@31 | 162 return nil |
| paddy@31 | 163 } |
| paddy@31 | 164 |
| paddy@31 | 165 func (m *Memstore) DeleteClient(id uuid.ID) error { |
| paddy@31 | 166 client, err := m.GetClient(id) |
| paddy@31 | 167 if err != nil { |
| paddy@31 | 168 return err |
| paddy@31 | 169 } |
| paddy@31 | 170 m.clientLock.Lock() |
| paddy@31 | 171 defer m.clientLock.Unlock() |
| paddy@31 | 172 delete(m.clients, id.String()) |
| paddy@31 | 173 pos := -1 |
| paddy@31 | 174 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 175 if item.Equal(id) { |
| paddy@31 | 176 pos = p |
| paddy@31 | 177 break |
| paddy@31 | 178 } |
| paddy@31 | 179 } |
| paddy@31 | 180 if pos >= 0 { |
| paddy@31 | 181 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 182 } |
| paddy@31 | 183 return nil |
| paddy@31 | 184 } |
| paddy@31 | 185 |
| paddy@31 | 186 func (m *Memstore) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 187 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 188 if len(ids) > num+offset { |
| paddy@31 | 189 ids = ids[offset : num+offset] |
| paddy@31 | 190 } else if len(ids) > offset { |
| paddy@31 | 191 ids = ids[offset:] |
| paddy@31 | 192 } else { |
| paddy@31 | 193 return []Client{}, nil |
| paddy@31 | 194 } |
| paddy@31 | 195 clients := []Client{} |
| paddy@31 | 196 for _, id := range ids { |
| paddy@31 | 197 client, err := m.GetClient(id) |
| paddy@31 | 198 if err != nil { |
| paddy@31 | 199 return []Client{}, err |
| paddy@31 | 200 } |
| paddy@31 | 201 clients = append(clients, client) |
| paddy@31 | 202 } |
| paddy@31 | 203 return clients, nil |
| paddy@31 | 204 } |
| paddy@41 | 205 |
| paddy@41 | 206 func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error { |
| paddy@41 | 207 m.endpointLock.Lock() |
| paddy@41 | 208 defer m.endpointLock.Unlock() |
| paddy@41 | 209 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint) |
| paddy@41 | 210 return nil |
| paddy@41 | 211 } |
| paddy@41 | 212 |
| paddy@41 | 213 func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 214 m.endpointLock.Lock() |
| paddy@41 | 215 defer m.endpointLock.Unlock() |
| paddy@41 | 216 pos := -1 |
| paddy@41 | 217 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 218 if item.ID.Equal(endpoint) { |
| paddy@41 | 219 pos = p |
| paddy@41 | 220 break |
| paddy@41 | 221 } |
| paddy@41 | 222 } |
| paddy@41 | 223 if pos >= 0 { |
| paddy@41 | 224 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 225 } |
| paddy@41 | 226 return nil |
| paddy@41 | 227 } |
| paddy@41 | 228 |
| paddy@41 | 229 func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 230 m.endpointLock.RLock() |
| paddy@41 | 231 defer m.endpointLock.RUnlock() |
| paddy@41 | 232 for _, candidate := range m.endpoints[client.String()] { |
| paddy@41 | 233 if strings.HasPrefix(endpoint, candidate.URI.String()) { |
| paddy@41 | 234 return true, nil |
| paddy@41 | 235 } |
| paddy@41 | 236 } |
| paddy@41 | 237 return false, nil |
| paddy@41 | 238 } |
| paddy@41 | 239 |
| paddy@41 | 240 func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 241 m.endpointLock.RLock() |
| paddy@41 | 242 defer m.endpointLock.RUnlock() |
| paddy@41 | 243 return m.endpoints[client.String()], nil |
| paddy@41 | 244 } |