auth
auth/client.go
Test our Postgres profileStore implementation. Update all our test cases to use time.Now().Round(time.Millisecond), because Go uses nanosecond precision on time values, but Postgres silently truncates that to millisecond precision. This caused our tests to report false failures that were just silent precision loss, not actual failures. Set up our authd server to use the Postgres store for profiles and automatically create a test scope when starting up. Log errors when creating Clients through the API, instead of just swallowing them and sending back cryptic act of god errors. Add a NewPostgres helper that returns a postgres profileStore from a connection string (passed through pq transparently). Add an Empty() bool helper to ProfileChange and BulkProfileChange types, so we can determine if there are any changes we need to act on easily. Log errors when creating Pofiles through the API, instead of just swalloing them and sending back cryptic act of god errors. Remove the ` quotes around field and table names, which are not supported in Postgres. This required adding a few functions/methods to pan. Detect situations where a profile was expected and not found, and return ErrProfileNotFound. Detect pq errors thrown when the profiles_pkey constraint is violated, and transform them to the ErrProfileAlreadyExists error. Detect empty ProfileChange and BulkProfileChange variables and abort the updateProfile and updateProfiles methods early, before invalid SQL is generated. Detect pq errors thrown when the logins_pkey constraint is violated, and transform them to the ErrLoginAlreadyExists error. Detect when removing a Login and no rows were affected, and return an ErrLoginNotFound. Create an sql dir with a postgres_init script that will initialize the schema of the tables expected in the database.
| paddy@6 | 1 package auth |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@108 | 4 "crypto/rand" |
| paddy@108 | 5 "encoding/hex" |
| paddy@85 | 6 "encoding/json" |
| paddy@31 | 7 "errors" |
| paddy@116 | 8 "log" |
| paddy@85 | 9 "net/http" |
| paddy@41 | 10 "net/url" |
| paddy@115 | 11 "strconv" |
| paddy@135 | 12 "strings" |
| paddy@41 | 13 "time" |
| paddy@31 | 14 |
| paddy@116 | 15 "github.com/PuerkitoBio/purell" |
| paddy@116 | 16 "github.com/gorilla/mux" |
| paddy@116 | 17 |
| paddy@107 | 18 "code.secondbit.org/uuid.hg" |
| paddy@0 | 19 ) |
| paddy@0 | 20 |
| paddy@121 | 21 func init() { |
| paddy@121 | 22 RegisterGrantType("client_credentials", GrantType{ |
| paddy@121 | 23 Validate: clientCredentialsValidate, |
| paddy@121 | 24 Invalidate: nil, |
| paddy@121 | 25 IssuesRefresh: true, |
| paddy@121 | 26 ReturnToken: RenderJSONToken, |
| paddy@123 | 27 AllowsPublic: false, |
| paddy@124 | 28 AuditString: clientCredentialsAuditString, |
| paddy@121 | 29 }) |
| paddy@121 | 30 } |
| paddy@121 | 31 |
| paddy@31 | 32 var ( |
| paddy@57 | 33 // ErrNoClientStore is returned when a Context tries to act on a clientStore without setting one first. |
| paddy@57 | 34 ErrNoClientStore = errors.New("no clientStore was specified for the Context") |
| paddy@57 | 35 // ErrClientNotFound is returned when a Client is requested but not found in a clientStore. |
| paddy@57 | 36 ErrClientNotFound = errors.New("client not found in clientStore") |
| paddy@57 | 37 // ErrClientAlreadyExists is returned when a Client is added to a clientStore, but another Client with |
| paddy@57 | 38 // the same ID already exists in the clientStore. |
| paddy@57 | 39 ErrClientAlreadyExists = errors.New("client already exists in clientStore") |
| paddy@143 | 40 // ErrEndpointNotFound is returned when an Endpoint is requested but not found in a clientSTore. |
| paddy@143 | 41 ErrEndpointNotFound = errors.New("endpoint not found in clientStore") |
| paddy@41 | 42 |
| paddy@57 | 43 // ErrEmptyChange is returned when a Change has all its properties set to nil. |
| paddy@57 | 44 ErrEmptyChange = errors.New("change must have at least one property set") |
| paddy@57 | 45 // ErrClientNameTooShort is returned when a Client's Name property is too short. |
| paddy@57 | 46 ErrClientNameTooShort = errors.New("client name must be at least 2 characters") |
| paddy@57 | 47 // ErrClientNameTooLong is returned when a Client's Name property is too long. |
| paddy@57 | 48 ErrClientNameTooLong = errors.New("client name must be at most 32 characters") |
| paddy@57 | 49 // ErrClientLogoTooLong is returned when a Client's Logo property is too long. |
| paddy@57 | 50 ErrClientLogoTooLong = errors.New("client logo must be at most 1024 characters") |
| paddy@57 | 51 // ErrClientLogoNotURL is returned when a Client's Logo property is not a valid absolute URL. |
| paddy@57 | 52 ErrClientLogoNotURL = errors.New("client logo must be a valid absolute URL") |
| paddy@57 | 53 // ErrClientWebsiteTooLong is returned when a Client's Website property is too long. |
| paddy@49 | 54 ErrClientWebsiteTooLong = errors.New("client website must be at most 1024 characters") |
| paddy@57 | 55 // ErrClientWebsiteNotURL is returned when a Client's Website property is not a valid absolute URL. |
| paddy@57 | 56 ErrClientWebsiteNotURL = errors.New("client website must be a valid absolute URL") |
| paddy@116 | 57 // ErrEndpointURINotURL is returned when an Endpoint's URI property is not a valid absolute URL. |
| paddy@116 | 58 ErrEndpointURINotURL = errors.New("endpoint URI must be a valid absolute URL") |
| paddy@31 | 59 ) |
| paddy@31 | 60 |
| paddy@115 | 61 const ( |
| paddy@138 | 62 clientTypePublic = "public" |
| paddy@138 | 63 clientTypeConfidential = "confidential" |
| paddy@138 | 64 minClientNameLen = 2 |
| paddy@138 | 65 maxClientNameLen = 24 |
| paddy@138 | 66 defaultClientResponseSize = 20 |
| paddy@138 | 67 maxClientResponseSize = 50 |
| paddy@138 | 68 defaultEndpointResponseSize = 20 |
| paddy@138 | 69 maxEndpointResponseSize = 50 |
| paddy@130 | 70 |
| paddy@130 | 71 normalizeFlags = purell.FlagsUsuallySafeNonGreedy | purell.FlagSortQuery |
| paddy@115 | 72 ) |
| paddy@115 | 73 |
| paddy@25 | 74 // Client represents a client that grants access |
| paddy@25 | 75 // to the auth server, exchanging grants for tokens, |
| paddy@25 | 76 // and tokens for access. |
| paddy@0 | 77 type Client struct { |
| paddy@116 | 78 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 79 Secret string `json:"secret,omitempty"` |
| paddy@116 | 80 OwnerID uuid.ID `json:"owner_id,omitempty"` |
| paddy@116 | 81 Name string `json:"name,omitempty"` |
| paddy@116 | 82 Logo string `json:"logo,omitempty"` |
| paddy@116 | 83 Website string `json:"website,omitempty"` |
| paddy@116 | 84 Type string `json:"type,omitempty"` |
| paddy@0 | 85 } |
| paddy@0 | 86 |
| paddy@57 | 87 // ApplyChange applies the properties of the passed |
| paddy@57 | 88 // ClientChange to the Client object it is called on. |
| paddy@39 | 89 func (c *Client) ApplyChange(change ClientChange) { |
| paddy@39 | 90 if change.Secret != nil { |
| paddy@39 | 91 c.Secret = *change.Secret |
| paddy@39 | 92 } |
| paddy@39 | 93 if change.OwnerID != nil { |
| paddy@39 | 94 c.OwnerID = change.OwnerID |
| paddy@39 | 95 } |
| paddy@39 | 96 if change.Name != nil { |
| paddy@39 | 97 c.Name = *change.Name |
| paddy@39 | 98 } |
| paddy@39 | 99 if change.Logo != nil { |
| paddy@39 | 100 c.Logo = *change.Logo |
| paddy@39 | 101 } |
| paddy@39 | 102 if change.Website != nil { |
| paddy@39 | 103 c.Website = *change.Website |
| paddy@39 | 104 } |
| paddy@39 | 105 } |
| paddy@39 | 106 |
| paddy@57 | 107 // ClientChange represents a bundle of options for |
| paddy@57 | 108 // updating a Client's mutable data. |
| paddy@31 | 109 type ClientChange struct { |
| paddy@41 | 110 Secret *string |
| paddy@41 | 111 OwnerID uuid.ID |
| paddy@41 | 112 Name *string |
| paddy@41 | 113 Logo *string |
| paddy@41 | 114 Website *string |
| paddy@31 | 115 } |
| paddy@31 | 116 |
| paddy@57 | 117 // Validate checks the ClientChange it is called on |
| paddy@57 | 118 // and asserts its internal validity, or lack thereof. |
| paddy@133 | 119 func (c ClientChange) Validate() []error { |
| paddy@133 | 120 errors := []error{} |
| paddy@42 | 121 if c.Secret == nil && c.OwnerID == nil && c.Name == nil && c.Logo == nil && c.Website == nil { |
| paddy@133 | 122 errors = append(errors, ErrEmptyChange) |
| paddy@133 | 123 return errors |
| paddy@42 | 124 } |
| paddy@41 | 125 if c.Name != nil && len(*c.Name) < 2 { |
| paddy@133 | 126 errors = append(errors, ErrClientNameTooShort) |
| paddy@41 | 127 } |
| paddy@41 | 128 if c.Name != nil && len(*c.Name) > 32 { |
| paddy@133 | 129 errors = append(errors, ErrClientNameTooLong) |
| paddy@41 | 130 } |
| paddy@42 | 131 if c.Logo != nil && *c.Logo != "" { |
| paddy@42 | 132 if len(*c.Logo) > 1024 { |
| paddy@133 | 133 errors = append(errors, ErrClientLogoTooLong) |
| paddy@42 | 134 } |
| paddy@42 | 135 u, err := url.Parse(*c.Logo) |
| paddy@42 | 136 if err != nil || !u.IsAbs() { |
| paddy@133 | 137 errors = append(errors, ErrClientLogoNotURL) |
| paddy@42 | 138 } |
| paddy@41 | 139 } |
| paddy@42 | 140 if c.Website != nil && *c.Website != "" { |
| paddy@42 | 141 if len(*c.Website) > 140 { |
| paddy@133 | 142 errors = append(errors, ErrClientWebsiteTooLong) |
| paddy@42 | 143 } |
| paddy@42 | 144 u, err := url.Parse(*c.Website) |
| paddy@42 | 145 if err != nil || !u.IsAbs() { |
| paddy@133 | 146 errors = append(errors, ErrClientWebsiteNotURL) |
| paddy@42 | 147 } |
| paddy@41 | 148 } |
| paddy@133 | 149 return errors |
| paddy@39 | 150 } |
| paddy@39 | 151 |
| paddy@123 | 152 func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { |
| paddy@85 | 153 enc := json.NewEncoder(w) |
| paddy@85 | 154 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 155 if !fromAuthHeader { |
| paddy@85 | 156 clientIDStr = r.PostFormValue("client_id") |
| paddy@85 | 157 } |
| paddy@123 | 158 if clientIDStr == "" { |
| paddy@85 | 159 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 160 if fromAuthHeader { |
| paddy@85 | 161 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 162 } |
| paddy@85 | 163 renderJSONError(enc, "invalid_client") |
| paddy@123 | 164 return nil, "", false |
| paddy@123 | 165 } |
| paddy@129 | 166 if !allowPublic && !fromAuthHeader { |
| paddy@129 | 167 w.WriteHeader(http.StatusBadRequest) |
| paddy@129 | 168 renderJSONError(enc, "unauthorized_client") |
| paddy@129 | 169 return nil, "", false |
| paddy@129 | 170 } |
| paddy@123 | 171 clientID, err := uuid.Parse(clientIDStr) |
| paddy@123 | 172 if err != nil { |
| paddy@123 | 173 log.Println("Error decoding client ID:", err) |
| paddy@123 | 174 w.WriteHeader(http.StatusUnauthorized) |
| paddy@123 | 175 if fromAuthHeader { |
| paddy@123 | 176 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@123 | 177 } |
| paddy@123 | 178 renderJSONError(enc, "invalid_client") |
| paddy@123 | 179 return nil, "", false |
| paddy@123 | 180 } |
| paddy@123 | 181 return clientID, clientSecret, true |
| paddy@123 | 182 } |
| paddy@123 | 183 |
| paddy@123 | 184 func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { |
| paddy@123 | 185 enc := json.NewEncoder(w) |
| paddy@123 | 186 clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) |
| paddy@123 | 187 if !ok { |
| paddy@85 | 188 return nil, false |
| paddy@85 | 189 } |
| paddy@123 | 190 _, _, fromAuthHeader := r.BasicAuth() |
| paddy@85 | 191 client, err := context.GetClient(clientID) |
| paddy@85 | 192 if err == ErrClientNotFound { |
| paddy@85 | 193 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 194 if fromAuthHeader { |
| paddy@85 | 195 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 196 } |
| paddy@85 | 197 renderJSONError(enc, "invalid_client") |
| paddy@85 | 198 return nil, false |
| paddy@85 | 199 } else if err != nil { |
| paddy@85 | 200 w.WriteHeader(http.StatusInternalServerError) |
| paddy@85 | 201 renderJSONError(enc, "server_error") |
| paddy@85 | 202 return nil, false |
| paddy@85 | 203 } |
| paddy@113 | 204 if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret. |
| paddy@85 | 205 w.WriteHeader(http.StatusUnauthorized) |
| paddy@85 | 206 if fromAuthHeader { |
| paddy@85 | 207 w.Header().Set("WWW-Authenticate", "Basic") |
| paddy@85 | 208 } |
| paddy@85 | 209 renderJSONError(enc, "invalid_client") |
| paddy@85 | 210 return nil, false |
| paddy@85 | 211 } |
| paddy@85 | 212 return clientID, true |
| paddy@85 | 213 } |
| paddy@85 | 214 |
| paddy@57 | 215 // Endpoint represents a single URI that a Client |
| paddy@57 | 216 // controls. Users will be redirected to these URIs |
| paddy@57 | 217 // following successful authorization grants and |
| paddy@57 | 218 // exchanges for access tokens. |
| paddy@41 | 219 type Endpoint struct { |
| paddy@116 | 220 ID uuid.ID `json:"id,omitempty"` |
| paddy@116 | 221 ClientID uuid.ID `json:"client_id,omitempty"` |
| paddy@116 | 222 URI string `json:"uri,omitempty"` |
| paddy@116 | 223 NormalizedURI string `json:"-"` |
| paddy@116 | 224 Added time.Time `json:"added,omitempty"` |
| paddy@116 | 225 } |
| paddy@116 | 226 |
| paddy@116 | 227 func normalizeURIString(in string) (string, error) { |
| paddy@130 | 228 n, err := purell.NormalizeURLString(in, normalizeFlags) |
| paddy@116 | 229 if err != nil { |
| paddy@116 | 230 log.Println(err) |
| paddy@116 | 231 return in, ErrEndpointURINotURL |
| paddy@116 | 232 } |
| paddy@116 | 233 return n, nil |
| paddy@116 | 234 } |
| paddy@116 | 235 |
| paddy@116 | 236 func normalizeURI(in *url.URL) string { |
| paddy@130 | 237 return purell.NormalizeURL(in, normalizeFlags) |
| paddy@41 | 238 } |
| paddy@41 | 239 |
| paddy@41 | 240 type sortedEndpoints []Endpoint |
| paddy@41 | 241 |
| paddy@41 | 242 func (s sortedEndpoints) Len() int { |
| paddy@41 | 243 return len(s) |
| paddy@41 | 244 } |
| paddy@41 | 245 |
| paddy@41 | 246 func (s sortedEndpoints) Less(i, j int) bool { |
| paddy@41 | 247 return s[i].Added.Before(s[j].Added) |
| paddy@41 | 248 } |
| paddy@41 | 249 |
| paddy@41 | 250 func (s sortedEndpoints) Swap(i, j int) { |
| paddy@41 | 251 s[i], s[j] = s[j], s[i] |
| paddy@41 | 252 } |
| paddy@41 | 253 |
| paddy@57 | 254 type clientStore interface { |
| paddy@57 | 255 getClient(id uuid.ID) (Client, error) |
| paddy@57 | 256 saveClient(client Client) error |
| paddy@57 | 257 updateClient(id uuid.ID, change ClientChange) error |
| paddy@57 | 258 deleteClient(id uuid.ID) error |
| paddy@57 | 259 listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) |
| paddy@41 | 260 |
| paddy@115 | 261 addEndpoints(client uuid.ID, endpoint []Endpoint) error |
| paddy@57 | 262 removeEndpoint(client, endpoint uuid.ID) error |
| paddy@143 | 263 getEndpoint(client, endpoint uuid.ID) (Endpoint, error) |
| paddy@58 | 264 checkEndpoint(client uuid.ID, endpoint string) (bool, error) |
| paddy@57 | 265 listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) |
| paddy@57 | 266 countEndpoints(client uuid.ID) (int64, error) |
| paddy@0 | 267 } |
| paddy@31 | 268 |
| paddy@57 | 269 func (m *memstore) getClient(id uuid.ID) (Client, error) { |
| paddy@31 | 270 m.clientLock.RLock() |
| paddy@31 | 271 defer m.clientLock.RUnlock() |
| paddy@31 | 272 c, ok := m.clients[id.String()] |
| paddy@31 | 273 if !ok { |
| paddy@31 | 274 return Client{}, ErrClientNotFound |
| paddy@31 | 275 } |
| paddy@31 | 276 return c, nil |
| paddy@31 | 277 } |
| paddy@31 | 278 |
| paddy@57 | 279 func (m *memstore) saveClient(client Client) error { |
| paddy@31 | 280 m.clientLock.Lock() |
| paddy@31 | 281 defer m.clientLock.Unlock() |
| paddy@31 | 282 if _, ok := m.clients[client.ID.String()]; ok { |
| paddy@31 | 283 return ErrClientAlreadyExists |
| paddy@31 | 284 } |
| paddy@31 | 285 m.clients[client.ID.String()] = client |
| paddy@31 | 286 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()], client.ID) |
| paddy@31 | 287 return nil |
| paddy@31 | 288 } |
| paddy@31 | 289 |
| paddy@57 | 290 func (m *memstore) updateClient(id uuid.ID, change ClientChange) error { |
| paddy@39 | 291 m.clientLock.Lock() |
| paddy@39 | 292 defer m.clientLock.Unlock() |
| paddy@39 | 293 c, ok := m.clients[id.String()] |
| paddy@39 | 294 if !ok { |
| paddy@39 | 295 return ErrClientNotFound |
| paddy@39 | 296 } |
| paddy@39 | 297 c.ApplyChange(change) |
| paddy@39 | 298 m.clients[id.String()] = c |
| paddy@31 | 299 return nil |
| paddy@31 | 300 } |
| paddy@31 | 301 |
| paddy@57 | 302 func (m *memstore) deleteClient(id uuid.ID) error { |
| paddy@57 | 303 client, err := m.getClient(id) |
| paddy@31 | 304 if err != nil { |
| paddy@31 | 305 return err |
| paddy@31 | 306 } |
| paddy@31 | 307 m.clientLock.Lock() |
| paddy@31 | 308 defer m.clientLock.Unlock() |
| paddy@31 | 309 delete(m.clients, id.String()) |
| paddy@31 | 310 pos := -1 |
| paddy@31 | 311 for p, item := range m.profileClientLookup[client.OwnerID.String()] { |
| paddy@31 | 312 if item.Equal(id) { |
| paddy@31 | 313 pos = p |
| paddy@31 | 314 break |
| paddy@31 | 315 } |
| paddy@31 | 316 } |
| paddy@31 | 317 if pos >= 0 { |
| paddy@31 | 318 m.profileClientLookup[client.OwnerID.String()] = append(m.profileClientLookup[client.OwnerID.String()][:pos], m.profileClientLookup[client.OwnerID.String()][pos+1:]...) |
| paddy@31 | 319 } |
| paddy@31 | 320 return nil |
| paddy@31 | 321 } |
| paddy@31 | 322 |
| paddy@57 | 323 func (m *memstore) listClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@33 | 324 ids := m.lookupClientsByProfileID(ownerID.String()) |
| paddy@31 | 325 if len(ids) > num+offset { |
| paddy@31 | 326 ids = ids[offset : num+offset] |
| paddy@31 | 327 } else if len(ids) > offset { |
| paddy@31 | 328 ids = ids[offset:] |
| paddy@31 | 329 } else { |
| paddy@31 | 330 return []Client{}, nil |
| paddy@31 | 331 } |
| paddy@31 | 332 clients := []Client{} |
| paddy@31 | 333 for _, id := range ids { |
| paddy@57 | 334 client, err := m.getClient(id) |
| paddy@31 | 335 if err != nil { |
| paddy@31 | 336 return []Client{}, err |
| paddy@31 | 337 } |
| paddy@31 | 338 clients = append(clients, client) |
| paddy@31 | 339 } |
| paddy@31 | 340 return clients, nil |
| paddy@31 | 341 } |
| paddy@41 | 342 |
| paddy@115 | 343 func (m *memstore) addEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@41 | 344 m.endpointLock.Lock() |
| paddy@41 | 345 defer m.endpointLock.Unlock() |
| paddy@115 | 346 m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoints...) |
| paddy@41 | 347 return nil |
| paddy@41 | 348 } |
| paddy@41 | 349 |
| paddy@143 | 350 func (m *memstore) getEndpoint(client, endpoint uuid.ID) (Endpoint, error) { |
| paddy@143 | 351 m.endpointLock.Lock() |
| paddy@143 | 352 defer m.endpointLock.Unlock() |
| paddy@143 | 353 for _, item := range m.endpoints[client.String()] { |
| paddy@143 | 354 if item.ID.Equal(endpoint) { |
| paddy@143 | 355 return item, nil |
| paddy@143 | 356 } |
| paddy@143 | 357 } |
| paddy@143 | 358 return Endpoint{}, ErrEndpointNotFound |
| paddy@143 | 359 } |
| paddy@143 | 360 |
| paddy@57 | 361 func (m *memstore) removeEndpoint(client, endpoint uuid.ID) error { |
| paddy@41 | 362 m.endpointLock.Lock() |
| paddy@41 | 363 defer m.endpointLock.Unlock() |
| paddy@41 | 364 pos := -1 |
| paddy@41 | 365 for p, item := range m.endpoints[client.String()] { |
| paddy@41 | 366 if item.ID.Equal(endpoint) { |
| paddy@41 | 367 pos = p |
| paddy@41 | 368 break |
| paddy@41 | 369 } |
| paddy@41 | 370 } |
| paddy@41 | 371 if pos >= 0 { |
| paddy@41 | 372 m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...) |
| paddy@41 | 373 } |
| paddy@41 | 374 return nil |
| paddy@41 | 375 } |
| paddy@41 | 376 |
| paddy@58 | 377 func (m *memstore) checkEndpoint(client uuid.ID, endpoint string) (bool, error) { |
| paddy@41 | 378 m.endpointLock.RLock() |
| paddy@41 | 379 defer m.endpointLock.RUnlock() |
| paddy@41 | 380 for _, candidate := range m.endpoints[client.String()] { |
| paddy@116 | 381 if endpoint == candidate.NormalizedURI { |
| paddy@41 | 382 return true, nil |
| paddy@41 | 383 } |
| paddy@41 | 384 } |
| paddy@41 | 385 return false, nil |
| paddy@41 | 386 } |
| paddy@41 | 387 |
| paddy@57 | 388 func (m *memstore) listEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@41 | 389 m.endpointLock.RLock() |
| paddy@41 | 390 defer m.endpointLock.RUnlock() |
| paddy@41 | 391 return m.endpoints[client.String()], nil |
| paddy@41 | 392 } |
| paddy@54 | 393 |
| paddy@57 | 394 func (m *memstore) countEndpoints(client uuid.ID) (int64, error) { |
| paddy@54 | 395 m.endpointLock.RLock() |
| paddy@54 | 396 defer m.endpointLock.RUnlock() |
| paddy@54 | 397 return int64(len(m.endpoints[client.String()])), nil |
| paddy@54 | 398 } |
| paddy@108 | 399 |
| paddy@108 | 400 type newClientReq struct { |
| paddy@108 | 401 Name string `json:"name"` |
| paddy@108 | 402 Logo string `json:"logo"` |
| paddy@108 | 403 Website string `json:"website"` |
| paddy@108 | 404 Type string `json:"type"` |
| paddy@108 | 405 Endpoints []string `json:"endpoints"` |
| paddy@108 | 406 } |
| paddy@108 | 407 |
| paddy@108 | 408 func RegisterClientHandlers(r *mux.Router, context Context) { |
| paddy@108 | 409 r.Handle("/clients", wrap(context, CreateClientHandler)).Methods("POST") |
| paddy@131 | 410 r.Handle("/clients", wrap(context, ListClientsHandler)).Methods("GET") |
| paddy@131 | 411 r.Handle("/clients/{id}", wrap(context, GetClientHandler)).Methods("GET") |
| paddy@133 | 412 r.Handle("/clients/{id}", wrap(context, UpdateClientHandler)).Methods("PATCH") |
| paddy@144 | 413 r.Handle("/clients/{id}", wrap(context, RemoveClientHandler)).Methods("DELETE") |
| paddy@137 | 414 r.Handle("/clients/{id}/endpoints", wrap(context, AddEndpointsHandler)).Methods("POST") |
| paddy@144 | 415 r.Handle("/clients/{client_id}/endpoints/{id}", wrap(context, RemoveEndpointHandler)).Methods("DELETE") |
| paddy@138 | 416 r.Handle("/clients/{id}/endpoints", wrap(context, ListEndpointsHandler)).Methods("GET") |
| paddy@108 | 417 } |
| paddy@108 | 418 |
| paddy@108 | 419 func CreateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@115 | 420 errors := []requestError{} |
| paddy@108 | 421 username, password, ok := r.BasicAuth() |
| paddy@108 | 422 if !ok { |
| paddy@115 | 423 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@115 | 424 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@108 | 425 return |
| paddy@108 | 426 } |
| paddy@108 | 427 profile, err := authenticate(username, password, c) |
| paddy@108 | 428 if err != nil { |
| paddy@139 | 429 if isAuthError(err) { |
| paddy@139 | 430 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@139 | 431 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@139 | 432 } else { |
| paddy@149 | 433 log.Printf("Error authenticating: %#+v\n", err) |
| paddy@139 | 434 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@139 | 435 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@139 | 436 } |
| paddy@108 | 437 return |
| paddy@108 | 438 } |
| paddy@108 | 439 var req newClientReq |
| paddy@108 | 440 decoder := json.NewDecoder(r.Body) |
| paddy@108 | 441 err = decoder.Decode(&req) |
| paddy@108 | 442 if err != nil { |
| paddy@108 | 443 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@108 | 444 return |
| paddy@108 | 445 } |
| paddy@116 | 446 if req.Type == "" { |
| paddy@116 | 447 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/type"}) |
| paddy@116 | 448 } else if req.Type != clientTypePublic && req.Type != clientTypeConfidential { |
| paddy@115 | 449 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/type"}) |
| paddy@116 | 450 } |
| paddy@116 | 451 if req.Name == "" { |
| paddy@116 | 452 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/name"}) |
| paddy@116 | 453 } else if len(req.Name) < minClientNameLen { |
| paddy@116 | 454 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@116 | 455 } else if len(req.Name) > maxClientNameLen { |
| paddy@116 | 456 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@116 | 457 } |
| paddy@116 | 458 if len(errors) > 0 { |
| paddy@115 | 459 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@108 | 460 return |
| paddy@108 | 461 } |
| paddy@108 | 462 client := Client{ |
| paddy@108 | 463 ID: uuid.NewID(), |
| paddy@108 | 464 OwnerID: profile.ID, |
| paddy@108 | 465 Name: req.Name, |
| paddy@108 | 466 Logo: req.Logo, |
| paddy@108 | 467 Website: req.Website, |
| paddy@108 | 468 Type: req.Type, |
| paddy@108 | 469 } |
| paddy@118 | 470 if client.Type == clientTypeConfidential { |
| paddy@115 | 471 secret := make([]byte, 32) |
| paddy@115 | 472 _, err = rand.Read(secret) |
| paddy@115 | 473 if err != nil { |
| paddy@149 | 474 log.Printf("Error generating secret: %#+v\n", err) |
| paddy@115 | 475 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@115 | 476 return |
| paddy@115 | 477 } |
| paddy@115 | 478 client.Secret = hex.EncodeToString(secret) |
| paddy@115 | 479 } |
| paddy@108 | 480 err = c.SaveClient(client) |
| paddy@108 | 481 if err != nil { |
| paddy@115 | 482 if err == ErrClientAlreadyExists { |
| paddy@115 | 483 errors = append(errors, requestError{Slug: requestErrConflict, Field: "/id"}) |
| paddy@115 | 484 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@115 | 485 return |
| paddy@115 | 486 } |
| paddy@149 | 487 log.Printf("Error saving client: %#+v\n", err) |
| paddy@115 | 488 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@108 | 489 return |
| paddy@108 | 490 } |
| paddy@108 | 491 endpoints := []Endpoint{} |
| paddy@115 | 492 for pos, u := range req.Endpoints { |
| paddy@108 | 493 uri, err := url.Parse(u) |
| paddy@108 | 494 if err != nil { |
| paddy@115 | 495 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@108 | 496 continue |
| paddy@108 | 497 } |
| paddy@116 | 498 if !uri.IsAbs() { |
| paddy@116 | 499 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@116 | 500 continue |
| paddy@116 | 501 } |
| paddy@108 | 502 endpoint := Endpoint{ |
| paddy@108 | 503 ID: uuid.NewID(), |
| paddy@108 | 504 ClientID: client.ID, |
| paddy@116 | 505 URI: uri.String(), |
| paddy@108 | 506 Added: time.Now(), |
| paddy@108 | 507 } |
| paddy@108 | 508 endpoints = append(endpoints, endpoint) |
| paddy@108 | 509 } |
| paddy@115 | 510 err = c.AddEndpoints(client.ID, endpoints) |
| paddy@115 | 511 if err != nil { |
| paddy@149 | 512 log.Printf("Error adding endpoints: %#+v\n", err) |
| paddy@115 | 513 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@115 | 514 encode(w, r, http.StatusInternalServerError, response{Errors: errors, Clients: []Client{client}}) |
| paddy@115 | 515 return |
| paddy@115 | 516 } |
| paddy@108 | 517 resp := response{ |
| paddy@108 | 518 Clients: []Client{client}, |
| paddy@108 | 519 Endpoints: endpoints, |
| paddy@116 | 520 Errors: errors, |
| paddy@108 | 521 } |
| paddy@108 | 522 encode(w, r, http.StatusCreated, resp) |
| paddy@108 | 523 } |
| paddy@121 | 524 |
| paddy@131 | 525 func GetClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 526 errors := []requestError{} |
| paddy@131 | 527 vars := mux.Vars(r) |
| paddy@131 | 528 if vars["id"] == "" { |
| paddy@131 | 529 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@131 | 530 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 531 return |
| paddy@131 | 532 } |
| paddy@131 | 533 id, err := uuid.Parse(vars["id"]) |
| paddy@131 | 534 if err != nil { |
| paddy@131 | 535 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@131 | 536 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 537 return |
| paddy@131 | 538 } |
| paddy@131 | 539 client, err := c.GetClient(id) |
| paddy@131 | 540 if err != nil { |
| paddy@131 | 541 if err == ErrClientNotFound { |
| paddy@131 | 542 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@139 | 543 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@131 | 544 return |
| paddy@131 | 545 } |
| paddy@131 | 546 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 547 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 548 return |
| paddy@131 | 549 } |
| paddy@139 | 550 username, password, ok := r.BasicAuth() |
| paddy@139 | 551 if !ok { |
| paddy@139 | 552 client.Secret = "" |
| paddy@139 | 553 } else { |
| paddy@139 | 554 profile, err := authenticate(username, password, c) |
| paddy@139 | 555 if err != nil { |
| paddy@139 | 556 if isAuthError(err) { |
| paddy@139 | 557 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@139 | 558 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@139 | 559 } else { |
| paddy@139 | 560 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@139 | 561 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@139 | 562 } |
| paddy@139 | 563 return |
| paddy@139 | 564 } |
| paddy@139 | 565 if !client.OwnerID.Equal(profile.ID) { |
| paddy@139 | 566 client.Secret = "" |
| paddy@139 | 567 } |
| paddy@139 | 568 } |
| paddy@131 | 569 resp := response{ |
| paddy@131 | 570 Clients: []Client{client}, |
| paddy@131 | 571 Errors: errors, |
| paddy@131 | 572 } |
| paddy@131 | 573 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 574 } |
| paddy@131 | 575 |
| paddy@131 | 576 func ListClientsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@131 | 577 errors := []requestError{} |
| paddy@131 | 578 var err error |
| paddy@131 | 579 // BUG(paddy): If ids are provided in query params, retrieve only those clients |
| paddy@131 | 580 num := defaultClientResponseSize |
| paddy@131 | 581 offset := 0 |
| paddy@131 | 582 ownerIDStr := r.URL.Query().Get("owner_id") |
| paddy@131 | 583 numStr := r.URL.Query().Get("num") |
| paddy@131 | 584 offsetStr := r.URL.Query().Get("offset") |
| paddy@131 | 585 if numStr != "" { |
| paddy@131 | 586 num, err = strconv.Atoi(numStr) |
| paddy@131 | 587 if err != nil { |
| paddy@131 | 588 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@131 | 589 } |
| paddy@131 | 590 if num > maxClientResponseSize { |
| paddy@131 | 591 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@131 | 592 } |
| paddy@131 | 593 } |
| paddy@131 | 594 if offsetStr != "" { |
| paddy@131 | 595 offset, err = strconv.Atoi(offsetStr) |
| paddy@131 | 596 if err != nil { |
| paddy@131 | 597 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@131 | 598 } |
| paddy@131 | 599 } |
| paddy@131 | 600 if ownerIDStr == "" { |
| paddy@131 | 601 errors = append(errors, requestError{Slug: requestErrMissing, Param: "owner_id"}) |
| paddy@131 | 602 } |
| paddy@131 | 603 if len(errors) > 0 { |
| paddy@131 | 604 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@131 | 605 return |
| paddy@131 | 606 } |
| paddy@131 | 607 ownerID, err := uuid.Parse(ownerIDStr) |
| paddy@131 | 608 if err != nil { |
| paddy@131 | 609 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "owner_id"}) |
| paddy@131 | 610 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 611 return |
| paddy@131 | 612 } |
| paddy@131 | 613 clients, err := c.ListClientsByOwner(ownerID, num, offset) |
| paddy@131 | 614 if err != nil { |
| paddy@131 | 615 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@131 | 616 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@131 | 617 return |
| paddy@131 | 618 } |
| paddy@140 | 619 username, password, ok := r.BasicAuth() |
| paddy@140 | 620 if !ok { |
| paddy@140 | 621 for pos, client := range clients { |
| paddy@140 | 622 client.Secret = "" |
| paddy@140 | 623 clients[pos] = client |
| paddy@140 | 624 } |
| paddy@140 | 625 } else { |
| paddy@140 | 626 profile, err := authenticate(username, password, c) |
| paddy@140 | 627 if err != nil { |
| paddy@140 | 628 if isAuthError(err) { |
| paddy@140 | 629 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@140 | 630 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@140 | 631 } else { |
| paddy@140 | 632 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@140 | 633 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@140 | 634 } |
| paddy@140 | 635 return |
| paddy@140 | 636 } |
| paddy@140 | 637 for pos, client := range clients { |
| paddy@140 | 638 if !client.OwnerID.Equal(profile.ID) { |
| paddy@140 | 639 client.Secret = "" |
| paddy@140 | 640 clients[pos] = client |
| paddy@140 | 641 } |
| paddy@140 | 642 } |
| paddy@131 | 643 } |
| paddy@131 | 644 resp := response{ |
| paddy@131 | 645 Clients: clients, |
| paddy@131 | 646 Errors: errors, |
| paddy@131 | 647 } |
| paddy@131 | 648 encode(w, r, http.StatusOK, resp) |
| paddy@131 | 649 } |
| paddy@131 | 650 |
| paddy@133 | 651 func UpdateClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@133 | 652 errors := []requestError{} |
| paddy@133 | 653 vars := mux.Vars(r) |
| paddy@133 | 654 if _, ok := vars["id"]; !ok { |
| paddy@133 | 655 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@133 | 656 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 657 return |
| paddy@133 | 658 } |
| paddy@141 | 659 id, err := uuid.Parse(vars["id"]) |
| paddy@141 | 660 if err != nil { |
| paddy@141 | 661 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@141 | 662 } |
| paddy@141 | 663 username, password, ok := r.BasicAuth() |
| paddy@141 | 664 if !ok { |
| paddy@141 | 665 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 666 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@141 | 667 return |
| paddy@141 | 668 } |
| paddy@141 | 669 profile, err := authenticate(username, password, c) |
| paddy@141 | 670 if err != nil { |
| paddy@141 | 671 if isAuthError(err) { |
| paddy@141 | 672 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 673 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@141 | 674 } else { |
| paddy@141 | 675 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@141 | 676 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@141 | 677 } |
| paddy@141 | 678 return |
| paddy@141 | 679 } |
| paddy@133 | 680 var change ClientChange |
| paddy@141 | 681 err = decode(r, &change) |
| paddy@133 | 682 if err != nil { |
| paddy@133 | 683 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/"}) |
| paddy@133 | 684 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 685 return |
| paddy@133 | 686 } |
| paddy@133 | 687 errs := change.Validate() |
| paddy@133 | 688 for _, err := range errs { |
| paddy@133 | 689 switch err { |
| paddy@133 | 690 case ErrEmptyChange: |
| paddy@133 | 691 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/"}) |
| paddy@133 | 692 case ErrClientNameTooShort: |
| paddy@133 | 693 errors = append(errors, requestError{Slug: requestErrInsufficient, Field: "/name"}) |
| paddy@133 | 694 case ErrClientNameTooLong: |
| paddy@133 | 695 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/name"}) |
| paddy@133 | 696 case ErrClientLogoTooLong: |
| paddy@133 | 697 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/logo"}) |
| paddy@133 | 698 case ErrClientLogoNotURL: |
| paddy@133 | 699 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/logo"}) |
| paddy@133 | 700 case ErrClientWebsiteTooLong: |
| paddy@133 | 701 errors = append(errors, requestError{Slug: requestErrOverflow, Field: "/website"}) |
| paddy@133 | 702 case ErrClientWebsiteNotURL: |
| paddy@133 | 703 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/website"}) |
| paddy@133 | 704 default: |
| paddy@133 | 705 log.Println("Unrecognised error from client change validation:", err) |
| paddy@133 | 706 } |
| paddy@133 | 707 } |
| paddy@133 | 708 if len(errors) > 0 { |
| paddy@133 | 709 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@133 | 710 return |
| paddy@133 | 711 } |
| paddy@133 | 712 client, err := c.GetClient(id) |
| paddy@133 | 713 if err == ErrClientNotFound { |
| paddy@133 | 714 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@133 | 715 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@133 | 716 return |
| paddy@133 | 717 } else if err != nil { |
| paddy@133 | 718 log.Println("Error retrieving client:", err) |
| paddy@133 | 719 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 720 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 721 return |
| paddy@133 | 722 } |
| paddy@141 | 723 if !client.OwnerID.Equal(profile.ID) { |
| paddy@141 | 724 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@141 | 725 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@141 | 726 return |
| paddy@141 | 727 } |
| paddy@133 | 728 if change.Secret != nil && client.Type == clientTypeConfidential { |
| paddy@133 | 729 secret := make([]byte, 32) |
| paddy@133 | 730 _, err = rand.Read(secret) |
| paddy@133 | 731 if err != nil { |
| paddy@133 | 732 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@133 | 733 return |
| paddy@133 | 734 } |
| paddy@133 | 735 newSecret := hex.EncodeToString(secret) |
| paddy@133 | 736 change.Secret = &newSecret |
| paddy@133 | 737 } |
| paddy@133 | 738 err = c.UpdateClient(id, change) |
| paddy@133 | 739 if err != nil { |
| paddy@133 | 740 log.Println("Error updating client:", err) |
| paddy@133 | 741 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@133 | 742 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@133 | 743 return |
| paddy@133 | 744 } |
| paddy@133 | 745 client.ApplyChange(change) |
| paddy@133 | 746 encode(w, r, http.StatusOK, response{Clients: []Client{client}, Errors: errors}) |
| paddy@133 | 747 return |
| paddy@133 | 748 } |
| paddy@133 | 749 |
| paddy@144 | 750 func RemoveClientHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@144 | 751 errors := []requestError{} |
| paddy@144 | 752 vars := mux.Vars(r) |
| paddy@144 | 753 if _, ok := vars["id"]; !ok { |
| paddy@144 | 754 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@144 | 755 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@144 | 756 return |
| paddy@144 | 757 } |
| paddy@144 | 758 id, err := uuid.Parse(vars["id"]) |
| paddy@144 | 759 if err != nil { |
| paddy@144 | 760 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@144 | 761 } |
| paddy@144 | 762 username, password, ok := r.BasicAuth() |
| paddy@144 | 763 if !ok { |
| paddy@144 | 764 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@144 | 765 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@144 | 766 return |
| paddy@144 | 767 } |
| paddy@144 | 768 profile, err := authenticate(username, password, c) |
| paddy@144 | 769 if err != nil { |
| paddy@144 | 770 if isAuthError(err) { |
| paddy@144 | 771 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@144 | 772 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@144 | 773 } else { |
| paddy@144 | 774 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@144 | 775 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@144 | 776 } |
| paddy@144 | 777 return |
| paddy@144 | 778 } |
| paddy@144 | 779 client, err := c.GetClient(id) |
| paddy@144 | 780 if err != nil { |
| paddy@144 | 781 if err == ErrClientNotFound { |
| paddy@144 | 782 errors = append(errors, requestError{Slug: requestErrNotFound}) |
| paddy@144 | 783 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@144 | 784 return |
| paddy@144 | 785 } |
| paddy@144 | 786 log.Println("Error retrieving client:", err) |
| paddy@144 | 787 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@144 | 788 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@144 | 789 return |
| paddy@144 | 790 } |
| paddy@144 | 791 if !client.OwnerID.Equal(profile.ID) { |
| paddy@144 | 792 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@144 | 793 encode(w, r, http.StatusForbidden, response{Errors: errors}) |
| paddy@144 | 794 return |
| paddy@144 | 795 } |
| paddy@144 | 796 err = c.DeleteClient(id) |
| paddy@144 | 797 if err != nil { |
| paddy@144 | 798 if err == ErrClientNotFound { |
| paddy@144 | 799 errors = append(errors, requestError{Slug: requestErrNotFound}) |
| paddy@144 | 800 encode(w, r, http.StatusNotFound, response{Errors: errors}) |
| paddy@144 | 801 return |
| paddy@144 | 802 } |
| paddy@144 | 803 log.Println("Error deleting client:", err) |
| paddy@144 | 804 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@144 | 805 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@144 | 806 return |
| paddy@144 | 807 } |
| paddy@144 | 808 // BUG(paddy): Client needs to clean up after itself, invalidating tokens, deleting unused grants, deleting endpoints |
| paddy@144 | 809 encode(w, r, http.StatusOK, response{Errors: errors}) |
| paddy@144 | 810 return |
| paddy@144 | 811 } |
| paddy@144 | 812 |
| paddy@137 | 813 func AddEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@137 | 814 type addEndpointReq struct { |
| paddy@137 | 815 Endpoints []string `json:"endpoints"` |
| paddy@137 | 816 } |
| paddy@137 | 817 errors := []requestError{} |
| paddy@137 | 818 vars := mux.Vars(r) |
| paddy@137 | 819 if vars["id"] == "" { |
| paddy@137 | 820 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@137 | 821 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 822 return |
| paddy@137 | 823 } |
| paddy@137 | 824 id, err := uuid.Parse(vars["id"]) |
| paddy@137 | 825 if err != nil { |
| paddy@137 | 826 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@137 | 827 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 828 return |
| paddy@137 | 829 } |
| paddy@142 | 830 username, password, ok := r.BasicAuth() |
| paddy@142 | 831 if !ok { |
| paddy@142 | 832 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@142 | 833 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@142 | 834 return |
| paddy@142 | 835 } |
| paddy@142 | 836 profile, err := authenticate(username, password, c) |
| paddy@142 | 837 if err != nil { |
| paddy@142 | 838 if isAuthError(err) { |
| paddy@142 | 839 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@142 | 840 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@142 | 841 } else { |
| paddy@142 | 842 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@142 | 843 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@142 | 844 } |
| paddy@142 | 845 return |
| paddy@142 | 846 } |
| paddy@143 | 847 client, err := c.GetClient(id) |
| paddy@137 | 848 if err != nil { |
| paddy@137 | 849 if err == ErrClientNotFound { |
| paddy@137 | 850 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@137 | 851 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 852 return |
| paddy@137 | 853 } |
| paddy@137 | 854 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@137 | 855 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@137 | 856 return |
| paddy@137 | 857 } |
| paddy@142 | 858 if !client.OwnerID.Equal(profile.ID) { |
| paddy@142 | 859 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@142 | 860 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@142 | 861 return |
| paddy@142 | 862 } |
| paddy@137 | 863 var req addEndpointReq |
| paddy@137 | 864 decoder := json.NewDecoder(r.Body) |
| paddy@137 | 865 err = decoder.Decode(&req) |
| paddy@137 | 866 if err != nil { |
| paddy@137 | 867 encode(w, r, http.StatusBadRequest, invalidFormatResponse) |
| paddy@137 | 868 return |
| paddy@137 | 869 } |
| paddy@137 | 870 if len(req.Endpoints) < 1 { |
| paddy@137 | 871 errors = append(errors, requestError{Slug: requestErrMissing, Field: "/endpoints"}) |
| paddy@137 | 872 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 873 return |
| paddy@137 | 874 } |
| paddy@137 | 875 endpoints := []Endpoint{} |
| paddy@137 | 876 for pos, u := range req.Endpoints { |
| paddy@137 | 877 if parsed, err := url.Parse(u); err != nil { |
| paddy@137 | 878 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Field: "/endpoints/" + strconv.Itoa(pos)}) |
| paddy@137 | 879 continue |
| paddy@137 | 880 } else if !parsed.IsAbs() { |
| paddy@137 | 881 errors = append(errors, requestError{Slug: requestErrInvalidValue, Field: "/endpoints" + strconv.Itoa(pos)}) |
| paddy@137 | 882 continue |
| paddy@137 | 883 } |
| paddy@137 | 884 e := Endpoint{ |
| paddy@137 | 885 ID: uuid.NewID(), |
| paddy@137 | 886 ClientID: id, |
| paddy@137 | 887 URI: u, |
| paddy@137 | 888 Added: time.Now(), |
| paddy@137 | 889 } |
| paddy@137 | 890 endpoints = append(endpoints, e) |
| paddy@137 | 891 } |
| paddy@137 | 892 if len(errors) > 0 { |
| paddy@137 | 893 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@137 | 894 return |
| paddy@137 | 895 } |
| paddy@137 | 896 err = c.AddEndpoints(id, endpoints) |
| paddy@137 | 897 if err != nil { |
| paddy@137 | 898 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@137 | 899 return |
| paddy@137 | 900 } |
| paddy@137 | 901 resp := response{ |
| paddy@137 | 902 Errors: errors, |
| paddy@137 | 903 Endpoints: endpoints, |
| paddy@137 | 904 } |
| paddy@137 | 905 encode(w, r, http.StatusCreated, resp) |
| paddy@137 | 906 } |
| paddy@137 | 907 |
| paddy@138 | 908 func ListEndpointsHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@138 | 909 errors := []requestError{} |
| paddy@138 | 910 vars := mux.Vars(r) |
| paddy@138 | 911 clientID, err := uuid.Parse(vars["id"]) |
| paddy@138 | 912 if err != nil { |
| paddy@138 | 913 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) |
| paddy@138 | 914 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 915 return |
| paddy@138 | 916 } |
| paddy@138 | 917 num := defaultEndpointResponseSize |
| paddy@138 | 918 offset := 0 |
| paddy@138 | 919 numStr := r.URL.Query().Get("num") |
| paddy@138 | 920 offsetStr := r.URL.Query().Get("offset") |
| paddy@138 | 921 if numStr != "" { |
| paddy@138 | 922 num, err = strconv.Atoi(numStr) |
| paddy@138 | 923 if err != nil { |
| paddy@138 | 924 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "num"}) |
| paddy@138 | 925 } |
| paddy@138 | 926 if num > maxEndpointResponseSize { |
| paddy@138 | 927 errors = append(errors, requestError{Slug: requestErrOverflow, Param: "num"}) |
| paddy@138 | 928 } |
| paddy@138 | 929 } |
| paddy@138 | 930 if offsetStr != "" { |
| paddy@138 | 931 offset, err = strconv.Atoi(offsetStr) |
| paddy@138 | 932 if err != nil { |
| paddy@138 | 933 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "offset"}) |
| paddy@138 | 934 } |
| paddy@138 | 935 } |
| paddy@138 | 936 if len(errors) > 0 { |
| paddy@138 | 937 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@138 | 938 return |
| paddy@138 | 939 } |
| paddy@138 | 940 endpoints, err := c.ListEndpoints(clientID, num, offset) |
| paddy@138 | 941 if err != nil { |
| paddy@138 | 942 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@138 | 943 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@138 | 944 return |
| paddy@138 | 945 } |
| paddy@138 | 946 resp := response{ |
| paddy@138 | 947 Endpoints: endpoints, |
| paddy@138 | 948 Errors: errors, |
| paddy@138 | 949 } |
| paddy@138 | 950 encode(w, r, http.StatusOK, resp) |
| paddy@138 | 951 } |
| paddy@138 | 952 |
| paddy@143 | 953 func RemoveEndpointHandler(w http.ResponseWriter, r *http.Request, c Context) { |
| paddy@143 | 954 errors := []requestError{} |
| paddy@143 | 955 vars := mux.Vars(r) |
| paddy@143 | 956 if vars["client_id"] == "" { |
| paddy@143 | 957 errors = append(errors, requestError{Slug: requestErrMissing, Param: "client_id"}) |
| paddy@143 | 958 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 959 return |
| paddy@143 | 960 } |
| paddy@143 | 961 clientID, err := uuid.Parse(vars["client_id"]) |
| paddy@143 | 962 if err != nil { |
| paddy@143 | 963 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "client_id"}) |
| paddy@143 | 964 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 965 return |
| paddy@143 | 966 } |
| paddy@143 | 967 if vars["id"] == "" { |
| paddy@143 | 968 errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) |
| paddy@143 | 969 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 970 return |
| paddy@143 | 971 } |
| paddy@143 | 972 id, err := uuid.Parse(vars["id"]) |
| paddy@143 | 973 if err != nil { |
| paddy@143 | 974 errors = append(errors, requestError{Slug: requestErrInvalidFormat, Param: "id"}) |
| paddy@143 | 975 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 976 return |
| paddy@143 | 977 } |
| paddy@143 | 978 username, password, ok := r.BasicAuth() |
| paddy@143 | 979 if !ok { |
| paddy@143 | 980 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@143 | 981 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@143 | 982 return |
| paddy@143 | 983 } |
| paddy@143 | 984 profile, err := authenticate(username, password, c) |
| paddy@143 | 985 if err != nil { |
| paddy@143 | 986 if isAuthError(err) { |
| paddy@143 | 987 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@143 | 988 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@143 | 989 } else { |
| paddy@143 | 990 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@143 | 991 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@143 | 992 } |
| paddy@143 | 993 return |
| paddy@143 | 994 } |
| paddy@143 | 995 client, err := c.GetClient(clientID) |
| paddy@143 | 996 if err != nil { |
| paddy@143 | 997 if err == ErrClientNotFound { |
| paddy@143 | 998 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "client_id"}) |
| paddy@143 | 999 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 1000 return |
| paddy@143 | 1001 } |
| paddy@143 | 1002 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@143 | 1003 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@143 | 1004 return |
| paddy@143 | 1005 } |
| paddy@143 | 1006 if !client.OwnerID.Equal(profile.ID) { |
| paddy@143 | 1007 errors = append(errors, requestError{Slug: requestErrAccessDenied}) |
| paddy@143 | 1008 encode(w, r, http.StatusUnauthorized, response{Errors: errors}) |
| paddy@143 | 1009 return |
| paddy@143 | 1010 } |
| paddy@143 | 1011 endpoint, err := c.GetEndpoint(clientID, id) |
| paddy@143 | 1012 if err != nil { |
| paddy@143 | 1013 if err == ErrEndpointNotFound { |
| paddy@143 | 1014 errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) |
| paddy@143 | 1015 encode(w, r, http.StatusBadRequest, response{Errors: errors}) |
| paddy@143 | 1016 return |
| paddy@143 | 1017 } |
| paddy@143 | 1018 errors = append(errors, requestError{Slug: requestErrActOfGod}) |
| paddy@143 | 1019 encode(w, r, http.StatusInternalServerError, response{Errors: errors}) |
| paddy@143 | 1020 return |
| paddy@143 | 1021 } |
| paddy@143 | 1022 err = c.RemoveEndpoint(clientID, id) |
| paddy@143 | 1023 if err != nil { |
| paddy@143 | 1024 encode(w, r, http.StatusInternalServerError, actOfGodResponse) |
| paddy@143 | 1025 return |
| paddy@143 | 1026 } |
| paddy@143 | 1027 resp := response{ |
| paddy@143 | 1028 Errors: errors, |
| paddy@143 | 1029 Endpoints: []Endpoint{endpoint}, |
| paddy@143 | 1030 } |
| paddy@143 | 1031 encode(w, r, http.StatusCreated, resp) |
| paddy@143 | 1032 } |
| paddy@143 | 1033 |
| paddy@135 | 1034 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) { |
| paddy@135 | 1035 scopes = strings.Split(r.PostFormValue("scope"), " ") |
| paddy@121 | 1036 valid = true |
| paddy@121 | 1037 return |
| paddy@121 | 1038 } |
| paddy@124 | 1039 |
| paddy@124 | 1040 func clientCredentialsAuditString(r *http.Request) string { |
| paddy@124 | 1041 return "client_credentials" |
| paddy@124 | 1042 } |