Add scopes to API to create token and display them (#22989)

The API to create tokens is missing the ability to set the required
scopes for tokens, and to show them on the API and on the UI.

This PR adds this functionality.

Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
zeripath 2023-02-20 21:28:44 +00:00 committed by GitHub
parent 330b166423
commit d2128b44f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 14 deletions

View file

@ -168,10 +168,23 @@ var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{
// Parse parses the scope string into a bitmap, thus removing possible duplicates. // Parse parses the scope string into a bitmap, thus removing possible duplicates.
func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) { func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
list := strings.Split(string(s), ",")
var bitmap AccessTokenScopeBitmap var bitmap AccessTokenScopeBitmap
for _, v := range list {
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
remainingScopes := string(s)
for len(remainingScopes) > 0 {
i := strings.IndexByte(remainingScopes, ',')
var v string
if i < 0 {
v = remainingScopes
remainingScopes = ""
} else if i+1 >= len(remainingScopes) {
v = remainingScopes[:i]
remainingScopes = ""
} else {
v = remainingScopes[:i]
remainingScopes = remainingScopes[i+1:]
}
singleScope := AccessTokenScope(v) singleScope := AccessTokenScope(v)
if singleScope == "" { if singleScope == "" {
continue continue
@ -187,9 +200,15 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
} }
bitmap |= bits bitmap |= bits
} }
return bitmap, nil return bitmap, nil
} }
// StringSlice returns the AccessTokenScope as a []string
func (s AccessTokenScope) StringSlice() []string {
return strings.Split(string(s), ",")
}
// Normalize returns a normalized scope string without any duplicates. // Normalize returns a normalized scope string without any duplicates.
func (s AccessTokenScope) Normalize() (AccessTokenScope, error) { func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
bitmap, err := s.Parse() bitmap, err := s.Parse()

View file

@ -11,10 +11,11 @@ import (
// AccessToken represents an API access token. // AccessToken represents an API access token.
// swagger:response AccessToken // swagger:response AccessToken
type AccessToken struct { type AccessToken struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Token string `json:"sha1"` Token string `json:"sha1"`
TokenLastEight string `json:"token_last_eight"` TokenLastEight string `json:"token_last_eight"`
Scopes []string `json:"scopes"`
} }
// AccessTokenList represents a list of API access token. // AccessTokenList represents a list of API access token.
@ -22,9 +23,10 @@ type AccessToken struct {
type AccessTokenList []*AccessToken type AccessTokenList []*AccessToken
// CreateAccessTokenOption options when create access token // CreateAccessTokenOption options when create access token
// swagger:parameters userCreateToken
type CreateAccessTokenOption struct { type CreateAccessTokenOption struct {
Name string `json:"name" binding:"Required"` // required: true
Name string `json:"name" binding:"Required"`
Scopes []string `json:"scopes"`
} }
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application // CreateOAuth2ApplicationOptions holds options to create an oauth2 application

View file

@ -757,6 +757,7 @@ access_token_deletion_confirm_action = Delete
access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue? access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?
delete_token_success = The token has been deleted. Applications using it no longer have access to your account. delete_token_success = The token has been deleted. Applications using it no longer have access to your account.
select_scopes = Select scopes select_scopes = Select scopes
scopes_list = Scopes:
manage_oauth2_applications = Manage OAuth2 Applications manage_oauth2_applications = Manage OAuth2 Applications
edit_oauth2_application = Edit OAuth2 Application edit_oauth2_application = Edit OAuth2 Application

View file

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
@ -62,6 +63,7 @@ func ListAccessTokens(ctx *context.APIContext) {
ID: tokens[i].ID, ID: tokens[i].ID,
Name: tokens[i].Name, Name: tokens[i].Name,
TokenLastEight: tokens[i].TokenLastEight, TokenLastEight: tokens[i].TokenLastEight,
Scopes: tokens[i].Scope.StringSlice(),
} }
} }
@ -82,9 +84,9 @@ func CreateAccessToken(ctx *context.APIContext) {
// - name: username // - name: username
// in: path // in: path
// description: username of user // description: username of user
// type: string
// required: true // required: true
// - name: userCreateToken // type: string
// - name: body
// in: body // in: body
// schema: // schema:
// "$ref": "#/definitions/CreateAccessTokenOption" // "$ref": "#/definitions/CreateAccessTokenOption"
@ -111,6 +113,13 @@ func CreateAccessToken(ctx *context.APIContext) {
return return
} }
scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize()
if err != nil {
ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err))
return
}
t.Scope = scope
if err := auth_model.NewAccessToken(t); err != nil { if err := auth_model.NewAccessToken(t); err != nil {
ctx.Error(http.StatusInternalServerError, "NewAccessToken", err) ctx.Error(http.StatusInternalServerError, "NewAccessToken", err)
return return

View file

@ -14084,14 +14084,13 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"x-go-name": "Name",
"description": "username of user", "description": "username of user",
"name": "username", "name": "username",
"in": "path", "in": "path",
"required": true "required": true
}, },
{ {
"name": "userCreateToken", "name": "body",
"in": "body", "in": "body",
"schema": { "schema": {
"$ref": "#/definitions/CreateAccessTokenOption" "$ref": "#/definitions/CreateAccessTokenOption"
@ -14194,6 +14193,13 @@
"type": "string", "type": "string",
"x-go-name": "Name" "x-go-name": "Name"
}, },
"scopes": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Scopes"
},
"sha1": { "sha1": {
"type": "string", "type": "string",
"x-go-name": "Token" "x-go-name": "Token"
@ -14925,10 +14931,20 @@
"CreateAccessTokenOption": { "CreateAccessTokenOption": {
"description": "CreateAccessTokenOption options when create access token", "description": "CreateAccessTokenOption options when create access token",
"type": "object", "type": "object",
"required": [
"name"
],
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"x-go-name": "Name" "x-go-name": "Name"
},
"scopes": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Scopes"
} }
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"

View file

@ -21,7 +21,14 @@
</div> </div>
<i class="icon tooltip{{if .HasRecentActivity}} green{{end}}" {{if .HasRecentActivity}}data-content="{{$.locale.Tr "settings.token_state_desc"}}"{{end}}>{{svg "fontawesome-send" 36}}</i> <i class="icon tooltip{{if .HasRecentActivity}} green{{end}}" {{if .HasRecentActivity}}data-content="{{$.locale.Tr "settings.token_state_desc"}}"{{end}}>{{svg "fontawesome-send" 36}}</i>
<div class="content"> <div class="content">
<strong>{{.Name}}</strong> <details><summary><strong>{{.Name}}</strong></summary>
<p class="gt-my-2">{{$.locale.Tr "settings.scopes_list"}}</p>
<ul class="gt-my-2">
{{range .Scope.StringSlice}}
<li>{{.}}</li>
{{end}}
</ul>
</details>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i> <i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
</div> </div>