Skip to content

Commit 159619b

Browse files
Merge pull request #6 from AntoineAugusti/get-accessible-features-for-request
Implement endpoint POST features/access
2 parents a8f7221 + 397deee commit 159619b

File tree

4 files changed

+166
-25
lines changed

4 files changed

+166
-25
lines changed

README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ This API does not ship with an authentication layer. You **should not** expose t
4949
- [`GET` /features/:featureKey](#get-featuresfeaturekey) - Get a single feature flag
5050
- [`DELETE` /features/:featureKey](#delete-featuresfeaturekey) - Delete a feature flag
5151
- [`PATCH` /features/:featureKey](#patch-featuresfeaturekey) - Update a feature flag
52-
- [`POST` /features/:featureKey/access](#get-featuresfeaturekeyaccess) - Check if someone has access to a feature
52+
- [`POST` /features/access](#post-featuresaccess) - Get accessible features for a user or some groups
53+
- [`POST` /features/:featureKey/access](#post-featuresfeaturekeyaccess) - Check if a user or some groups have access to a feature
5354

5455
### API Documentation
5556
#### `GET` `/features`
5657
Get a list of available feature flags.
5758
- Method: `GET`
5859
- Endpoint: `/features`
5960
- Responses:
60-
* **200** on success
61+
* 200 OK
6162
```json
6263
[
6364
{
@@ -249,6 +250,34 @@ Update a feature flag.
249250
Common reason:
250251
- the percentage must be between `0` and `100`
251252

253+
#### `POST` `/features/access`
254+
Get a list of accessible features for a user or a list of groups.
255+
- Method: `POST`
256+
- Endpoint: `/features/ccess`
257+
- Input:
258+
The `Content-Type` HTTP header should be set to `application/json`
259+
260+
```json
261+
{
262+
"groups":[
263+
"dev",
264+
"test"
265+
],
266+
"user":42
267+
}
268+
```
269+
- Responses:
270+
* 200 OK
271+
272+
Same as in [`POST` /features](#post-features). An empty array indicates that no known features are accessible for the given input.
273+
* 422 Unprocessable entity:
274+
```json
275+
{
276+
"status":"invalid_json",
277+
"message":"Cannot decode the given JSON payload"
278+
}
279+
```
280+
252281
#### `POST` `/features/:featureKey/access`
253282
Check if a feature flag is enabled for a user or a list of groups.
254283
- Method: `POST`

http/handlers.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,37 @@ func (handler APIHandler) FeatureShow(w http.ResponseWriter, r *http.Request) {
6565
}
6666
}
6767

68+
func (handler APIHandler) FeaturesAccess(w http.ResponseWriter, r *http.Request) {
69+
var ar AccessRequest
70+
71+
// Get all features in the bucket
72+
features, err := handler.FeatureService.GetFeatures()
73+
if err != nil {
74+
panic(err)
75+
}
76+
77+
// Decode the access request
78+
err = json.NewDecoder(r.Body).Decode(&ar)
79+
if err != nil {
80+
writeUnprocessableEntity(err, w)
81+
return
82+
}
83+
84+
// Keep only accessible features
85+
accessibleFeatures := make(m.FeatureFlags, 0)
86+
for _, feature := range features {
87+
if hasAccessToFeature(feature, ar) {
88+
accessibleFeatures = append(accessibleFeatures, feature)
89+
}
90+
}
91+
92+
w.Header().Set("Content-Type", getJsonHeader())
93+
w.WriteHeader(http.StatusOK)
94+
if err := json.NewEncoder(w).Encode(accessibleFeatures); err != nil {
95+
panic(err)
96+
}
97+
}
98+
6899
func (handler APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request) {
69100
var ar AccessRequest
70101
vars := mux.Vars(r)
@@ -81,29 +112,14 @@ func (handler APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request)
81112
panic(err)
82113
}
83114

84-
hasAccess := feature.IsEnabled()
85-
86115
// Decode the access request
87116
err = json.NewDecoder(r.Body).Decode(&ar)
88117
if err != nil {
89118
writeUnprocessableEntity(err, w)
90119
return
91120
}
92121

93-
if len(ar.Groups) > 0 {
94-
for _, group := range ar.Groups {
95-
if feature.GroupHasAccess(group) {
96-
hasAccess = true
97-
break
98-
}
99-
}
100-
}
101-
102-
if ar.User > 0 && !hasAccess {
103-
hasAccess = feature.UserHasAccess(ar.User)
104-
}
105-
106-
if hasAccess {
122+
if hasAccessToFeature(feature, ar) {
107123
writeMessage(http.StatusOK, "has_access", "The user has access to the feature", w)
108124
} else {
109125
writeMessage(http.StatusOK, "not_access", "The user does not have access to the feature", w)
@@ -217,3 +233,26 @@ func writeMessage(code int, status string, message string, w http.ResponseWriter
217233
w.WriteHeader(apiMessage.code)
218234
w.Write(bytes)
219235
}
236+
237+
func hasAccessToFeature(feature m.FeatureFlag, ar AccessRequest) bool {
238+
// Handle trivial case
239+
if feature.IsEnabled() {
240+
return true
241+
}
242+
243+
// Access thanks to a group?
244+
if len(ar.Groups) > 0 {
245+
for _, group := range ar.Groups {
246+
if feature.GroupHasAccess(group) {
247+
return true
248+
}
249+
}
250+
}
251+
252+
// Access thanks to the user?
253+
if ar.User > 0 {
254+
return feature.UserHasAccess(ar.User)
255+
}
256+
257+
return false
258+
}

http/handlers_test.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,92 @@ func TestEditFeatureFlag(t *testing.T) {
170170
assertResponseWithStatusAndMessage(t, res, http.StatusBadRequest, "invalid_feature", "Percentage must be between 0 and 100")
171171
}
172172

173-
func TestAccessFeatureFlag(t *testing.T) {
173+
func TestAccessFeatureFlags(t *testing.T) {
174+
var features m.FeatureFlags
174175
onStart()
175176
defer onFinish()
176177

178+
url := fmt.Sprintf("%s/access", base)
179+
177180
// Add the default dummy feature
178181
createDummyFeatureFlag()
179182

183+
// Invalid JSON payload
184+
reader = strings.NewReader(`{foo:bar}`)
185+
request, _ := http.NewRequest("POST", url, reader)
186+
res, _ := http.DefaultClient.Do(request)
187+
assert422Response(t, res)
188+
180189
// Access thanks to the user ID
181190
reader = strings.NewReader(`{"user":2}`)
182-
request, _ := http.NewRequest("POST", fmt.Sprintf("%s/%s/access", base, "homepage_v2"), reader)
191+
request, _ = http.NewRequest("POST", url, reader)
192+
res, _ = http.DefaultClient.Do(request)
193+
194+
json.NewDecoder(res.Body).Decode(&features)
195+
assert.Equal(t, 1, len(features))
196+
assert.Equal(t, "homepage_v2", features[0].Key)
197+
198+
// No access because of the user ID
199+
reader = strings.NewReader(`{"user":0}`)
200+
request, _ = http.NewRequest("POST", url, reader)
201+
res, _ = http.DefaultClient.Do(request)
202+
203+
json.NewDecoder(res.Body).Decode(&features)
204+
assert.Equal(t, 0, len(features))
205+
206+
// Add a feature enabled for everybody
207+
payload := `{
208+
"key":"testflag",
209+
"enabled":true,
210+
"users":[],
211+
"groups":[],
212+
"percentage":0
213+
}`
214+
createFeatureWithPayload(payload)
215+
216+
// Access thanks to the group
217+
reader = strings.NewReader(`{"groups":["dev"]}`)
218+
request, _ = http.NewRequest("POST", url, reader)
219+
res, _ = http.DefaultClient.Do(request)
220+
221+
json.NewDecoder(res.Body).Decode(&features)
222+
assert.Equal(t, 2, len(features))
223+
assert.Equal(t, "homepage_v2", features[0].Key)
224+
assert.Equal(t, "testflag", features[1].Key)
225+
}
226+
227+
func TestAccessFeatureFlag(t *testing.T) {
228+
onStart()
229+
defer onFinish()
230+
231+
url := fmt.Sprintf("%s/%s/access", base, "homepage_v2")
232+
233+
// Add the default dummy feature
234+
createDummyFeatureFlag()
235+
236+
// Invalid JSON payload
237+
reader = strings.NewReader(`{foo:bar}`)
238+
request, _ := http.NewRequest("POST", url, reader)
183239
res, _ := http.DefaultClient.Do(request)
240+
assert422Response(t, res)
241+
242+
// Access thanks to the user ID
243+
reader = strings.NewReader(`{"user":2}`)
244+
request, _ = http.NewRequest("POST", url, reader)
245+
res, _ = http.DefaultClient.Do(request)
184246

185247
assertAccessToTheFeature(t, res)
186248

187249
// No access because of the user ID
188250
reader = strings.NewReader(`{"user":3}`)
189-
request, _ = http.NewRequest("POST", fmt.Sprintf("%s/%s/access", base, "homepage_v2"), reader)
251+
request, _ = http.NewRequest("POST", url, reader)
190252
res, _ = http.DefaultClient.Do(request)
191253

192254
assertNoAccessToTheFeature(t, res)
193255

194256
// Access thanks to the group
195257
reader = strings.NewReader(`{"user":3, "groups":["dev", "foo"]}`)
196-
request, _ = http.NewRequest("POST", fmt.Sprintf("%s/%s/access", base, "homepage_v2"), reader)
258+
request, _ = http.NewRequest("POST", url, reader)
197259
res, _ = http.DefaultClient.Do(request)
198260

199261
assertAccessToTheFeature(t, res)
@@ -229,8 +291,8 @@ func assertNoAccessToTheFeature(t *testing.T, res *http.Response) {
229291
assertResponseWithStatusAndMessage(t, res, http.StatusOK, "not_access", "The user does not have access to the feature")
230292
}
231293

232-
func createDummyFeatureFlag() *http.Response {
233-
reader = strings.NewReader(getDummyFeaturePayload())
294+
func createFeatureWithPayload(payload string) *http.Response {
295+
reader = strings.NewReader(payload)
234296
postRequest, _ := http.NewRequest("POST", base, reader)
235297
res, err := http.DefaultClient.Do(postRequest)
236298
if err != nil {
@@ -240,6 +302,10 @@ func createDummyFeatureFlag() *http.Response {
240302
return res
241303
}
242304

305+
func createDummyFeatureFlag() *http.Response {
306+
return createFeatureWithPayload(getDummyFeaturePayload())
307+
}
308+
243309
func assert422Response(t *testing.T, res *http.Response) {
244310
assertResponseWithStatusAndMessage(t, res, 422, "invalid_json", "Cannot decode the given JSON payload")
245311
}

http/routes.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ func getRoutes(api APIHandler) Routes {
4343
"/features/{featureKey}",
4444
api.FeatureShow,
4545
},
46-
// curl -H "Content-Type: application/json" -X POST -d '{"groups":"foo"}' -X GET http://localhost:8080/features/feature_test/access
46+
// curl -H "Content-Type: application/json" -X POST -d '{"groups":["foo"]}' http://localhost:8080/features/access
47+
Route{
48+
"FeaturesAccess",
49+
"POST",
50+
"/features/access",
51+
api.FeaturesAccess,
52+
},
53+
// curl -H "Content-Type: application/json" -X POST -d '{"groups":["foo"]}' http://localhost:8080/features/feature_test/access
4754
Route{
4855
"FeatureAccess",
4956
"POST",

0 commit comments

Comments
 (0)