mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-02 09:17:10 +02:00
Merge branch 'main' into improve-auth-failure-logging
This commit is contained in:
@@ -164,7 +164,12 @@ Mergers are the maintainers who carry out the final merge of approved PRs. Their
|
||||
|
||||
#### Becoming a merger
|
||||
|
||||
A merger should already be a Gitea maintainer. To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel. Mergers teams may also invite contributors.
|
||||
A merger must already be a Gitea maintainer.
|
||||
To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel.
|
||||
The minimum requirement for applications to become a merger is to have participated actively in the community for at least four months before applying.
|
||||
Ultimately, regardless of previous participation, you can only become a merger if the TOC votes in your favor.
|
||||
|
||||
You may also be invited by the TOC to become a merger.
|
||||
|
||||
### Technical Oversight Committee (TOC)
|
||||
|
||||
|
||||
@@ -20,42 +20,6 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error.
|
||||
type ErrAccessTokenNotExist struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
// IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist.
|
||||
func IsErrAccessTokenNotExist(err error) bool {
|
||||
_, ok := err.(ErrAccessTokenNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrAccessTokenNotExist) Error() string {
|
||||
return fmt.Sprintf("access token does not exist [sha: %s]", err.Token)
|
||||
}
|
||||
|
||||
func (err ErrAccessTokenNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error.
|
||||
type ErrAccessTokenEmpty struct{}
|
||||
|
||||
// IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty.
|
||||
func IsErrAccessTokenEmpty(err error) bool {
|
||||
_, ok := err.(ErrAccessTokenEmpty)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrAccessTokenEmpty) Error() string {
|
||||
return "access token is empty"
|
||||
}
|
||||
|
||||
func (err ErrAccessTokenEmpty) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
var successfulAccessTokenCache *lru.Cache[string, any]
|
||||
|
||||
// AccessToken represents a personal access token.
|
||||
@@ -134,21 +98,11 @@ func getAccessTokenIDFromCache(token string) int64 {
|
||||
|
||||
// GetAccessTokenBySHA returns access token by given token value
|
||||
func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) {
|
||||
if token == "" {
|
||||
return nil, ErrAccessTokenEmpty{}
|
||||
}
|
||||
// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
|
||||
if len(token) != 40 {
|
||||
return nil, ErrAccessTokenNotExist{token}
|
||||
}
|
||||
for _, x := range []byte(token) {
|
||||
if x < '0' || (x > '9' && x < 'a') || x > 'f' {
|
||||
return nil, ErrAccessTokenNotExist{token}
|
||||
}
|
||||
if len(token) < 8 {
|
||||
return nil, util.NewNotExistErrorf("access token not found")
|
||||
}
|
||||
|
||||
lastEight := token[len(token)-8:]
|
||||
|
||||
if id := getAccessTokenIDFromCache(token); id > 0 {
|
||||
accessToken := &AccessToken{
|
||||
TokenLastEight: lastEight,
|
||||
@@ -169,7 +123,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(tokens) == 0 {
|
||||
return nil, ErrAccessTokenNotExist{token}
|
||||
return nil, util.NewNotExistErrorf("access token not found")
|
||||
}
|
||||
|
||||
for _, t := range tokens {
|
||||
@@ -181,7 +135,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrAccessTokenNotExist{token}
|
||||
return nil, util.NewNotExistErrorf("access token not found")
|
||||
}
|
||||
|
||||
// AccessTokenByNameExists checks if a token name has been used already by a user.
|
||||
@@ -218,13 +172,11 @@ func UpdateAccessToken(ctx context.Context, t *AccessToken) error {
|
||||
|
||||
// DeleteAccessTokenByID deletes access token by given ID.
|
||||
func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
|
||||
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{
|
||||
UID: userID,
|
||||
})
|
||||
cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{UID: userID})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if cnt != 1 {
|
||||
return ErrAccessTokenNotExist{}
|
||||
return util.NewNotExistErrorf("access token not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -76,11 +77,11 @@ func TestGetAccessTokenBySHA(t *testing.T) {
|
||||
|
||||
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
|
||||
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrAccessTokenEmpty(err))
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestListAccessTokens(t *testing.T) {
|
||||
@@ -128,5 +129,5 @@ func TestDeleteAccessTokenByID(t *testing.T) {
|
||||
|
||||
err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
@@ -63,20 +63,24 @@ func (s *Sitemap) Add(u URL) {
|
||||
// WriteTo writes the sitemap to a response
|
||||
func (s *Sitemap) WriteTo(w io.Writer) (int64, error) {
|
||||
if l := len(s.URLs); l > urlsLimit {
|
||||
return 0, fmt.Errorf("The sitemap contains %d URLs, but only %d are allowed", l, urlsLimit)
|
||||
return 0, fmt.Errorf("sitemap contains %d URLs, but only %d are allowed", l, urlsLimit)
|
||||
}
|
||||
if l := len(s.Sitemaps); l > urlsLimit {
|
||||
return 0, fmt.Errorf("The sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit)
|
||||
return 0, fmt.Errorf("sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit)
|
||||
}
|
||||
buf := bytes.NewBufferString(xml.Header)
|
||||
if err := xml.NewEncoder(buf).Encode(s); err != nil {
|
||||
encoder := xml.NewEncoder(buf)
|
||||
defer encoder.Close()
|
||||
if err := encoder.Encode(s); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_ = encoder.Flush()
|
||||
if err := buf.WriteByte('\n'); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// FIXME: such limit is not right, the content has been written, it would have already caused OOM
|
||||
if buf.Len() > sitemapFileLimit {
|
||||
return 0, fmt.Errorf("The sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit)
|
||||
return 0, fmt.Errorf("sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit)
|
||||
}
|
||||
return buf.WriteTo(w)
|
||||
}
|
||||
|
||||
@@ -61,14 +61,14 @@ func TestNewSitemap(t *testing.T) {
|
||||
{
|
||||
name: "too many urls",
|
||||
urls: make([]URL, 50001),
|
||||
wantErr: "The sitemap contains 50001 URLs, but only 50000 are allowed",
|
||||
wantErr: "sitemap contains 50001 URLs, but only 50000 are allowed",
|
||||
},
|
||||
{
|
||||
name: "too big file",
|
||||
urls: []URL{
|
||||
{URL: strings.Repeat("b", 50*1024*1024+1)},
|
||||
},
|
||||
wantErr: "The sitemap has 52428932 bytes, but only 52428800 are allowed",
|
||||
wantErr: "sitemap has 52428932 bytes, but only 52428800 are allowed",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -137,14 +137,14 @@ func TestNewSitemapIndex(t *testing.T) {
|
||||
{
|
||||
name: "too many sitemaps",
|
||||
urls: make([]URL, 50001),
|
||||
wantErr: "The sitemap contains 50001 sub-sitemaps, but only 50000 are allowed",
|
||||
wantErr: "sitemap contains 50001 sub-sitemaps, but only 50000 are allowed",
|
||||
},
|
||||
{
|
||||
name: "too big file",
|
||||
urls: []URL{
|
||||
{URL: strings.Repeat("b", 50*1024*1024+1)},
|
||||
},
|
||||
wantErr: "The sitemap has 52428952 bytes, but only 52428800 are allowed",
|
||||
wantErr: "sitemap has 52428952 bytes, but only 52428800 are allowed",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// CurrentAccessToken represents the metadata of the currently authenticated token.
|
||||
// swagger:model CurrentAccessToken
|
||||
type CurrentAccessToken struct {
|
||||
// The unique identifier of the access token
|
||||
ID int64 `json:"id"`
|
||||
// The name of the access token
|
||||
Name string `json:"name"`
|
||||
// The scopes granted to this access token
|
||||
Scopes []string `json:"scopes"`
|
||||
// The timestamp when the token was created
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// The timestamp when the token was last used
|
||||
LastUsedAt time.Time `json:"last_used_at"`
|
||||
// The owner of the access token
|
||||
User *UserMeta `json:"user"`
|
||||
}
|
||||
|
||||
// UserMeta represents minimal user information for the token owner.
|
||||
type UserMeta struct {
|
||||
// The unique identifier of the user
|
||||
ID int64 `json:"id"`
|
||||
// The username of the user
|
||||
Login string `json:"login"`
|
||||
}
|
||||
@@ -88,6 +88,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/packages"
|
||||
"gitea.dev/routers/api/v1/repo"
|
||||
"gitea.dev/routers/api/v1/settings"
|
||||
"gitea.dev/routers/api/v1/token"
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/actions"
|
||||
@@ -977,6 +978,11 @@ func Routes() *web.Router {
|
||||
})
|
||||
})
|
||||
|
||||
// Token introspection and deletion endpoint
|
||||
m.Combo("/token").
|
||||
Get(reqToken(), token.GetCurrentToken).
|
||||
Delete(reqToken(), token.DeleteCurrentToken)
|
||||
|
||||
// Notifications (requires 'notifications' scope)
|
||||
// The notifications API is not available for public-only tokens because a user's notifications mix
|
||||
// public and private repository events in the same mailbox.
|
||||
|
||||
@@ -20,3 +20,10 @@ type swaggerResponseAccessToken struct {
|
||||
// in:body
|
||||
Body api.AccessToken `json:"body"`
|
||||
}
|
||||
|
||||
// CurrentAccessToken represents the currently authenticated access token.
|
||||
// swagger:response CurrentAccessToken
|
||||
type swaggerResponseCurrentAccessToken struct {
|
||||
// in:body
|
||||
Body api.CurrentAccessToken `json:"body"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/httpauth"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetCurrentToken returns metadata about the currently authenticated token.
|
||||
func GetCurrentToken(ctx *context.APIContext) {
|
||||
// swagger:operation GET /token miscellaneous getCurrentToken
|
||||
// ---
|
||||
// summary: Get the currently authenticated token
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CurrentAccessToken"
|
||||
accessToken, err := getToken(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user info
|
||||
user, err := user_model.GetUserByID(ctx, accessToken.UID)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.CurrentAccessToken{
|
||||
ID: accessToken.ID,
|
||||
Name: accessToken.Name,
|
||||
Scopes: accessToken.Scope.StringSlice(),
|
||||
CreatedAt: accessToken.CreatedUnix.AsTime(),
|
||||
LastUsedAt: accessToken.UpdatedUnix.AsTime(),
|
||||
User: &api.UserMeta{
|
||||
ID: user.ID,
|
||||
Login: user.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCurrentToken deletes the currently authenticated token.
|
||||
func DeleteCurrentToken(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /token miscellaneous deleteCurrentToken
|
||||
// ---
|
||||
// summary: Delete the currently authenticated token
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "204":
|
||||
// description: token deleted
|
||||
accessToken, err := getToken(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the token
|
||||
err = auth_model.DeleteAccessTokenByID(ctx, accessToken.ID, accessToken.UID)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// getToken retrieves an access token from the API context's Authorization header and validates it against the database.
|
||||
// Returns nil if the token is invalid and handles the response
|
||||
func getToken(ctx *context.APIContext) (*auth_model.AccessToken, error) {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
|
||||
if !ok || parsed.BearerToken == nil {
|
||||
return nil, util.NewNotExistErrorf("invalid access token")
|
||||
}
|
||||
return auth_model.GetAccessTokenBySHA(ctx, parsed.BearerToken.Token)
|
||||
}
|
||||
@@ -191,17 +191,9 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if tokenID == 0 {
|
||||
ctx.APIErrorInternal(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil {
|
||||
if auth_model.IsErrAccessTokenNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
ctx.APIErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@ func Organizations(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.organizations")
|
||||
ctx.Data["PageIsAdminOrganizations"] = true
|
||||
|
||||
if ctx.FormString("sort") == "" {
|
||||
ctx.SetFormString("sort", UserSearchDefaultAdminSort)
|
||||
}
|
||||
|
||||
sortOrder := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
@@ -35,5 +32,6 @@ func Organizations(ctx *context.Context) {
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
},
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
}, tplOrgs)
|
||||
}
|
||||
|
||||
@@ -55,11 +55,7 @@ func Users(ctx *context.Context) {
|
||||
statusFilterMap[filterKey] = paramVal
|
||||
}
|
||||
|
||||
sortType := ctx.FormString("sort")
|
||||
if sortType == "" {
|
||||
sortType = UserSearchDefaultAdminSort
|
||||
ctx.SetFormString("sort", sortType)
|
||||
}
|
||||
sortType := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
ctx.PageData["adminUserListSearchForm"] = map[string]any{
|
||||
"StatusFilterMap": statusFilterMap,
|
||||
"SortType": sortType,
|
||||
@@ -78,6 +74,7 @@ func Users(ctx *context.Context) {
|
||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
|
||||
OrderBy: db.SearchOrderBy(sortType),
|
||||
}, tplUsers)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,17 +38,14 @@ func Organizations(ctx *context.Context) {
|
||||
"alphabetically",
|
||||
"reversealphabetically",
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder := ctx.FormString("sort", sortOrderDefault)
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
Visible: visibleTypes,
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
|
||||
SupportedSortOrders: supportedSortOrders,
|
||||
}, tplExploreUsers)
|
||||
|
||||
@@ -55,11 +55,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
)
|
||||
|
||||
// we can not set orderBy to `models.SearchOrderByXxx`, because there may be a JOIN in the statement, different tables may have the same name columns
|
||||
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = setting.UI.ExploreDefaultSort
|
||||
}
|
||||
sortOrder := util.IfZero(string(opts.OrderBy), ctx.FormString("sort", setting.UI.ExploreDefaultSort))
|
||||
ctx.Data["SortType"] = sortOrder
|
||||
|
||||
switch sortOrder {
|
||||
@@ -145,18 +141,15 @@ func Users(ctx *context.Context) {
|
||||
"alphabetically",
|
||||
"reversealphabetically",
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder := ctx.FormString("sort", sortOrderDefault)
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
|
||||
SupportedSortOrders: supportedSortOrders,
|
||||
}, tplExploreUsers)
|
||||
|
||||
@@ -15,22 +15,16 @@ import (
|
||||
|
||||
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.SetFormString("id", "1")
|
||||
|
||||
DeleteOpenID(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
|
||||
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.SetFormString("id", "1")
|
||||
|
||||
ToggleOpenIDVisibility(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
@@ -104,8 +105,8 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiTokenScope"] = token.Scope
|
||||
return u, nil
|
||||
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySha: %v", err)
|
||||
} else if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
}
|
||||
|
||||
// check task token
|
||||
|
||||
@@ -130,7 +130,7 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
}
|
||||
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
|
||||
if err != nil {
|
||||
if auth_model.IsErrAccessTokenNotExist(err) {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
// check task token
|
||||
if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
|
||||
|
||||
@@ -78,8 +78,3 @@ func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return optional.Some(v)
|
||||
}
|
||||
|
||||
func (b *Base) SetFormString(key, value string) {
|
||||
_ = b.Req.FormValue(key) // force parse form
|
||||
b.Req.Form.Set(key, value)
|
||||
}
|
||||
|
||||
Generated
+97
@@ -19202,6 +19202,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/token": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"miscellaneous"
|
||||
],
|
||||
"summary": "Get the currently authenticated token",
|
||||
"operationId": "getCurrentToken",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/CurrentAccessToken"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"miscellaneous"
|
||||
],
|
||||
"summary": "Delete the currently authenticated token",
|
||||
"operationId": "deleteCurrentToken",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "token deleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/topics/search": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -25116,6 +25148,47 @@
|
||||
},
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"CurrentAccessToken": {
|
||||
"type": "object",
|
||||
"title": "CurrentAccessToken represents the metadata of the currently authenticated token.",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "The timestamp when the token was created",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "CreatedAt"
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique identifier of the access token",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"last_used_at": {
|
||||
"description": "The timestamp when the token was last used",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "LastUsedAt"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the access token",
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "The scopes granted to this access token",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Scopes"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/definitions/UserMeta"
|
||||
}
|
||||
},
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"DeleteEmailOption": {
|
||||
"description": "DeleteEmailOption options when deleting email addresses",
|
||||
"type": "object",
|
||||
@@ -30585,6 +30658,24 @@
|
||||
},
|
||||
"x-go-package": "gitea.dev/models/activities"
|
||||
},
|
||||
"UserMeta": {
|
||||
"type": "object",
|
||||
"title": "UserMeta represents minimal user information for the token owner.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "The unique identifier of the user",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"login": {
|
||||
"description": "The username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "Login"
|
||||
}
|
||||
},
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"UserSettings": {
|
||||
"description": "UserSettings represents user settings",
|
||||
"type": "object",
|
||||
@@ -31089,6 +31180,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CurrentAccessToken": {
|
||||
"description": "CurrentAccessToken represents the currently authenticated access token.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CurrentAccessToken"
|
||||
}
|
||||
},
|
||||
"DeployKey": {
|
||||
"description": "DeployKey",
|
||||
"schema": {
|
||||
|
||||
Generated
+95
@@ -399,6 +399,16 @@
|
||||
},
|
||||
"description": "CronList"
|
||||
},
|
||||
"CurrentAccessToken": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CurrentAccessToken"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "CurrentAccessToken represents the currently authenticated access token."
|
||||
},
|
||||
"DeployKey": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -4952,6 +4962,47 @@
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"CurrentAccessToken": {
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "The timestamp when the token was created",
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "CreatedAt"
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique identifier of the access token",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"last_used_at": {
|
||||
"description": "The timestamp when the token was last used",
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"x-go-name": "LastUsedAt"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the access token",
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"scopes": {
|
||||
"description": "The scopes granted to this access token",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"x-go-name": "Scopes"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserMeta"
|
||||
}
|
||||
},
|
||||
"title": "CurrentAccessToken represents the metadata of the currently authenticated token.",
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"DeleteEmailOption": {
|
||||
"description": "DeleteEmailOption options when deleting email addresses",
|
||||
"properties": {
|
||||
@@ -10454,6 +10505,24 @@
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/models/activities"
|
||||
},
|
||||
"UserMeta": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "The unique identifier of the user",
|
||||
"format": "int64",
|
||||
"type": "integer",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"login": {
|
||||
"description": "The username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "Login"
|
||||
}
|
||||
},
|
||||
"title": "UserMeta represents minimal user information for the token owner.",
|
||||
"type": "object",
|
||||
"x-go-package": "gitea.dev/modules/structs"
|
||||
},
|
||||
"UserSettings": {
|
||||
"description": "UserSettings represents user settings",
|
||||
"properties": {
|
||||
@@ -31385,6 +31454,32 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/token": {
|
||||
"delete": {
|
||||
"operationId": "deleteCurrentToken",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "token deleted"
|
||||
}
|
||||
},
|
||||
"summary": "Delete the currently authenticated token",
|
||||
"tags": [
|
||||
"miscellaneous"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getCurrentToken",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/CurrentAccessToken"
|
||||
}
|
||||
},
|
||||
"summary": "Get the currently authenticated token",
|
||||
"tags": [
|
||||
"miscellaneous"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/topics/search": {
|
||||
"get": {
|
||||
"operationId": "topicSearch",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAPIGetCurrentToken tests getting metadata of the currently authenticated token
|
||||
func TestAPIGetCurrentToken(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Success with all scopes", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-all", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{})
|
||||
assert.Equal(t, accessToken.ID, currentToken.ID)
|
||||
assert.Equal(t, accessToken.Name, currentToken.Name)
|
||||
assert.Equal(t, user.ID, currentToken.User.ID)
|
||||
assert.Equal(t, user.Name, currentToken.User.Login)
|
||||
})
|
||||
|
||||
t.Run("Success with limited scopes", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-limited", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadRepository})
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{})
|
||||
assert.Equal(t, accessToken.ID, currentToken.ID)
|
||||
assert.Equal(t, accessToken.Name, currentToken.Name)
|
||||
assert.Equal(t, user.ID, currentToken.User.ID)
|
||||
assert.Equal(t, user.Name, currentToken.User.Login)
|
||||
})
|
||||
|
||||
t.Run("Bad token", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth("this does not exist")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/token")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPITokenSelfService tests delete operations on token
|
||||
func TestAPITokenSelfService(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Success then verify deleted", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-delete-current-token", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||
|
||||
// Delete the token via the endpoint
|
||||
req := NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify the token is deleted
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: accessToken.ID})
|
||||
|
||||
// Verify the token can no longer be used for GET
|
||||
req = NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
// Verify the token can no longer be used for DELETE
|
||||
req = NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Bad token", func(t *testing.T) {
|
||||
req := NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth("this does not exist")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
req = NewRequest(t, "DELETE", "/api/v1/token")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user