auth
auth/client.go
Actually validate grant requests. Write the logic to validate grant requests and stub out the rendering/error handling/redirecting locations. Finally, we get to the good stuff: implementing the specification. Write some tests to verify that granting requests works the way we think it does.
| 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 } |