diff --git a/.schema/api.swagger.json b/.schema/api.swagger.json index db3ddfa8578..0284ac0893c 100755 --- a/.schema/api.swagger.json +++ b/.schema/api.swagger.json @@ -74,7 +74,7 @@ }, "/identities": { "get": { - "description": "This endpoint returns a login request's context with, for example, error details and\nother information.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "description": "Lists all identities. Does not support search at the moment.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", "produces": [ "application/json" ], @@ -85,8 +85,29 @@ "tags": [ "admin" ], - "summary": "List all identities in the system", + "summary": "List Identities", "operationId": "listIdentities", + "parameters": [ + { + "maximum": 500, + "minimum": 1, + "type": "integer", + "format": "int64", + "default": 100, + "description": "Pagination Limit\n\nThis is the number of items per page.", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "format": "int64", + "default": 0, + "description": "Pagination Page", + "name": "page", + "in": "query" + } + ], "responses": { "200": { "description": "A list of identities.", @@ -120,7 +141,7 @@ "tags": [ "admin" ], - "summary": "Create an identity", + "summary": "Create an Identity", "operationId": "createIdentity", "parameters": [ { @@ -170,7 +191,7 @@ "tags": [ "admin" ], - "summary": "Get an identity", + "summary": "Get an Identity", "operationId": "getIdentity", "parameters": [ { @@ -217,7 +238,7 @@ "tags": [ "admin" ], - "summary": "Update an identity", + "summary": "Update an Identity", "operationId": "updateIdentity", "parameters": [ { @@ -264,7 +285,7 @@ } }, "delete": { - "description": "This endpoint deletes an identity. This can not be undone.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", + "description": "Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.\nThis endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is\nassumed that is has been deleted already.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).", "produces": [ "application/json" ], @@ -275,7 +296,7 @@ "tags": [ "admin" ], - "summary": "Delete an identity", + "summary": "Delete an Identity", "operationId": "deleteIdentity", "parameters": [ { @@ -290,12 +311,6 @@ "204": { "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201." }, - "404": { - "description": "genericError", - "schema": { - "$ref": "#/definitions/genericError" - } - }, "500": { "description": "genericError", "schema": { diff --git a/identity/handler.go b/identity/handler.go index 9a7787a6f9f..069aa5c33bb 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -11,8 +11,6 @@ import ( "github.com/ory/x/jsonx" "github.com/ory/x/urlx" - "github.com/ory/x/pagination" - "github.com/ory/kratos/x" ) @@ -71,12 +69,33 @@ type identitiesListResponse struct { Body []Identity } +// swagger:parameters listIdentities +type listIdentityParameters struct { + // Pagination Limit + // + // This is the number of items per page. + // + // required: false + // in: query + // default: 100 + // min: 1 + // max: 500 + Limit int `json:"limit"` + + // Pagination Page + // + // required: false + // in: query + // default: 0 + // min: 0 + Page int `json:"page"` +} + // swagger:route GET /identities admin listIdentities // -// List all identities in the system +// List Identities // -// This endpoint returns a login request's context with, for example, error details and -// other information. +// Lists all identities. Does not support search at the moment. // // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). // @@ -88,9 +107,9 @@ type identitiesListResponse struct { // Responses: // 200: identityList // 500: genericError -func (h *Handler) list(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - limit, offset := pagination.Parse(r, 100, 0, 500) - is, err := h.r.IdentityPool().ListIdentities(r.Context(), limit, offset) +func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + page, limit := x.ParsePagination(r) + is, err := h.r.IdentityPool().ListIdentities(r.Context(), page, limit) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -110,7 +129,7 @@ type getIdentityParameters struct { // swagger:route GET /identities/{id} admin getIdentity // -// Get an identity +// Get an Identity // // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). // @@ -160,7 +179,7 @@ type CreateIdentityRequestPayload struct { // swagger:route POST /identities admin createIdentity // -// Create an identity +// Create an Identity // // This endpoint creates an identity. It is NOT possible to set an identity's credentials (password, ...) // using this method! A way to achieve that will be introduced in the future. @@ -223,7 +242,7 @@ type UpdateIdentityRequestPayload struct { // swagger:route PUT /identities/{id} admin updateIdentity // -// Update an identity +// Update an Identity // // This endpoint updates an identity. It is NOT possible to set an identity's credentials (password, ...) // using this method! A way to achieve that will be introduced in the future. @@ -278,9 +297,11 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P // swagger:route DELETE /identities/{id} admin deleteIdentity // -// Delete an identity +// Delete an Identity // -// This endpoint deletes an identity. This can not be undone. +// Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone. +// This endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is +// assumed that is has been deleted already. // // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). // @@ -291,7 +312,6 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P // // Responses: // 204: emptyResponse -// 404: genericError // 500: genericError func (h *Handler) delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if err := h.r.IdentityPool().(PrivilegedPool).DeleteIdentity(r.Context(), x.ParseUUID(ps.ByName("id"))); err != nil { diff --git a/identity/pool.go b/identity/pool.go index d2845df9788..14c998c4468 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -30,7 +30,7 @@ import ( type ( Pool interface { - ListIdentities(ctx context.Context, limit, offset int) ([]Identity, error) + ListIdentities(ctx context.Context, page, limit int) ([]Identity, error) // GetIdentity returns an identity by its id. Will return an error if the identity does not exist or backend // connectivity is broken. @@ -351,7 +351,7 @@ func TestPool(p PrivilegedPool) func(t *testing.T) { }) t.Run("case=list", func(t *testing.T) { - is, err := p.ListIdentities(context.Background(), 25, 0) + is, err := p.ListIdentities(context.Background(), 0, 25) require.NoError(t, err) assert.Len(t, is, len(createdIDs)) for _, id := range createdIDs { diff --git a/internal/httpclient/client/admin/admin_client.go b/internal/httpclient/client/admin/admin_client.go index 1836cb6fefe..640abba6f3a 100644 --- a/internal/httpclient/client/admin/admin_client.go +++ b/internal/httpclient/client/admin/admin_client.go @@ -82,7 +82,9 @@ func (a *Client) CreateIdentity(params *CreateIdentityParams) (*CreateIdentityCr /* DeleteIdentity deletes an identity - This endpoint deletes an identity. This can not be undone. + Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone. +This endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is +assumed that is has been deleted already. Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). */ @@ -154,10 +156,9 @@ func (a *Client) GetIdentity(params *GetIdentityParams) (*GetIdentityOK, error) } /* - ListIdentities lists all identities in the system + ListIdentities lists identities - This endpoint returns a login request's context with, for example, error details and -other information. + Lists all identities. Does not support search at the moment. Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). */ diff --git a/internal/httpclient/client/admin/delete_identity_responses.go b/internal/httpclient/client/admin/delete_identity_responses.go index adc17a709f7..e427870e714 100644 --- a/internal/httpclient/client/admin/delete_identity_responses.go +++ b/internal/httpclient/client/admin/delete_identity_responses.go @@ -29,12 +29,6 @@ func (o *DeleteIdentityReader) ReadResponse(response runtime.ClientResponse, con return nil, err } return result, nil - case 404: - result := NewDeleteIdentityNotFound() - if err := result.readResponse(response, consumer, o.formats); err != nil { - return nil, err - } - return nil, result case 500: result := NewDeleteIdentityInternalServerError() if err := result.readResponse(response, consumer, o.formats); err != nil { @@ -69,39 +63,6 @@ func (o *DeleteIdentityNoContent) readResponse(response runtime.ClientResponse, return nil } -// NewDeleteIdentityNotFound creates a DeleteIdentityNotFound with default headers values -func NewDeleteIdentityNotFound() *DeleteIdentityNotFound { - return &DeleteIdentityNotFound{} -} - -/*DeleteIdentityNotFound handles this case with default header values. - -genericError -*/ -type DeleteIdentityNotFound struct { - Payload *models.GenericError -} - -func (o *DeleteIdentityNotFound) Error() string { - return fmt.Sprintf("[DELETE /identities/{id}][%d] deleteIdentityNotFound %+v", 404, o.Payload) -} - -func (o *DeleteIdentityNotFound) GetPayload() *models.GenericError { - return o.Payload -} - -func (o *DeleteIdentityNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { - - o.Payload = new(models.GenericError) - - // response payload - if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { - return err - } - - return nil -} - // NewDeleteIdentityInternalServerError creates a DeleteIdentityInternalServerError with default headers values func NewDeleteIdentityInternalServerError() *DeleteIdentityInternalServerError { return &DeleteIdentityInternalServerError{} diff --git a/internal/httpclient/client/admin/list_identities_parameters.go b/internal/httpclient/client/admin/list_identities_parameters.go index b88e9923073..6fe6de23a59 100644 --- a/internal/httpclient/client/admin/list_identities_parameters.go +++ b/internal/httpclient/client/admin/list_identities_parameters.go @@ -14,13 +14,19 @@ import ( "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" ) // NewListIdentitiesParams creates a new ListIdentitiesParams object // with the default values initialized. func NewListIdentitiesParams() *ListIdentitiesParams { - + var ( + limitDefault = int64(100) + pageDefault = int64(0) + ) return &ListIdentitiesParams{ + Limit: &limitDefault, + Page: &pageDefault, timeout: cr.DefaultTimeout, } @@ -29,8 +35,13 @@ func NewListIdentitiesParams() *ListIdentitiesParams { // NewListIdentitiesParamsWithTimeout creates a new ListIdentitiesParams object // with the default values initialized, and the ability to set a timeout on a request func NewListIdentitiesParamsWithTimeout(timeout time.Duration) *ListIdentitiesParams { - + var ( + limitDefault = int64(100) + pageDefault = int64(0) + ) return &ListIdentitiesParams{ + Limit: &limitDefault, + Page: &pageDefault, timeout: timeout, } @@ -39,8 +50,13 @@ func NewListIdentitiesParamsWithTimeout(timeout time.Duration) *ListIdentitiesPa // NewListIdentitiesParamsWithContext creates a new ListIdentitiesParams object // with the default values initialized, and the ability to set a context for a request func NewListIdentitiesParamsWithContext(ctx context.Context) *ListIdentitiesParams { - + var ( + limitDefault = int64(100) + pageDefault = int64(0) + ) return &ListIdentitiesParams{ + Limit: &limitDefault, + Page: &pageDefault, Context: ctx, } @@ -49,8 +65,13 @@ func NewListIdentitiesParamsWithContext(ctx context.Context) *ListIdentitiesPara // NewListIdentitiesParamsWithHTTPClient creates a new ListIdentitiesParams object // with the default values initialized, and the ability to set a custom HTTPClient for a request func NewListIdentitiesParamsWithHTTPClient(client *http.Client) *ListIdentitiesParams { - + var ( + limitDefault = int64(100) + pageDefault = int64(0) + ) return &ListIdentitiesParams{ + Limit: &limitDefault, + Page: &pageDefault, HTTPClient: client, } } @@ -59,6 +80,20 @@ func NewListIdentitiesParamsWithHTTPClient(client *http.Client) *ListIdentitiesP for the list identities operation typically these are written to a http.Request */ type ListIdentitiesParams struct { + + /*Limit + Pagination Limit + + This is the number of items per page. + + */ + Limit *int64 + /*Page + Pagination Page + + */ + Page *int64 + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -97,6 +132,28 @@ func (o *ListIdentitiesParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithLimit adds the limit to the list identities params +func (o *ListIdentitiesParams) WithLimit(limit *int64) *ListIdentitiesParams { + o.SetLimit(limit) + return o +} + +// SetLimit adds the limit to the list identities params +func (o *ListIdentitiesParams) SetLimit(limit *int64) { + o.Limit = limit +} + +// WithPage adds the page to the list identities params +func (o *ListIdentitiesParams) WithPage(page *int64) *ListIdentitiesParams { + o.SetPage(page) + return o +} + +// SetPage adds the page to the list identities params +func (o *ListIdentitiesParams) SetPage(page *int64) { + o.Page = page +} + // WriteToRequest writes these params to a swagger request func (o *ListIdentitiesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -105,6 +162,38 @@ func (o *ListIdentitiesParams) WriteToRequest(r runtime.ClientRequest, reg strfm } var res []error + if o.Limit != nil { + + // query param limit + var qrLimit int64 + if o.Limit != nil { + qrLimit = *o.Limit + } + qLimit := swag.FormatInt64(qrLimit) + if qLimit != "" { + if err := r.SetQueryParam("limit", qLimit); err != nil { + return err + } + } + + } + + if o.Page != nil { + + // query param page + var qrPage int64 + if o.Page != nil { + qrPage = *o.Page + } + qPage := swag.FormatInt64(qrPage) + if qPage != "" { + if err := r.SetQueryParam("page", qPage); err != nil { + return err + } + } + + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 26ae8a91758..4e3468713ec 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -114,7 +114,7 @@ func TestMigrations(t *testing.T) { t.Run("suite=fixtures", func(t *testing.T) { t.Run("case=identity", func(t *testing.T) { - ids, err := d.Registry().PrivilegedIdentityPool().ListIdentities(context.Background(), 100, 0) + ids, err := d.Registry().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) require.NoError(t, err) for _, id := range ids { diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go index 629639ff344..1239ae98df5 100644 --- a/persistence/sql/persister_identity.go +++ b/persistence/sql/persister_identity.go @@ -191,16 +191,16 @@ func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) er }) } -func (p *Persister) ListIdentities(ctx context.Context, limit, offset int) ([]identity.Identity, error) { +func (p *Persister) ListIdentities(ctx context.Context, page, limit int) ([]identity.Identity, error) { is := make([]identity.Identity, 0) /* #nosec G201 TableName is static */ if err := sqlcon.HandleError(p.GetConnection(ctx). - RawQuery(fmt.Sprintf("SELECT * FROM %s LIMIT ? OFFSET ?", new(identity.Identity).TableName()), limit, offset). - Eager("VerifiableAddresses", "RecoveryAddresses").All(&is)); err != nil { + Paginate(page, limit).Order("id DESC"). + Eager("VerifiableAddresses", "RecoveryAddresses"). + All(&is)); err != nil { return nil, err } - for i := range is { if err := p.injectTraitsSchemaURL(&(is[i])); err != nil { return nil, err diff --git a/x/pagination.go b/x/pagination.go new file mode 100644 index 00000000000..bdf555df87f --- /dev/null +++ b/x/pagination.go @@ -0,0 +1,45 @@ +package x + +import ( + "net/http" + "strconv" +) + +const paginationMaxItems = 500 + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func ParsePagination(r *http.Request) (page, limit int) { + if offsetParam := r.URL.Query().Get("page"); offsetParam == "" { + page = 0 + } else { + if offset64, err := strconv.ParseInt(offsetParam, 10, 64); err != nil { + page = 0 + } else { + page = int(offset64) + } + } + + if limitParam := r.URL.Query().Get("limit"); limitParam == "" { + limit = paginationMaxItems + } else { + if limit64, err := strconv.ParseInt(limitParam, 10, 64); err != nil { + limit = paginationMaxItems + } else { + limit = int(limit64) + } + } + + if limit > paginationMaxItems { + limit = paginationMaxItems + } + + if limit < 0 { + limit = 0 + } + + if page < 0 { + page = 0 + } + + return +}