Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve "generate new access token" form #33730

Merged
merged 12 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion models/auth/access_token_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package auth

import (
"fmt"
"slices"
"strings"

"code.gitea.io/gitea/models/perm"
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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), ",")
}

Expand Down
4 changes: 2 additions & 2 deletions models/auth/access_token_scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{"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 AllAccessTokenScopeCategoryNames {

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / test-unit

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 28 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

undefined: AllAccessTokenScopeCategoryNames
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},
Expand Down Expand Up @@ -59,7 +59,7 @@
{"public-only", "read:issue", false, nil},
}

for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
for _, scope := range AllAccessTokenScopeCategoryNames {

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / test-unit

undefined: AllAccessTokenScopeCategoryNames

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

undefined: AllAccessTokenScopeCategoryNames (typecheck)

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-backend

undefined: AllAccessTokenScopeCategoryNames) (typecheck)

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

undefined: AllAccessTokenScopeCategoryNames (typecheck)

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

undefined: AllAccessTokenScopeCategoryNames) (typecheck)

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

undefined: AllAccessTokenScopeCategoryNames (typecheck)

Check failure on line 62 in models/auth/access_token_scope_test.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

undefined: AllAccessTokenScopeCategoryNames) (typecheck)
tests = append(tests,
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
Expand Down
1 change: 0 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 27 additions & 7 deletions routers/web/user/setting/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 == "" {
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,
Expand Down Expand Up @@ -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 {
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,
Expand Down
10 changes: 5 additions & 5 deletions services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ 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 {
return false
hasErr, _ := ctx.Data["HasError"].(bool)
hasErr = hasErr || ctx.Flash.ErrorMsg != ""
if ctx.Flash.ErrorMsg == "" {
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
}
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
ctx.Data["Flash"] = ctx.Flash
return hasErr.(bool)
return hasErr
}

// GetErrMsg returns error message in form validation.
Expand Down
11 changes: 1 addition & 10 deletions services/forms/user_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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"`
Expand Down
76 changes: 34 additions & 42 deletions templates/user/settings/applications.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,49 +50,41 @@
</div>
</div>
<div class="ui bottom attached segment">
<h5 class="ui top header">
{{ctx.Locale.Tr "settings.generate_new_token"}}
</h5>
<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_Name}}error{{end}}">
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
<label class="tw-cursor-pointer">
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
{{ctx.Locale.Tr "settings.permissions_public_only"}}
</label>
<label class="tw-cursor-pointer">
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
{{ctx.Locale.Tr "settings.permissions_access_all"}}
</label>
</div>
<details class="ui optional field">
<summary class="tw-pb-4 tw-pl-1">
{{ctx.Locale.Tr "settings.select_permissions"}}
</summary>
<p class="activity meta">
<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>
</p>
<div id="scoped-access-token-selector"
data-is-admin="{{if .IsAdmin}}true{{else}}false{{end}}"
data-no-access-label="{{ctx.Locale.Tr "settings.permission_no_access"}}"
data-read-label="{{ctx.Locale.Tr "settings.permission_read"}}"
data-write-label="{{ctx.Locale.Tr "settings.permission_write"}}"
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
>
<details {{if or .name (not .Tokens)}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.generate_new_token"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_Name}}error{{end}}">
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
<input id="name" name="name" value="{{.name}}" required maxlength="255">
</div>
</details>
<button id="scoped-access-submit" class="ui primary button">
{{ctx.Locale.Tr "settings.generate_token"}}
</button>
</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
<div id="scoped-access-warning" class="ui warning message center tw-hidden">
{{ctx.Locale.Tr "settings.at_least_one_permission"}}
</div>
<div class="field">
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
<label class="gt-checkbox">
<input type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
</label>
<label class="gt-checkbox">
<input type="radio" name="scope" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
</label>
</div>
<div>
<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>
<table class="ui table unstackable tw-my-2">
{{range $category := .TokenCategories}}
<tr>
<td>{{$category}}</td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
</tr>
{{end}}
</table>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "settings.generate_token"}}
</button>
</form>
</details>
</div>

{{if .EnableOAuth2}}
Expand Down
54 changes: 27 additions & 27 deletions templates/user/settings/applications_oauth2_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,33 @@
</div>

<div class="ui bottom attached segment">
<h5 class="ui top header">
{{ctx.Locale.Tr "settings.create_oauth2_application"}}
</h5>
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
</div>
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris"></textarea>
</div>
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked>
<details {{if .application_name}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.create_oauth2_application"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_AppName}}error{{end}}">
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
</div>
</div>
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
<input type="checkbox" name="skip_secondary_authorization">
<div class="field {{if .Err_RedirectURI}}error{{end}}">
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
<textarea name="redirect_uris" id="redirect-uris"></textarea>
</div>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
</button>
</form>
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked>
</div>
</div>
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
<input type="checkbox" name="skip_secondary_authorization">
</div>
</div>
<button class="ui primary button">
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
</button>
</form>
</details>
</div>
1 change: 0 additions & 1 deletion web_src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
@import "./modules/dimmer.css";
@import "./modules/modal.css";

@import "./modules/select.css";
@import "./modules/tippy.css";
@import "./modules/breadcrumb.css";
@import "./modules/comment.css";
Expand Down
10 changes: 10 additions & 0 deletions web_src/css/modules/checkbox.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,13 @@ input[type="radio"] {
.ui.toggle.checkbox input:focus:checked ~ label::before {
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;
}
25 changes: 0 additions & 25 deletions web_src/css/modules/select.css

This file was deleted.

Loading
Loading