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