diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go
index 897ff3fc9e..0e5b2e96e6 100644
--- a/models/auth/access_token_scope.go
+++ b/models/auth/access_token_scope.go
@@ -5,6 +5,7 @@ package auth
import (
"fmt"
+ "slices"
"strings"
"code.gitea.io/gitea/models/perm"
@@ -14,7 +15,7 @@ import (
type AccessTokenScopeCategory int
const (
- AccessTokenScopeCategoryActivityPub = iota
+ AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
AccessTokenScopeCategoryAdmin
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
AccessTokenScopeCategoryNotification
@@ -193,6 +194,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
},
}
+func GetAccessTokenCategories() (res []string) {
+ for _, cat := range accessTokenScopes[Read] {
+ res = append(res, strings.TrimPrefix(string(cat), "read:"))
+ }
+ slices.Sort(res)
+ return res
+}
+
// GetRequiredScopes gets the specific scopes for a given level and categories
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
@@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
// StringSlice returns the AccessTokenScope as a []string
func (s AccessTokenScope) StringSlice() []string {
+ if s == "" {
+ return nil
+ }
return strings.Split(string(s), ",")
}
diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go
index a6097e45d7..9e4aa83633 100644
--- a/models/auth/access_token_scope_test.go
+++ b/models/auth/access_token_scope_test.go
@@ -17,6 +17,7 @@ type scopeTestNormalize struct {
}
func TestAccessTokenScope_Normalize(t *testing.T) {
+ assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{
{"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
@@ -25,7 +26,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
}
- for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+ for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
@@ -59,7 +60,7 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
{"public-only", "read:issue", false, nil},
}
- for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+ for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4c25ba3205..5aa6d19dae 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -917,7 +917,6 @@ delete_token_success = The token has been deleted. Applications using it no long
repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only
permissions_access_all = All (public, private, and limited)
-select_permissions = Select permissions
permission_not_set = Not set
permission_no_access = No Access
permission_read = Read
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index cf71d01dc1..1f6c97a5cc 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -6,12 +6,14 @@ package setting
import (
"net/http"
+ "strings"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@@ -39,18 +41,29 @@ func ApplicationsPost(ctx *context.Context) {
ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
- if ctx.HasError() {
- loadApplicationsData(ctx)
-
- ctx.HTML(http.StatusOK, tplSettingsApplications)
- return
+ _ = ctx.Req.ParseForm()
+ var scopeNames []string
+ for k, v := range ctx.Req.Form {
+ if strings.HasPrefix(k, "scope-") {
+ scopeNames = append(scopeNames, v...)
+ }
}
- scope, err := form.GetScope()
+ scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
if err != nil {
ctx.ServerError("GetScope", err)
return
}
+ if scope == "" || scope == auth_model.AccessTokenScopePublicOnly {
+ ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
+ }
+
+ if ctx.HasError() {
+ loadApplicationsData(ctx)
+ ctx.HTML(http.StatusOK, tplSettingsApplications)
+ return
+ }
+
t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
@@ -99,7 +112,14 @@ func loadApplicationsData(ctx *context.Context) {
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
- ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
+
+ // Handle specific ordered token categories for admin or non-admin users
+ tokenCategoryNames := auth_model.GetAccessTokenCategories()
+ if !ctx.Doer.IsAdmin {
+ tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
+ }
+ ctx.Data["TokenCategories"] = tokenCategoryNames
+
if setting.OAuth2.Enabled {
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
OwnerID: ctx.Doer.ID,
diff --git a/services/context/context.go b/services/context/context.go
index 5e08fba442..f3a0f0bb5f 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -213,13 +213,16 @@ func Contexter() func(next http.Handler) http.Handler {
// Attention: this function changes ctx.Data and ctx.Flash
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
func (ctx *Context) HasError() bool {
- hasErr, ok := ctx.Data["HasError"]
- if !ok {
+ hasErr, _ := ctx.Data["HasError"].(bool)
+ hasErr = hasErr || ctx.Flash.ErrorMsg != ""
+ if !hasErr {
return false
}
- ctx.Flash.ErrorMsg = ctx.GetErrMsg()
+ if ctx.Flash.ErrorMsg == "" {
+ ctx.Flash.ErrorMsg = ctx.GetErrMsg()
+ }
ctx.Data["Flash"] = ctx.Flash
- return hasErr.(bool)
+ return hasErr
}
// GetErrMsg returns error message in form validation.
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index ed79936add..c9ce71e886 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -7,9 +7,7 @@ package forms
import (
"mime/multipart"
"net/http"
- "strings"
- auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
@@ -347,8 +345,7 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
- Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
- Scope []string
+ Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
}
// Validate validates the fields
@@ -357,12 +354,6 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
- scope := strings.Join(f.Scope, ",")
- s, err := auth_model.AccessTokenScope(scope).Normalize()
- return s, err
-}
-
// EditOAuth2ApplicationForm form for editing oauth2 applications
type EditOAuth2ApplicationForm struct {
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index 66050187c9..b4120f20ed 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -4,10 +4,8 @@
package forms
import (
- "strconv"
"testing"
- auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/setting"
"github.com/gobwas/glob"
@@ -104,28 +102,3 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
}
}
-
-func TestNewAccessTokenForm_GetScope(t *testing.T) {
- tests := []struct {
- form NewAccessTokenForm
- scope auth_model.AccessTokenScope
- expectedErr error
- }{
- {
- form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}},
- scope: "read:repository",
- },
- {
- form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}},
- scope: "read:repository,write:user",
- },
- }
-
- for i, test := range tests {
- t.Run(strconv.Itoa(i), func(t *testing.T) {
- scope, err := test.form.GetScope()
- assert.Equal(t, test.expectedErr, err)
- assert.Equal(t, test.scope, scope)
- })
- }
-}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 31d1a2ac5b..501f238c7a 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -50,49 +50,41 @@
-
-