mirror of
https://github.com/go-gitea/gitea
synced 2025-03-09 20:24:28 +00:00
Improve "generate new access token" form (#33730)
Fix: https://github.com/go-gitea/gitea/issues/33519 As discussed in [PR #33614](https://github.com/go-gitea/gitea/pull/33614), the ScopedAccessTokenSelector Vue component is not particularly useful. This PR removes the component and reverts to using HTML templates. It also introduces some (hopefully) useful refactoring. The Vue component was causing the UX bug reported in the linked issue. Required form fields are now properly working, as expected (see screenshot).  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
8362a41559
commit
303af554c9
@ -5,6 +5,7 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
@ -14,7 +15,7 @@ import (
|
|||||||
type AccessTokenScopeCategory int
|
type AccessTokenScopeCategory int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AccessTokenScopeCategoryActivityPub = iota
|
AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
|
||||||
AccessTokenScopeCategoryAdmin
|
AccessTokenScopeCategoryAdmin
|
||||||
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
|
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
|
||||||
AccessTokenScopeCategoryNotification
|
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
|
// GetRequiredScopes gets the specific scopes for a given level and categories
|
||||||
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
|
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
|
||||||
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
|
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
|
||||||
@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
|||||||
|
|
||||||
// StringSlice returns the AccessTokenScope as a []string
|
// StringSlice returns the AccessTokenScope as a []string
|
||||||
func (s AccessTokenScope) StringSlice() []string {
|
func (s AccessTokenScope) StringSlice() []string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return strings.Split(string(s), ",")
|
return strings.Split(string(s), ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ type scopeTestNormalize struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessTokenScope_Normalize(t *testing.T) {
|
func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||||
|
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
|
||||||
tests := []scopeTestNormalize{
|
tests := []scopeTestNormalize{
|
||||||
{"", "", nil},
|
{"", "", nil},
|
||||||
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", 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},
|
{"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,
|
tests = append(tests,
|
||||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
|
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},
|
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},
|
{"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,
|
tests = append(tests,
|
||||||
scopeTestHasScope{
|
scopeTestHasScope{
|
||||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
|
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
|
||||||
|
@ -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
|
repo_and_org_access = Repository and Organization Access
|
||||||
permissions_public_only = Public only
|
permissions_public_only = Public only
|
||||||
permissions_access_all = All (public, private, and limited)
|
permissions_access_all = All (public, private, and limited)
|
||||||
select_permissions = Select permissions
|
|
||||||
permission_not_set = Not set
|
permission_not_set = Not set
|
||||||
permission_no_access = No Access
|
permission_no_access = No Access
|
||||||
permission_read = Read
|
permission_read = Read
|
||||||
|
@ -6,12 +6,14 @@ package setting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
@ -39,18 +41,29 @@ func ApplicationsPost(ctx *context.Context) {
|
|||||||
ctx.Data["PageIsSettingsApplications"] = true
|
ctx.Data["PageIsSettingsApplications"] = true
|
||||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||||
|
|
||||||
if ctx.HasError() {
|
_ = ctx.Req.ParseForm()
|
||||||
loadApplicationsData(ctx)
|
var scopeNames []string
|
||||||
|
for k, v := range ctx.Req.Form {
|
||||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
if strings.HasPrefix(k, "scope-") {
|
||||||
return
|
scopeNames = append(scopeNames, v...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope, err := form.GetScope()
|
scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetScope", err)
|
ctx.ServerError("GetScope", err)
|
||||||
return
|
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{
|
t := &auth_model.AccessToken{
|
||||||
UID: ctx.Doer.ID,
|
UID: ctx.Doer.ID,
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
@ -99,7 +112,14 @@ func loadApplicationsData(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["Tokens"] = tokens
|
ctx.Data["Tokens"] = tokens
|
||||||
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
|
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 {
|
if setting.OAuth2.Enabled {
|
||||||
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
|
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
|
||||||
OwnerID: ctx.Doer.ID,
|
OwnerID: ctx.Doer.ID,
|
||||||
|
@ -213,13 +213,16 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||||||
// Attention: this function changes ctx.Data and ctx.Flash
|
// 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.
|
// 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 {
|
func (ctx *Context) HasError() bool {
|
||||||
hasErr, ok := ctx.Data["HasError"]
|
hasErr, _ := ctx.Data["HasError"].(bool)
|
||||||
if !ok {
|
hasErr = hasErr || ctx.Flash.ErrorMsg != ""
|
||||||
|
if !hasErr {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if ctx.Flash.ErrorMsg == "" {
|
||||||
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
||||||
|
}
|
||||||
ctx.Data["Flash"] = ctx.Flash
|
ctx.Data["Flash"] = ctx.Flash
|
||||||
return hasErr.(bool)
|
return hasErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetErrMsg returns error message in form validation.
|
// GetErrMsg returns error message in form validation.
|
||||||
|
@ -7,9 +7,7 @@ package forms
|
|||||||
import (
|
import (
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
@ -348,7 +346,6 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
|
|||||||
// NewAccessTokenForm form for creating access token
|
// NewAccessTokenForm form for creating access token
|
||||||
type NewAccessTokenForm struct {
|
type NewAccessTokenForm struct {
|
||||||
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
|
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
|
||||||
Scope []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// 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)
|
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
|
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||||
type EditOAuth2ApplicationForm struct {
|
type EditOAuth2ApplicationForm struct {
|
||||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
package forms
|
package forms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
@ -104,28 +102,3 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
|
|||||||
assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
|
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -50,49 +50,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached segment">
|
<div class="ui bottom attached segment">
|
||||||
<h5 class="ui top header">
|
<details {{if or .name (not .Tokens)}}open{{end}}>
|
||||||
{{ctx.Locale.Tr "settings.generate_new_token"}}
|
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.generate_new_token"}}</h4></summary>
|
||||||
</h5>
|
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
|
||||||
<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
|
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="field {{if .Err_Name}}error{{end}}">
|
<div class="field {{if .Err_Name}}error{{end}}">
|
||||||
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
|
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
|
||||||
<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
|
<input id="name" name="name" value="{{.name}}" required maxlength="255">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
|
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
|
||||||
<label class="tw-cursor-pointer">
|
<label class="gt-checkbox">
|
||||||
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
|
<input type="radio" name="scope-public-only" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
|
||||||
{{ctx.Locale.Tr "settings.permissions_public_only"}}
|
|
||||||
</label>
|
</label>
|
||||||
<label class="tw-cursor-pointer">
|
<label class="gt-checkbox">
|
||||||
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
|
<input type="radio" name="scope-public-only" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
|
||||||
{{ctx.Locale.Tr "settings.permissions_access_all"}}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<details class="ui optional field">
|
<div>
|
||||||
<summary class="tw-pb-4 tw-pl-1">
|
<div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</div>
|
||||||
{{ctx.Locale.Tr "settings.select_permissions"}}
|
<table class="ui table unstackable tw-my-2">
|
||||||
</summary>
|
{{range $category := .TokenCategories}}
|
||||||
<p class="activity meta">
|
<tr>
|
||||||
<i>{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
|
<td>{{$category}}</td>
|
||||||
</p>
|
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
|
||||||
<div id="scoped-access-token-selector"
|
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
|
||||||
data-is-admin="{{if .IsAdmin}}true{{else}}false{{end}}"
|
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
|
||||||
data-no-access-label="{{ctx.Locale.Tr "settings.permission_no_access"}}"
|
</tr>
|
||||||
data-read-label="{{ctx.Locale.Tr "settings.permission_read"}}"
|
{{end}}
|
||||||
data-write-label="{{ctx.Locale.Tr "settings.permission_write"}}"
|
</table>
|
||||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<button class="ui primary button">
|
||||||
<button id="scoped-access-submit" class="ui primary button">
|
|
||||||
{{ctx.Locale.Tr "settings.generate_token"}}
|
{{ctx.Locale.Tr "settings.generate_token"}}
|
||||||
</button>
|
</button>
|
||||||
</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
|
</form>
|
||||||
<div id="scoped-access-warning" class="ui warning message center tw-hidden">
|
</details>
|
||||||
{{ctx.Locale.Tr "settings.at_least_one_permission"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .EnableOAuth2}}
|
{{if .EnableOAuth2}}
|
||||||
|
@ -48,9 +48,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui bottom attached segment">
|
<div class="ui bottom attached segment">
|
||||||
<h5 class="ui top header">
|
<details {{if .application_name}}open{{end}}>
|
||||||
{{ctx.Locale.Tr "settings.create_oauth2_application"}}
|
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.create_oauth2_application"}}</h4></summary>
|
||||||
</h5>
|
|
||||||
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
|
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="field {{if .Err_AppName}}error{{end}}">
|
<div class="field {{if .Err_AppName}}error{{end}}">
|
||||||
@ -77,4 +76,5 @@
|
|||||||
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
|
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,7 +76,7 @@ func TestAPIAdminOrgCreateNotAdmin(t *testing.T) {
|
|||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
nonAdminUsername := "user2"
|
nonAdminUsername := "user2"
|
||||||
session := loginUser(t, nonAdminUsername)
|
session := loginUser(t, nonAdminUsername)
|
||||||
token := getTokenForLoggedInUser(t, session)
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
org := api.CreateOrgOption{
|
org := api.CreateOrgOption{
|
||||||
UserName: "user2_org",
|
UserName: "user2_org",
|
||||||
FullName: "User2's organization",
|
FullName: "User2's organization",
|
||||||
|
@ -76,7 +76,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
|
|||||||
var newPublicKey api.PublicKey
|
var newPublicKey api.PublicKey
|
||||||
DecodeJSON(t, resp, &newPublicKey)
|
DecodeJSON(t, resp, &newPublicKey)
|
||||||
|
|
||||||
token = getUserToken(t, normalUsername)
|
token = getUserToken(t, normalUsername, auth_model.AccessTokenScopeAll)
|
||||||
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID).
|
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID).
|
||||||
AddTokenAuth(token)
|
AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusForbidden)
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
@ -139,7 +139,7 @@ func TestAPIListUsersNotLoggedIn(t *testing.T) {
|
|||||||
func TestAPIListUsersNonAdmin(t *testing.T) {
|
func TestAPIListUsersNonAdmin(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
nonAdminUsername := "user2"
|
nonAdminUsername := "user2"
|
||||||
token := getUserToken(t, nonAdminUsername)
|
token := getUserToken(t, nonAdminUsername, auth_model.AccessTokenScopeAll)
|
||||||
req := NewRequest(t, "GET", "/api/v1/admin/users").
|
req := NewRequest(t, "GET", "/api/v1/admin/users").
|
||||||
AddTokenAuth(token)
|
AddTokenAuth(token)
|
||||||
MakeRequest(t, req, http.StatusForbidden)
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
@ -33,6 +33,10 @@ type APITestContext struct {
|
|||||||
|
|
||||||
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
|
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
|
||||||
session := loginUser(t, username)
|
session := loginUser(t, username)
|
||||||
|
if len(scope) == 0 {
|
||||||
|
// FIXME: legacy logic: no scope means all
|
||||||
|
scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll}
|
||||||
|
}
|
||||||
token := getTokenForLoggedInUser(t, session, scope...)
|
token := getTokenForLoggedInUser(t, session, scope...)
|
||||||
return APITestContext{
|
return APITestContext{
|
||||||
Session: session,
|
Session: session,
|
||||||
|
@ -72,7 +72,7 @@ func TestAPIReposGitBlobs(t *testing.T) {
|
|||||||
|
|
||||||
// Login as User4.
|
// Login as User4.
|
||||||
session = loginUser(t, user4.Name)
|
session = loginUser(t, user4.Name)
|
||||||
token4 := getTokenForLoggedInUser(t, session)
|
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
|
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)
|
||||||
|
@ -69,7 +69,7 @@ func TestAPIReposGitTrees(t *testing.T) {
|
|||||||
|
|
||||||
// Login as User4.
|
// Login as User4.
|
||||||
session = loginUser(t, user4.Name)
|
session = loginUser(t, user4.Name)
|
||||||
token4 := getTokenForLoggedInUser(t, session)
|
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
|
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)
|
||||||
|
@ -249,55 +249,19 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
|
|||||||
// token has to be unique this counter take care of
|
// token has to be unique this counter take care of
|
||||||
var tokenCounter int64
|
var tokenCounter int64
|
||||||
|
|
||||||
// getTokenForLoggedInUser returns a token for a logged in user.
|
// getTokenForLoggedInUser returns a token for a logged-in user.
|
||||||
// The scope is an optional list of snake_case strings like the frontend form fields,
|
|
||||||
// but without the "scope_" prefix.
|
|
||||||
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
|
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var token string
|
|
||||||
req := NewRequest(t, "GET", "/user/settings/applications")
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
var csrf string
|
|
||||||
for _, cookie := range resp.Result().Cookies() {
|
|
||||||
if cookie.Name != "_csrf" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
csrf = cookie.Value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if csrf == "" {
|
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
|
||||||
csrf = doc.GetCSRF()
|
|
||||||
}
|
|
||||||
assert.NotEmpty(t, csrf)
|
|
||||||
urlValues := url.Values{}
|
urlValues := url.Values{}
|
||||||
urlValues.Add("_csrf", csrf)
|
urlValues.Add("_csrf", GetUserCSRFToken(t, session))
|
||||||
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
|
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
urlValues.Add("scope", string(scope))
|
urlValues.Add("scope-dummy", string(scope)) // it only needs to start with "scope-" to be accepted
|
||||||
}
|
}
|
||||||
req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
|
req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
flashes := session.GetCookieFlashMessage()
|
||||||
// Log the flash values on failure
|
return flashes.InfoMsg
|
||||||
if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) {
|
|
||||||
for _, cookie := range resp.Result().Cookies() {
|
|
||||||
if cookie.Name != gitea_context.CookieNameFlash {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
flash, _ := url.ParseQuery(cookie.Value)
|
|
||||||
for key, value := range flash {
|
|
||||||
t.Logf("Flash %q: %q", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req = NewRequest(t, "GET", "/user/settings/applications")
|
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
token = htmlDoc.doc.Find(".ui.info p").Text()
|
|
||||||
assert.NotEmpty(t, token)
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestWrapper struct {
|
type RequestWrapper struct {
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
@import "./modules/dimmer.css";
|
@import "./modules/dimmer.css";
|
||||||
@import "./modules/modal.css";
|
@import "./modules/modal.css";
|
||||||
|
|
||||||
@import "./modules/select.css";
|
|
||||||
@import "./modules/tippy.css";
|
@import "./modules/tippy.css";
|
||||||
@import "./modules/breadcrumb.css";
|
@import "./modules/breadcrumb.css";
|
||||||
@import "./modules/comment.css";
|
@import "./modules/comment.css";
|
||||||
|
@ -119,3 +119,13 @@ input[type="radio"] {
|
|||||||
.ui.toggle.checkbox input:focus:checked ~ label::before {
|
.ui.toggle.checkbox input:focus:checked ~ label::before {
|
||||||
background: var(--color-primary) !important;
|
background: var(--color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.gt-checkbox {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.form .field > label.gt-checkbox {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
.gitea-select {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gitea-select select {
|
|
||||||
appearance: none; /* hide default triangle */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ::before and ::after pseudo elements don't work on select elements,
|
|
||||||
so we need to put it on the parent. */
|
|
||||||
.gitea-select::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
right: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: "";
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
mask-size: cover;
|
|
||||||
-webkit-mask-size: cover;
|
|
||||||
mask-image: var(--octicon-chevron-right);
|
|
||||||
-webkit-mask-image: var(--octicon-chevron-right);
|
|
||||||
transform: rotate(90deg); /* point the chevron down */
|
|
||||||
background: currentcolor;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {computed, onMounted, onUnmounted} from 'vue';
|
|
||||||
import {hideElem, showElem} from '../utils/dom.ts';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isAdmin: boolean;
|
|
||||||
noAccessLabel: string;
|
|
||||||
readLabel: string;
|
|
||||||
writeLabel: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const categories = computed(() => {
|
|
||||||
const categories = [
|
|
||||||
'activitypub',
|
|
||||||
];
|
|
||||||
if (props.isAdmin) {
|
|
||||||
categories.push('admin');
|
|
||||||
}
|
|
||||||
categories.push(
|
|
||||||
'issue',
|
|
||||||
'misc',
|
|
||||||
'notification',
|
|
||||||
'organization',
|
|
||||||
'package',
|
|
||||||
'repository',
|
|
||||||
'user');
|
|
||||||
return categories;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.querySelector('#scoped-access-submit').addEventListener('click', onClickSubmit);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
|
|
||||||
});
|
|
||||||
|
|
||||||
function onClickSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const warningEl = document.querySelector('#scoped-access-warning');
|
|
||||||
// check that at least one scope has been selected
|
|
||||||
for (const el of document.querySelectorAll<HTMLInputElement>('.access-token-select')) {
|
|
||||||
if (el.value) {
|
|
||||||
// Hide the error if it was visible from previous attempt.
|
|
||||||
hideElem(warningEl);
|
|
||||||
// Submit the form.
|
|
||||||
document.querySelector<HTMLFormElement>('#scoped-access-form').submit();
|
|
||||||
// Don't show the warning.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// no scopes selected, show validation error
|
|
||||||
showElem(warningEl);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
|
|
||||||
<label class="category-label" :for="'access-token-scope-' + category">
|
|
||||||
{{ category }}
|
|
||||||
</label>
|
|
||||||
<div class="gitea-select">
|
|
||||||
<select
|
|
||||||
class="ui selection access-token-select"
|
|
||||||
name="scope"
|
|
||||||
:id="'access-token-scope-' + category"
|
|
||||||
>
|
|
||||||
<option value="">
|
|
||||||
{{ noAccessLabel }}
|
|
||||||
</option>
|
|
||||||
<option :value="'read:' + category">
|
|
||||||
{{ readLabel }}
|
|
||||||
</option>
|
|
||||||
<option :value="'write:' + category">
|
|
||||||
{{ writeLabel }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,20 +0,0 @@
|
|||||||
import {createApp} from 'vue';
|
|
||||||
|
|
||||||
export async function initScopedAccessTokenCategories() {
|
|
||||||
const el = document.querySelector('#scoped-access-token-selector');
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const {default: ScopedAccessTokenSelector} = await import(/* webpackChunkName: "scoped-access-token-selector" */'../components/ScopedAccessTokenSelector.vue');
|
|
||||||
try {
|
|
||||||
const View = createApp(ScopedAccessTokenSelector, {
|
|
||||||
isAdmin: JSON.parse(el.getAttribute('data-is-admin')),
|
|
||||||
noAccessLabel: el.getAttribute('data-no-access-label'),
|
|
||||||
readLabel: el.getAttribute('data-read-label'),
|
|
||||||
writeLabel: el.getAttribute('data-write-label'),
|
|
||||||
});
|
|
||||||
View.mount(el);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('ScopedAccessTokenSelector failed to load', err);
|
|
||||||
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
|
|
||||||
}
|
|
||||||
}
|
|
@ -68,7 +68,6 @@ import {initColorPickers} from './features/colorpicker.ts';
|
|||||||
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
||||||
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
|
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
|
||||||
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
|
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
|
||||||
import {initScopedAccessTokenCategories} from './features/scoped-access-token.ts';
|
|
||||||
import {
|
import {
|
||||||
initFootLanguageMenu,
|
initFootLanguageMenu,
|
||||||
initGlobalDropdown,
|
initGlobalDropdown,
|
||||||
@ -209,7 +208,6 @@ onDomReady(() => {
|
|||||||
initUserSettings,
|
initUserSettings,
|
||||||
initRepoDiffView,
|
initRepoDiffView,
|
||||||
initPdfViewer,
|
initPdfViewer,
|
||||||
initScopedAccessTokenCategories,
|
|
||||||
initColorPickers,
|
initColorPickers,
|
||||||
|
|
||||||
initOAuth2SettingsDisableCheckbox,
|
initOAuth2SettingsDisableCheckbox,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user