-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add route to fetch OAuth clients usage (#4087)
This will be useful to know how many OAuth clients a user has already connected to their Cozy and how many they're allowed to create, especially for the flagship app to decide whether it should complete its onboarding flow or present the user with a modal requesting to either remove some clients or increase the limit if they're able to.
- Loading branch information
Showing
5 changed files
with
248 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package settings | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/cozy/cozy-stack/model/feature" | ||
"github.com/cozy/cozy-stack/model/oauth" | ||
"github.com/cozy/cozy-stack/model/permission" | ||
"github.com/cozy/cozy-stack/pkg/consts" | ||
"github.com/cozy/cozy-stack/pkg/couchdb" | ||
"github.com/cozy/cozy-stack/pkg/jsonapi" | ||
"github.com/cozy/cozy-stack/web/middlewares" | ||
"github.com/labstack/echo/v4" | ||
) | ||
|
||
type apiClientsUsage struct { | ||
Limit *int `json:"limit,omitempty"` | ||
Count int `json:"count"` | ||
LimitReached bool `json:"limitReached"` | ||
LimitExceeded bool `json:"limitExceeded"` | ||
} | ||
|
||
func (j *apiClientsUsage) ID() string { return consts.ClientsUsageID } | ||
func (j *apiClientsUsage) Rev() string { return "" } | ||
func (j *apiClientsUsage) DocType() string { return consts.Settings } | ||
func (j *apiClientsUsage) Clone() couchdb.Doc { return j } | ||
func (j *apiClientsUsage) SetID(_ string) {} | ||
func (j *apiClientsUsage) SetRev(_ string) {} | ||
func (j *apiClientsUsage) Relationships() jsonapi.RelationshipMap { return nil } | ||
func (j *apiClientsUsage) Included() []jsonapi.Object { return nil } | ||
func (j *apiClientsUsage) Links() *jsonapi.LinksList { | ||
return &jsonapi.LinksList{Self: "/settings/clients-usage"} | ||
} | ||
|
||
// Settings objects permissions are only on ID | ||
func (j *apiClientsUsage) Fetch(field string) []string { return nil } | ||
|
||
func (h *HTTPHandler) clientsUsage(c echo.Context) error { | ||
inst := middlewares.GetInstance(c) | ||
var result apiClientsUsage | ||
|
||
if err := middlewares.Allow(c, permission.GET, &result); err != nil { | ||
return err | ||
} | ||
|
||
flags, err := feature.GetFlags(inst) | ||
if err != nil { | ||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("Could not get flags: %w", err)) | ||
} | ||
|
||
limit := -1 | ||
if clientsLimit, ok := flags.M["cozy.oauthclients.max"].(float64); ok && clientsLimit >= 0 { | ||
limit = int(clientsLimit) | ||
} | ||
|
||
clients, _, err := oauth.GetConnectedUserClients(inst, 100, "") | ||
if err != nil { | ||
return fmt.Errorf("Could not get user OAuth clients: %w", err) | ||
} | ||
count := len(clients) | ||
|
||
if limit != -1 { | ||
result.Limit = &limit | ||
|
||
if count >= limit { | ||
result.LimitReached = true | ||
} | ||
if count > limit { | ||
result.LimitExceeded = true | ||
} | ||
} | ||
result.Count = count | ||
return jsonapi.Data(c, http.StatusOK, &result, nil) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package settings_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/cozy/cozy-stack/model/instance" | ||
"github.com/cozy/cozy-stack/model/instance/lifecycle" | ||
"github.com/cozy/cozy-stack/model/oauth" | ||
csettings "github.com/cozy/cozy-stack/model/settings" | ||
"github.com/cozy/cozy-stack/pkg/config/config" | ||
"github.com/cozy/cozy-stack/pkg/consts" | ||
"github.com/cozy/cozy-stack/tests/testutils" | ||
"github.com/gavv/httpexpect/v2" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func setClientsLimit(t *testing.T, inst *instance.Instance, limit float64) { | ||
inst.FeatureFlags = map[string]interface{}{"cozy.oauthclients.max": limit} | ||
require.NoError(t, instance.Update(inst)) | ||
} | ||
|
||
func TestClientsUsage(t *testing.T) { | ||
config.UseTestFile(t) | ||
testutils.NeedCouchdb(t) | ||
setup := testutils.NewSetup(t, t.Name()) | ||
testInstance := setup.GetTestInstance(&lifecycle.Options{ | ||
Locale: "en", | ||
Timezone: "Europe/Berlin", | ||
Email: "[email protected]", | ||
ContextName: "test-context", | ||
}) | ||
scope := consts.Settings + " " + consts.OAuthClients | ||
_, token := setup.GetTestClient(scope) | ||
|
||
svc := csettings.NewServiceMock(t) | ||
ts := setupRouter(t, testInstance, svc) | ||
|
||
flagship := oauth.Client{ | ||
RedirectURIs: []string{"cozy://flagship"}, | ||
ClientName: "flagship-app", | ||
ClientKind: "mobile", | ||
SoftwareID: "github.com/cozy/cozy-stack/testing/flagship", | ||
Flagship: true, | ||
} | ||
require.Nil(t, flagship.Create(testInstance, oauth.NotPending)) | ||
|
||
t.Run("WithoutLimit", func(t *testing.T) { | ||
setClientsLimit(t, testInstance, -1) | ||
|
||
e := testutils.CreateTestClient(t, ts.URL) | ||
obj := e.GET("/settings/clients-usage"). | ||
WithHeader("Authorization", "Bearer "+token). | ||
Expect().Status(200). | ||
JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). | ||
Object() | ||
|
||
data := obj.Value("data").Object() | ||
data.ValueEqual("type", "io.cozy.settings") | ||
data.ValueEqual("id", "io.cozy.settings.clients-usage") | ||
|
||
attrs := data.Value("attributes").Object() | ||
attrs.NotContainsKey("limit") | ||
attrs.ValueEqual("count", 1) | ||
attrs.ValueEqual("limitReached", false) | ||
attrs.ValueEqual("limitExceeded", false) | ||
}) | ||
|
||
t.Run("WithLimitNotReached", func(t *testing.T) { | ||
setClientsLimit(t, testInstance, 2) | ||
|
||
e := testutils.CreateTestClient(t, ts.URL) | ||
obj := e.GET("/settings/clients-usage"). | ||
WithHeader("Authorization", "Bearer "+token). | ||
Expect().Status(200). | ||
JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). | ||
Object() | ||
|
||
data := obj.Value("data").Object() | ||
data.ValueEqual("type", "io.cozy.settings") | ||
data.ValueEqual("id", "io.cozy.settings.clients-usage") | ||
|
||
attrs := data.Value("attributes").Object() | ||
attrs.ValueEqual("limit", 2) | ||
attrs.ValueEqual("count", 1) | ||
attrs.ValueEqual("limitReached", false) | ||
attrs.ValueEqual("limitExceeded", false) | ||
}) | ||
|
||
t.Run("WithLimitReached", func(t *testing.T) { | ||
setClientsLimit(t, testInstance, 1) | ||
|
||
e := testutils.CreateTestClient(t, ts.URL) | ||
obj := e.GET("/settings/clients-usage"). | ||
WithHeader("Authorization", "Bearer "+token). | ||
Expect().Status(200). | ||
JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). | ||
Object() | ||
|
||
data := obj.Value("data").Object() | ||
data.ValueEqual("type", "io.cozy.settings") | ||
data.ValueEqual("id", "io.cozy.settings.clients-usage") | ||
|
||
attrs := data.Value("attributes").Object() | ||
attrs.ValueEqual("limit", 1) | ||
attrs.ValueEqual("count", 1) | ||
attrs.ValueEqual("limitReached", true) | ||
attrs.ValueEqual("limitExceeded", false) | ||
}) | ||
|
||
t.Run("WithLimitExceeded", func(t *testing.T) { | ||
setClientsLimit(t, testInstance, 0) | ||
|
||
e := testutils.CreateTestClient(t, ts.URL) | ||
obj := e.GET("/settings/clients-usage"). | ||
WithHeader("Authorization", "Bearer "+token). | ||
Expect().Status(200). | ||
JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). | ||
Object() | ||
|
||
data := obj.Value("data").Object() | ||
data.ValueEqual("type", "io.cozy.settings") | ||
data.ValueEqual("id", "io.cozy.settings.clients-usage") | ||
|
||
attrs := data.Value("attributes").Object() | ||
attrs.ValueEqual("limit", 0) | ||
attrs.ValueEqual("count", 1) | ||
attrs.ValueEqual("limitReached", true) | ||
attrs.ValueEqual("limitExceeded", true) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters