ducky/devices

Paddy 2016-01-02 Parent:b2fdf827758e

20:ed1b5ba69551 Go to Latest

ducky/devices/apiv1/handlers.go

Add updating devices to apiv1. We needed a way to be able to update devices after they were created. This is supported in the devices package, we just needed to expose it using apiv1 endpoints. In doing so, it became apparent that allowing users to change the Owner of their Devices wasn't properly thought through, and pending a reason to use it, I'm just removing it. The biggest issue came when trying to return usable error messages; we couldn't distinguish between "you don't own the device you're trying to update" and "you're not allowed to change the owner of the device". I also couldn't figure out _who should be able to_ change the owner of the device, which is generally an indication that I'm building a feature before I have a use case for it. To support this change, the apiv1.DeviceChange type needed its Owner property removed. I also needed to add deviceFromAPI and devicesFromAPI helpers to return devices.Device types from apiv1.Device types. There's now a new validateDeviceUpdate helper that checks to ensure that a device update request is valid and the user has the appropriate permissions. The createRequest type now accepts a slice of Devices, not a slice of DeviceChanges, because we want to pass the Owner in. A new updateRequest type is created, which accepts a DeviceChange to apply. A new handleUpdateDevice handler is created, which is assigned to the endpoint for PATCH requests against a device ID. It checks that the user is logged in, the Device they're trying to update exists, and that it's a valid update. If all of that is true, the device is updated and the updated device is returned. Finally, we had to add two new scopes to support new functionality: ScopeUpdateOtherUserDevices allows a user to update other user's devices, and ScopeUpdateLastSeen allows a user to update the LastSeen property of a device. Pending some better error messages, this should be a full implementation of updating a device, which leaves only the deletion endpoint to deal with.

History
1 package apiv1
3 import (
4 "fmt"
5 "log"
6 "net/http"
8 "code.secondbit.org/api.hg"
9 "code.secondbit.org/ducky/devices.hg"
10 "code.secondbit.org/trout.hg"
11 "code.secondbit.org/uuid.hg"
13 "golang.org/x/net/context"
14 )
16 type createRequest struct {
17 Devices []Device `json:"devices"`
18 }
20 type updateRequest struct {
21 DeviceChange DeviceChange `json:"device"`
22 }
24 func handleCreateDevices(ctx context.Context, w http.ResponseWriter, r *http.Request) {
25 var req createRequest
27 userID, err := api.AuthUser(r)
28 if err != nil {
29 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
30 return
31 }
33 err = api.Decode(r, &req)
34 if err != nil {
35 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
36 return
37 }
39 devicesToCreate := devicesFromAPI(req.Devices)
40 passedScopes := api.GetScopes(r)
41 for pos, device := range devicesToCreate {
42 err := validateDeviceCreation(device, passedScopes, userID)
43 if err == nil {
44 continue
45 }
46 var requestErr api.RequestError
47 switch err {
48 case errUnauthorizedLastSeen:
49 requestErr.Slug = api.RequestErrAccessDenied
50 requestErr.Field = fmt.Sprintf("devices/%d/lastSeen", pos)
51 case errUnauthorizedCreated:
52 requestErr.Slug = api.RequestErrAccessDenied
53 requestErr.Field = fmt.Sprintf("devices/%d/created", pos)
54 case errUnauthorizedOwner:
55 requestErr.Slug = api.RequestErrAccessDenied
56 requestErr.Field = fmt.Sprintf("devices/%d/owner", pos)
57 case errInvalidDeviceType:
58 requestErr.Slug = api.RequestErrInvalidValue
59 requestErr.Field = fmt.Sprintf("devices/%d/type", pos)
60 case errDeviceNameTooShort:
61 if len(device.Name) == 0 {
62 requestErr.Slug = api.RequestErrMissing
63 } else {
64 requestErr.Slug = api.RequestErrInsufficient
65 }
66 requestErr.Field = fmt.Sprintf("devices/%d/name", pos)
67 case errDeviceNameTooLong:
68 requestErr.Slug = api.RequestErrOverflow
69 requestErr.Field = fmt.Sprintf("devices/%d/name", pos)
70 }
71 if requestErr.Slug != "" {
72 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{requestErr}})
73 return
74 }
75 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
76 }
77 createdDevices, err := devices.CreateMany(devicesToCreate, ctx)
78 if err != nil {
79 // BUG(paddy): we should filter out non-internal errors here and expose better error responses
80 log.Printf("Error creating devices: %+v\n", err)
81 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
82 return
83 }
84 var resp Response
85 for _, device := range createdDevices {
86 resp.Devices = append(resp.Devices, apiDeviceFromCore(device, api.CheckScopes(passedScopes, ScopeViewPushToken.ID)))
87 }
88 api.Encode(w, r, http.StatusCreated, resp)
89 }
91 func handleGetDevices(ctx context.Context, w http.ResponseWriter, r *http.Request) {
92 userID, err := api.AuthUser(r)
93 if err != nil {
94 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
95 return
96 }
98 passedScopes := api.GetScopes(r)
99 if !api.CheckScopes(passedScopes, ScopeViewDevices.ID) {
100 api.Encode(w, r, http.StatusForbidden, Response{Errors: api.AccessDeniedError})
101 return
102 }
104 var retrievedDevices []devices.Device
105 requestedIDStrs := r.URL.Query()["id"]
106 requestedIDStrs = append(requestedIDStrs, trout.RequestVars(r)[http.CanonicalHeaderKey("id")]...)
108 if len(requestedIDStrs) < 1 {
109 retrievedDevices, err = devices.ListByOwner(userID, ctx)
110 if err != nil {
111 log.Printf("Error listing devices: %+v\n", err)
112 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
113 return
114 }
115 } else {
116 requestedIDs := make([]uuid.ID, 0, len(requestedIDStrs))
117 var reqErrs []api.RequestError
118 for pos, idStr := range requestedIDStrs {
119 id, err := uuid.Parse(idStr)
120 if err != nil {
121 reqErrs = append(reqErrs, api.RequestError{Slug: api.RequestErrInvalidFormat, Param: fmt.Sprintf("id[%d]", pos)})
122 continue
123 }
124 requestedIDs = append(requestedIDs, id)
125 }
126 if len(reqErrs) > 0 {
127 api.Encode(w, r, http.StatusBadRequest, Response{Errors: reqErrs})
128 return
129 }
130 getDevices, err := devices.GetMany(requestedIDs, ctx)
131 if err != nil {
132 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
133 return
134 }
135 for _, device := range getDevices {
136 if device.Owner.Equal(userID) {
137 retrievedDevices = append(retrievedDevices, device)
138 }
139 }
140 }
142 var resp Response
143 for _, device := range retrievedDevices {
144 resp.Devices = append(resp.Devices, apiDeviceFromCore(device, api.CheckScopes(passedScopes, ScopeViewPushToken.ID)))
145 }
146 api.Encode(w, r, http.StatusOK, resp)
147 }
149 func handleUpdateDevice(ctx context.Context, w http.ResponseWriter, r *http.Request) {
150 userID, err := api.AuthUser(r)
151 if err != nil {
152 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
153 return
154 }
156 id, err := uuid.Parse(trout.RequestVars(r).Get("id"))
157 if err != nil {
158 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
159 return
160 }
161 device, err := devices.Get(id, ctx)
162 if err != nil {
163 if err == devices.ErrDeviceNotFound {
164 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
165 return
166 }
167 log.Printf("Error retrieving device %s: %+v\n", id, err)
168 api.Encode(w, r, http.StatusInternalServerError, api.ActOfGodError)
169 return
170 }
172 var req updateRequest
173 err = api.Decode(r, &req)
174 if err != nil {
175 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
176 return
177 }
179 passedScopes := api.GetScopes(r)
180 err = validateDeviceUpdate(apiDeviceFromCore(device, true), req.DeviceChange, passedScopes, userID)
181 var requestErr api.RequestError
182 switch err {
183 case errUnauthorizedLastSeen:
184 requestErr.Slug = api.RequestErrAccessDenied
185 requestErr.Field = "device/lastSeen"
186 case errUnauthorizedCreated:
187 requestErr.Slug = api.RequestErrAccessDenied
188 requestErr.Field = "device/created"
189 case errUnauthorizedOwner:
190 requestErr.Slug = api.RequestErrAccessDenied
191 requestErr.Field = "device/owner"
192 case errInvalidDeviceType:
193 requestErr.Slug = api.RequestErrInvalidValue
194 requestErr.Field = "device/type"
195 case errDeviceNameTooShort:
196 if len(device.Name) == 0 {
197 requestErr.Slug = api.RequestErrMissing
198 } else {
199 requestErr.Slug = api.RequestErrInsufficient
200 }
201 requestErr.Field = "device/name"
202 case errDeviceNameTooLong:
203 requestErr.Slug = api.RequestErrOverflow
204 requestErr.Field = "device/name"
205 }
206 if requestErr.Slug != "" {
207 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{requestErr}})
208 return
209 }
210 if err != nil {
211 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
212 return
213 }
215 change := changeFromAPI(req.DeviceChange)
216 updatedDevice, err := devices.Update(device, change, ctx)
217 if err != nil {
218 // BUG(paddy): we should filter out non-internal errors here and expose better error responses
219 log.Printf("Error updating device %s: %+v\n", id, err)
220 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
221 return
222 }
223 resp := Response{Devices: []Device{apiDeviceFromCore(updatedDevice, api.CheckScopes(passedScopes, ScopeViewPushToken.ID))}}
224 api.Encode(w, r, http.StatusOK, resp)
225 }