From 5e89ba1bacbb6740141f0543b3ec38f133587123 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Tue, 12 Sep 2023 15:16:16 +0200 Subject: [PATCH] Fix user token API endpoints --- docs/content/development/api-usage.en-us.md | 10 +- modules/context/api.go | 6 + routers/api/v1/api.go | 12 +- routers/api/v1/user/app.go | 116 ++++++++++++++--- templates/swagger/v1_json.tmpl | 131 +++++++++++++++++++- tests/integration/api_token_test.go | 6 +- 6 files changed, 250 insertions(+), 31 deletions(-) diff --git a/docs/content/development/api-usage.en-us.md b/docs/content/development/api-usage.en-us.md index 465f4d380c8e4..2fb878bd5409c 100644 --- a/docs/content/development/api-usage.en-us.md +++ b/docs/content/development/api-usage.en-us.md @@ -41,13 +41,13 @@ Gitea parses queries and headers to find the token in ## Generating and listing API tokens A new token can be generated with a `POST` request to -`/users/:name/tokens`. +`/user/tokens`. -Note that `/users/:name/tokens` is a special endpoint and requires you +Note that `/user/tokens` is a special endpoint and requires you to authenticate using `BasicAuth` and a password, as follows: ```sh -$ curl -H "Content-Type: application/json" -d '{"name":"test"}' -u username:password https://gitea.your.host/api/v1/users//tokens +$ curl -H "Content-Type: application/json" -d '{"name":"test"}' -u username:password https://gitea.your.host/api/v1/user/tokens {"id":1,"name":"test","sha1":"9fcb1158165773dd010fca5f0cf7174316c3e37d","token_last_eight":"16c3e37d"} ``` @@ -56,7 +56,7 @@ plain-text. It will not be displayed when listing tokens with a `GET` request; e.g. ```sh -$ curl --url https://yourusername:password@gitea.your.host/api/v1/users//tokens +$ curl --url https://yourusername:password@gitea.your.host/api/v1/user/tokens [{"name":"test","sha1":"","token_last_eight:"........":},{"name":"dev","sha1":"","token_last_eight":"........"}] ``` @@ -68,7 +68,7 @@ is where you'd place the code from your authenticator. Here is how the request would look like in curl: ```sh -$ curl -H "X-Gitea-OTP: 123456" --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens +$ curl -H "X-Gitea-OTP: 123456" --url https://yourusername:yourpassword@gitea.your.host/api/v1/user/tokens ``` You can also create an API key token via your Gitea installation's web diff --git a/modules/context/api.go b/modules/context/api.go index 58532b883dde0..60406fec714ab 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -79,6 +79,12 @@ type APIInvalidTopicsError struct { // swagger:response empty type APIEmpty struct{} +// APIUnauthorizedError is a unauthorized error response +// swagger:response unauthorized +type APIUnauthorizedError struct { + APIError +} + // APIForbiddenError is a forbidden error response // swagger:response forbidden type APIForbiddenError struct { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index fd7d3687acbc4..2dc7c82d811e9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -904,9 +904,9 @@ func Routes() *web.Route { m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos) m.Group("/tokens", func() { - m.Combo("").Get(user.ListAccessTokens). - Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) - m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken) + m.Combo("").Get(user.ListAccessTokensOld). + Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessTokenOld) + m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessTokenOld) }, reqBasicOrRevProxyAuth()) m.Get("/activities/feeds", user.ListUserActivityFeeds) @@ -968,6 +968,12 @@ func Routes() *web.Route { Delete(user.DeletePublicKey) }) + m.Group("/tokens", func() { + m.Combo("").Get(reqToken(), user.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) + m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken) + }, reqBasicOrRevProxyAuth()) + // (admin:application scope) m.Group("/applications", func() { m.Combo("/oauth2"). diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index f89d53945fa0b..7dcae07720a8a 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -21,17 +21,12 @@ import ( // ListAccessTokens list all the access tokens func ListAccessTokens(ctx *context.APIContext) { - // swagger:operation GET /users/{username}/tokens user userGetTokens + // swagger:operation GET /user/tokens user userGetTokens // --- // summary: List the authenticated user's access tokens // produces: // - application/json // parameters: - // - name: username - // in: path - // description: username of user - // type: string - // required: true // - name: page // in: query // description: page number of results to return (1-based) @@ -43,6 +38,8 @@ func ListAccessTokens(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/AccessTokenList" + // "401": + // "$ref": "#/responses/unauthorized" opts := auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID, ListOptions: utils.GetListOptions(ctx)} @@ -71,9 +68,39 @@ func ListAccessTokens(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &apiTokens) } +// ListAccessTokensOld is a compatibility layer for ListAccessTokens +func ListAccessTokensOld(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/tokens user userGetTokensOld + // --- + // summary: List the authenticated user's access tokens + // deprecated: true + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/AccessTokenList" + // "401": + // "$ref": "#/responses/unauthorized" + ListAccessTokens(ctx) +} + // CreateAccessToken create access tokens func CreateAccessToken(ctx *context.APIContext) { - // swagger:operation POST /users/{username}/tokens user userCreateToken + // swagger:operation POST /user/tokens user userCreateToken // --- // summary: Create an access token // consumes: @@ -81,11 +108,6 @@ func CreateAccessToken(ctx *context.APIContext) { // produces: // - application/json // parameters: - // - name: username - // in: path - // description: username of user - // required: true - // type: string // - name: body // in: body // schema: @@ -95,6 +117,8 @@ func CreateAccessToken(ctx *context.APIContext) { // "$ref": "#/responses/AccessToken" // "400": // "$ref": "#/responses/error" + // "401": + // "$ref": "#/responses/unauthorized" form := web.GetForm(ctx).(*api.CreateAccessTokenOption) @@ -132,19 +156,44 @@ func CreateAccessToken(ctx *context.APIContext) { }) } -// DeleteAccessToken delete access tokens -func DeleteAccessToken(ctx *context.APIContext) { - // swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessToken +// CreateAccessTokenOld is a compatibility layer for CreateAccessToken +func CreateAccessTokenOld(ctx *context.APIContext) { + // swagger:operation POST /users/{username}/tokens user userCreateTokenOld // --- - // summary: delete an access token + // summary: Create an access token + // deprecated: true + // consumes: + // - application/json // produces: // - application/json // parameters: // - name: username // in: path // description: username of user - // type: string // required: true + // type: string + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateAccessTokenOption" + // responses: + // "201": + // "$ref": "#/responses/AccessToken" + // "400": + // "$ref": "#/responses/error" + // "401": + // "$ref": "#/responses/unauthorized" + CreateAccessToken(ctx) +} + +// DeleteAccessToken delete access tokens +func DeleteAccessToken(ctx *context.APIContext) { + // swagger:operation DELETE /user/tokens/{token} user userDeleteAccessToken + // --- + // summary: delete an access token + // produces: + // - application/json + // parameters: // - name: token // in: path // description: token to be deleted, identified by ID and if not available by name @@ -153,6 +202,8 @@ func DeleteAccessToken(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "401": + // "$ref": "#/responses/unauthorized" // "404": // "$ref": "#/responses/notFound" // "422": @@ -199,6 +250,37 @@ func DeleteAccessToken(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// DeleteAccessTokenOld is a compatibility layer for DeleteAccessToken +func DeleteAccessTokenOld(ctx *context.APIContext) { + // swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessTokenOld + // --- + // summary: delete an access token + // deprecated: true + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: token + // in: path + // description: token to be deleted, identified by ID and if not available by name + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "401": + // "$ref": "#/responses/unauthorized" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + DeleteAccessToken(ctx) +} + // CreateOauth2Application is the handler to create a new OAuth2 Application for the authenticated user func CreateOauth2Application(ctx *context.APIContext) { // swagger:operation POST /user/applications/oauth2 user userCreateOAuth2Application diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 03beca3f73de5..67af7e10dbffc 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15323,6 +15323,108 @@ } } }, + "/user/tokens": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "List the authenticated user's access tokens", + "operationId": "userGetTokens", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/AccessTokenList" + }, + "401": { + "$ref": "#/responses/unauthorized" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create an access token", + "operationId": "userCreateToken", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateAccessTokenOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/AccessToken" + }, + "400": { + "$ref": "#/responses/error" + }, + "401": { + "$ref": "#/responses/unauthorized" + } + } + } + }, + "/user/tokens/{token}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "delete an access token", + "operationId": "userDeleteAccessToken", + "parameters": [ + { + "type": "string", + "description": "token to be deleted, identified by ID and if not available by name", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "401": { + "$ref": "#/responses/unauthorized" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/error" + } + } + } + }, "/users/search": { "get": { "produces": [ @@ -15884,7 +15986,8 @@ "user" ], "summary": "List the authenticated user's access tokens", - "operationId": "userGetTokens", + "operationId": "userGetTokensOld", + "deprecated": true, "parameters": [ { "type": "string", @@ -15909,6 +16012,9 @@ "responses": { "200": { "$ref": "#/responses/AccessTokenList" + }, + "401": { + "$ref": "#/responses/unauthorized" } } }, @@ -15923,7 +16029,8 @@ "user" ], "summary": "Create an access token", - "operationId": "userCreateToken", + "operationId": "userCreateTokenOld", + "deprecated": true, "parameters": [ { "type": "string", @@ -15946,6 +16053,9 @@ }, "400": { "$ref": "#/responses/error" + }, + "401": { + "$ref": "#/responses/unauthorized" } } } @@ -15959,7 +16069,8 @@ "user" ], "summary": "delete an access token", - "operationId": "userDeleteAccessToken", + "operationId": "userDeleteAccessTokenOld", + "deprecated": true, "parameters": [ { "type": "string", @@ -15980,6 +16091,9 @@ "204": { "$ref": "#/responses/empty" }, + "401": { + "$ref": "#/responses/unauthorized" + }, "404": { "$ref": "#/responses/notFound" }, @@ -23450,6 +23564,17 @@ "type": "string" } }, + "unauthorized": { + "description": "APIUnauthorizedError is a unauthorized error response", + "headers": { + "message": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "validationError": { "description": "APIValidationError is error format response related to input validation", "headers": { diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go index 1c63d07f22023..4307d47834b91 100644 --- a/tests/integration/api_token_test.go +++ b/tests/integration/api_token_test.go @@ -35,7 +35,7 @@ func TestAPIDeleteMissingToken(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", unittest.NonexistentID) + req := NewRequestf(t, "DELETE", "/api/v1/user/tokens/%d", unittest.NonexistentID) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) } @@ -503,7 +503,7 @@ func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *us } } log.Debug("Requesting creation of token with scopes: %v", scopes) - req := NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", payload) + req := NewRequestWithJSON(t, "POST", "/api/v1/user/tokens", payload) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusCreated) @@ -523,7 +523,7 @@ func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *us // createAPIAccessTokenWithoutCleanUp Delete an API access token and assert that // deletion succeeded. func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) { - req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", accessToken.ID) + req := NewRequestf(t, "DELETE", "/api/v1/user/tokens/%d", accessToken.ID) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNoContent)