Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement endpoint POST features/access #6

Merged
merged 2 commits into from
Dec 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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