auth
auth/client.go
Update Context with new CheckEndpoint and CountEndpoints. Update the Context.CheckEndpoint method to pass through the new "strict" parameter that controls how URLs are checked for validation. Add a Context.CountEndpoints method to safely access the number of endpoints in stored for the client in the associent ClientStore. Finally, remove the error return from Context.Render, as we'd only ever want to log that, anyways. So we may as well just log it from the central function.
| 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 } |