diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 4f1e9a3f5373a..fce3b0aef352e 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "code.gitea.io/gitea/models" @@ -530,3 +531,183 @@ func RenameUser(ctx *context.APIContext) { log.Trace("User name changed: %s -> %s", oldName, newName) ctx.Status(http.StatusOK) } + +// ListAccessTokens list all the access tokens +func ListAccessTokens(ctx *context.APIContext) { + // swagger:operation GET /admin/users/{username}/tokens admin listAccessTokens + // --- + // summary: List the user's access tokens of {username} by admin + // 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" + + opts := auth.ListAccessTokensOptions{UserID: ctx.ContextUser.ID, ListOptions: utils.GetListOptions(ctx)} + + count, err := auth.CountAccessTokens(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + tokens, err := auth.ListAccessTokens(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiTokens := make([]*api.AccessToken, len(tokens)) + for i := range tokens { + apiTokens[i] = &api.AccessToken{ + ID: tokens[i].ID, + Name: tokens[i].Name, + TokenLastEight: tokens[i].TokenLastEight, + Scopes: tokens[i].Scope.StringSlice(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, &apiTokens) +} + +// CreateAccessToken create access tokens +func CreateAccessToken(ctx *context.APIContext) { + // swagger:operation POST /admin/users/{username}/tokens admin adminCreateAccessToken + // --- + // summary: Create an access token for {username} by admin + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateAccessTokenOption" + // responses: + // "201": + // "$ref": "#/responses/AccessToken" + // "400": + // "$ref": "#/responses/error" + + form := web.GetForm(ctx).(*api.CreateAccessTokenOption) + + t := &auth.AccessToken{ + UID: ctx.ContextUser.ID, + Name: form.Name, + } + + exist, err := auth.AccessTokenByNameExists(t) + if err != nil { + ctx.InternalServerError(err) + return + } + if exist { + ctx.Error(http.StatusBadRequest, "AccessTokenByNameExists", errors.New("access token name has been used already")) + return + } + + scope, err := auth.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() + if err != nil { + ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err)) + return + } + t.Scope = scope + + if err := auth.NewAccessToken(t); err != nil { + ctx.Error(http.StatusInternalServerError, "NewAccessToken", err) + return + } + ctx.JSON(http.StatusCreated, &api.AccessToken{ + Name: t.Name, + Token: t.Token, + ID: t.ID, + TokenLastEight: t.TokenLastEight, + }) +} + +// DeleteAccessToken delete access tokens +func DeleteAccessToken(ctx *context.APIContext) { + // swagger:operation DELETE /admin/users/{username}/tokens/{token} admin adminDeleteAccessToken + // --- + // summary: delete an access token of {username} by admin + // 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" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + + token := ctx.Params(":id") + tokenID, _ := strconv.ParseInt(token, 0, 64) + + if tokenID == 0 { + tokens, err := auth.ListAccessTokens(auth.ListAccessTokensOptions{ + Name: token, + UserID: ctx.ContextUser.ID, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListAccessTokens", err) + return + } + + switch len(tokens) { + case 0: + ctx.NotFound() + return + case 1: + tokenID = tokens[0].ID + default: + ctx.Error(http.StatusUnprocessableEntity, "DeleteAccessTokenByID", fmt.Errorf("multiple matches for token name '%s'", token)) + return + } + } + if tokenID == 0 { + ctx.Error(http.StatusInternalServerError, "Invalid TokenID", nil) + return + } + + if err := auth.DeleteAccessTokenByID(tokenID, ctx.Doer.ID); err != nil { + if auth.IsErrAccessTokenNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeleteAccessTokenByID", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index fd7d3687acbc4..2ea5506c531ea 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -931,6 +931,15 @@ func Routes() *web.Route { }, context_service.UserAssignmentAPI()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) + // User (requires user scope) + m.Group("user", func() { + m.Group("/tokens", func() { + m.Combo("").Get(user.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) + m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken) + }, reqBasicOrRevProxyAuth()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) + // Users (requires user scope) m.Group("/user", func() { m.Get("", user.GetAuthenticatedUser) @@ -1475,6 +1484,18 @@ func Routes() *web.Route { m.Get("/activities/feeds", org.ListTeamActivityFeeds) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership()) + m.Group("/admin", func() { + m.Group("/users", func() { + m.Group("/{username}", func() { + m.Group("/tokens", func() { + m.Combo("").Get(admin.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), reqToken(), admin.CreateAccessToken) + m.Combo("/{id}").Delete(reqToken(), admin.DeleteAccessToken) + }) + }, context_service.UserAssignmentAPI()) + }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqBasicOrRevProxyAuth(), reqSiteAdmin()) + m.Group("/admin", func() { m.Group("/cron", func() { m.Get("", admin.ListCronTasks) diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index f89d53945fa0b..3428d04a121e3 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) @@ -73,7 +68,7 @@ func ListAccessTokens(ctx *context.APIContext) { // 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 +76,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: @@ -134,17 +124,12 @@ func CreateAccessToken(ctx *context.APIContext) { // DeleteAccessToken delete access tokens func DeleteAccessToken(ctx *context.APIContext) { - // swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessToken + // swagger:operation DELETE /user/tokens/{token} user userDeleteAccessToken // --- // summary: delete an access token // 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 @@ -199,6 +184,109 @@ func DeleteAccessToken(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// ListAccessTokens list all the access tokens +func ListAccessTokensDeprecated(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/tokens user userGetTokensDeprecated + // --- + // 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) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/AccessTokenList" + // "403": + // "$ref": "#/responses/error" + // Deprecated: true + if ctx.Doer != ctx.ContextUser { + ctx.Error(http.StatusForbidden, "ListAccessTokens", errors.New("can only list access tokens for yourself")) + return + } + ListAccessTokens(ctx) +} + +// CreateAccessTokenDeprecated create access tokens +func CreateAccessTokenDeprecated(ctx *context.APIContext) { + // swagger:operation POST /users/{username}/tokens user CreateAccessTokenDeprecated + // --- + // summary: Create an access token + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // required: true + // type: string + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateAccessTokenOption" + // responses: + // "201": + // "$ref": "#/responses/AccessToken" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/error" + // Deprecated: true + if ctx.Doer != ctx.ContextUser { + ctx.Error(http.StatusForbidden, "", errors.New("Can't create token for another user")) + return + } + CreateAccessToken(ctx) +} + +// DeleteAccessToken delete access tokens +func DeleteAccessTokenDeprecated(ctx *context.APIContext) { + // swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessTokenDeprecated + // --- + // summary: delete an access token + // 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" + // "403": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + // Deprecated: true + if ctx.Doer != ctx.ContextUser { + ctx.Error(http.StatusForbidden, "", "You can only delete your own tokens.") + return + } + 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 88dc9ea1ce551..956a9538dd4a0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -886,6 +886,120 @@ } } }, + "/admin/users/{username}/tokens": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List the user's access tokens of {username} by admin", + "operationId": "listAccessTokens", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "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" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create an access token for {username} by admin", + "operationId": "adminCreateAccessToken", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateAccessTokenOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/AccessToken" + }, + "400": { + "$ref": "#/responses/error" + } + } + } + }, + "/admin/users/{username}/tokens/{token}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "delete an access token of {username} by admin", + "operationId": "adminDeleteAccessToken", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "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" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/error" + } + } + } + }, "/gitignore/templates": { "get": { "produces": [ @@ -15749,6 +15863,99 @@ } } }, + "/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" + } + } + }, + "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" + } + } + } + }, + "/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" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/error" + } + } + } + }, "/users/search": { "get": { "produces": [ @@ -16334,7 +16541,8 @@ "user" ], "summary": "List the authenticated user's access tokens", - "operationId": "userGetTokens", + "operationId": "userGetTokensDeprecated", + "deprecated": true, "parameters": [ { "type": "string", @@ -16359,6 +16567,9 @@ "responses": { "200": { "$ref": "#/responses/AccessTokenList" + }, + "403": { + "$ref": "#/responses/error" } } }, @@ -16373,7 +16584,8 @@ "user" ], "summary": "Create an access token", - "operationId": "userCreateToken", + "operationId": "CreateAccessTokenDeprecated", + "deprecated": true, "parameters": [ { "type": "string", @@ -16396,6 +16608,9 @@ }, "400": { "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/error" } } } @@ -16409,7 +16624,8 @@ "user" ], "summary": "delete an access token", - "operationId": "userDeleteAccessToken", + "operationId": "userDeleteAccessTokenDeprecated", + "deprecated": true, "parameters": [ { "type": "string", @@ -16430,6 +16646,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/error" + }, "404": { "$ref": "#/responses/notFound" },