Skip to content

Commit

Permalink
Merge pull request #6 from AntoineAugusti/get-accessible-features-for…
Browse files Browse the repository at this point in the history
…-request

Implement endpoint POST features/access
  • Loading branch information
AntoineAugusti committed Dec 14, 2015
2 parents a8f7221 + 397deee commit 159619b
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 25 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ This API does not ship with an authentication layer. You **should not** expose t
- [`GET` /features/:featureKey](#get-featuresfeaturekey) - Get a single feature flag
- [`DELETE` /features/:featureKey](#delete-featuresfeaturekey) - Delete a feature flag
- [`PATCH` /features/:featureKey](#patch-featuresfeaturekey) - Update a feature flag
- [`POST` /features/:featureKey/access](#get-featuresfeaturekeyaccess) - Check if someone has access to a feature
- [`POST` /features/access](#post-featuresaccess) - Get accessible features for a user or some groups
- [`POST` /features/:featureKey/access](#post-featuresfeaturekeyaccess) - Check if a user or some groups have access to a feature

### API Documentation
#### `GET` `/features`
Get a list of available feature flags.
- Method: `GET`
- Endpoint: `/features`
- Responses:
* **200** on success
* 200 OK
```json
[
{
Expand Down Expand Up @@ -249,6 +250,34 @@ Update a feature flag.
Common reason:
- the percentage must be between `0` and `100`

#### `POST` `/features/access`
Get a list of accessible features for a user or a list of groups.
- Method: `POST`
- Endpoint: `/features/ccess`
- Input:
The `Content-Type` HTTP header should be set to `application/json`

```json
{
"groups":[
"dev",
"test"
],
"user":42
}
```
- Responses:
* 200 OK

Same as in [`POST` /features](#post-features). An empty array indicates that no known features are accessible for the given input.
* 422 Unprocessable entity:
```json
{
"status":"invalid_json",
"message":"Cannot decode the given JSON payload"
}
```

#### `POST` `/features/:featureKey/access`
Check if a feature flag is enabled for a user or a list of groups.
- Method: `POST`
Expand Down
71 changes: 55 additions & 16 deletions http/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ func (handler APIHandler) FeatureShow(w http.ResponseWriter, r *http.Request) {
}
}

func (handler APIHandler) FeaturesAccess(w http.ResponseWriter, r *http.Request) {
var ar AccessRequest

// Get all features in the bucket
features, err := handler.FeatureService.GetFeatures()
if err != nil {
panic(err)
}

// Decode the access request
err = json.NewDecoder(r.Body).Decode(&ar)
if err != nil {
writeUnprocessableEntity(err, w)
return
}

// Keep only accessible features
accessibleFeatures := make(m.FeatureFlags, 0)
for _, feature := range features {
if hasAccessToFeature(feature, ar) {
accessibleFeatures = append(accessibleFeatures, feature)
}
}

w.Header().Set("Content-Type", getJsonHeader())
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(accessibleFeatures); err != nil {
panic(err)
}
}

func (handler APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request) {
var ar AccessRequest
vars := mux.Vars(r)
Expand All @@ -81,29 +112,14 @@ func (handler APIHandler) FeatureAccess(w http.ResponseWriter, r *http.Request)
panic(err)
}

hasAccess := feature.IsEnabled()

// Decode the access request
err = json.NewDecoder(r.Body).Decode(&ar)
if err != nil {
writeUnprocessableEntity(err, w)
return
}

if len(ar.Groups) > 0 {
for _, group := range ar.Groups {
if feature.GroupHasAccess(group) {
hasAccess = true
break
}
}
}

if ar.User > 0 && !hasAccess {
hasAccess = feature.UserHasAccess(ar.User)
}

if hasAccess {
if hasAccessToFeature(feature, ar) {
writeMessage(http.StatusOK, "has_access", "The user has access to the feature", w)
} else {
writeMessage(http.StatusOK, "not_access", "The user does not have access to the feature", w)
Expand Down Expand Up @@ -217,3 +233,26 @@ func writeMessage(code int, status string, message string, w http.ResponseWriter
w.WriteHeader(apiMessage.code)
w.Write(bytes)
}

func hasAccessToFeature(feature m.FeatureFlag, ar AccessRequest) bool {
// Handle trivial case
if feature.IsEnabled() {
return true
}

// Access thanks to a group?
if len(ar.Groups) > 0 {
for _, group := range ar.Groups {
if feature.GroupHasAccess(group) {
return true
}
}
}

// Access thanks to the user?
if ar.User > 0 {
return feature.UserHasAccess(ar.User)
}

return false
}
78 changes: 72 additions & 6 deletions http/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,30 +170,92 @@ func TestEditFeatureFlag(t *testing.T) {
assertResponseWithStatusAndMessage(t, res, http.StatusBadRequest, "invalid_feature", "Percentage must be between 0 and 100")
}

func TestAccessFeatureFlag(t *testing.T) {
func TestAccessFeatureFlags(t *testing.T) {
var features m.FeatureFlags
onStart()
defer onFinish()

url := fmt.Sprintf("%s/access", base)

// Add the default dummy feature
createDummyFeatureFlag()

// Invalid JSON payload
reader = strings.NewReader(`{foo:bar}`)
request, _ := http.NewRequest("POST", url, reader)
res, _ := http.DefaultClient.Do(request)
assert422Response(t, res)

// Access thanks to the user ID
reader = strings.NewReader(`{"user":2}`)
request, _ := http.NewRequest("POST", fmt.Sprintf("%s/%s/access", base, "homepage_v2"), reader)
request, _ = http.NewRequest("POST", url, reader)
res, _ = http.DefaultClient.Do(request)

json.NewDecoder(res.Body).Decode(&features)
assert.Equal(t, 1, len(features))
assert.Equal(t, "homepage_v2", features[0].Key)

// No access because of the user ID
reader = strings.NewReader(`{"user":0}`)
request, _ = http.NewRequest("POST", url, reader)
res, _ = http.DefaultClient.Do(request)

json.NewDecoder(res.Body).Decode(&features)
assert.Equal(t, 0, len(features))

// Add a feature enabled for everybody
payload := `{
"key":"testflag",
"enabled":true,
"users":[],
"groups":[],
"percentage":0
}`
createFeatureWithPayload(payload)

// Access thanks to the group
reader = strings.NewReader(`{"groups":["dev"]}`)
request, _ = http.NewRequest("POST", url, reader)
res, _ = http.DefaultClient.Do(request)

json.NewDecoder(res.Body).Decode(&features)
assert.Equal(t, 2, len(features))
assert.Equal(t, "homepage_v2", features[0].Key)
assert.Equal(t, "testflag", features[1].Key)
}

func TestAccessFeatureFlag(t *testing.T) {
onStart()
defer onFinish()

url := fmt.Sprintf("%s/%s/access", base, "homepage_v2")

// Add the default dummy feature
createDummyFeatureFlag()

// Invalid JSON payload
reader = strings.NewReader(`{foo:bar}`)
request, _ := http.NewRequest("POST", url, reader)
res, _ := http.DefaultClient.Do(request)
assert422Response(t, res)

// Access thanks to the user ID
reader = strings.NewReader(`{"user":2}`)
request, _ = http.NewRequest("POST", url, reader)
res, _ = http.DefaultClient.Do(request)

assertAccessToTheFeature(t, res)

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

assertNoAccessToTheFeature(t, res)

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

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

func createDummyFeatureFlag() *http.Response {
reader = strings.NewReader(getDummyFeaturePayload())
func createFeatureWithPayload(payload string) *http.Response {
reader = strings.NewReader(payload)
postRequest, _ := http.NewRequest("POST", base, reader)
res, err := http.DefaultClient.Do(postRequest)
if err != nil {
Expand All @@ -240,6 +302,10 @@ func createDummyFeatureFlag() *http.Response {
return res
}

func createDummyFeatureFlag() *http.Response {
return createFeatureWithPayload(getDummyFeaturePayload())
}

func assert422Response(t *testing.T, res *http.Response) {
assertResponseWithStatusAndMessage(t, res, 422, "invalid_json", "Cannot decode the given JSON payload")
}
Expand Down
9 changes: 8 additions & 1 deletion http/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ func getRoutes(api APIHandler) Routes {
"/features/{featureKey}",
api.FeatureShow,
},
// curl -H "Content-Type: application/json" -X POST -d '{"groups":"foo"}' -X GET http://localhost:8080/features/feature_test/access
// curl -H "Content-Type: application/json" -X POST -d '{"groups":["foo"]}' http://localhost:8080/features/access
Route{
"FeaturesAccess",
"POST",
"/features/access",
api.FeaturesAccess,
},
// curl -H "Content-Type: application/json" -X POST -d '{"groups":["foo"]}' http://localhost:8080/features/feature_test/access
Route{
"FeatureAccess",
"POST",
Expand Down

0 comments on commit 159619b

Please sign in to comment.