Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)

Backport #34080

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
This commit is contained in:
wxiaoguang 2025-04-03 01:05:28 +08:00 committed by GitHub
parent 8f75f61b64
commit a40e15a116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 124 additions and 30 deletions

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"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"
@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
Name: "access-token", Name: "access-token",
Usage: "Generate access token for the user", Usage: "Generate access token for the user",
}, },
&cli.StringFlag{
Name: "access-token-name",
Usage: `Name of the generated access token`,
Value: "gitea-admin",
},
&cli.StringFlag{
Name: "access-token-scopes",
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
Value: "all",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "restricted", Name: "restricted",
Usage: "Make a restricted user account", Usage: "Make a restricted user account",
@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
IsRestricted: restricted, IsRestricted: restricted,
} }
var accessTokenName string
var accessTokenScope auth_model.AccessTokenScope
if c.IsSet("access-token") {
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
if accessTokenName == "" {
return errors.New("access-token-name cannot be empty")
}
var err error
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err)
}
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
}
// arguments should be prepared before creating the user & access token, in case there is anything wrong
// create the user
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err) return fmt.Errorf("CreateUser: %w", err)
} }
// create the access token
if c.Bool("access-token") { if accessTokenScope != "" {
t := &auth_model.AccessToken{ t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
Name: "gitea-admin",
UID: u.ID,
}
if err := auth_model.NewAccessToken(ctx, t); err != nil { if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err return err
} }
fmt.Printf("Access token was successfully created... %s\n", t.Token) fmt.Printf("Access token was successfully created... %s\n", t.Token)
} }
fmt.Printf("New user '%s' has been successfully created!\n", username) fmt.Printf("New user '%s' has been successfully created!\n", username)
return nil return nil
} }

View File

@ -8,37 +8,97 @@ import (
"strings" "strings"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAdminUserCreate(t *testing.T) { func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{}) app := NewMainApp(AppVersion{})
reset := func() { reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
} }
t.Run("MustChangePassword", func(t *testing.T) {
type createCheck struct{ IsAdmin, MustChangePassword bool } type check struct{ IsAdmin, MustChangePassword bool }
createUser := func(name, args string) createCheck { createCheck := func(name, args string) check {
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return createCheck{u.IsAdmin, u.MustChangePassword} return check{u.IsAdmin, u.MustChangePassword}
} }
reset() reset()
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password") assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
reset() reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
reset() reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
})
createUser := func(name, args string) error {
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
}
t.Run("AccessToken", func(t *testing.T) {
// no generated access token
reset()
assert.NoError(t, createUser("u", "--random-password"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
// using "--access-token" only means "all" access
reset()
assert.NoError(t, createUser("u", "--random-password --access-token"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.True(t, hasScopes)
// using "--access-token" with name & scopes
reset()
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
assert.NoError(t, err)
assert.True(t, hasScopes)
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.False(t, hasScopes)
// using "--access-token-name" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-name new-token-name")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// using "--access-token-scopes" without "--access-token"
reset()
err = createUser("u", "--random-password --access-token-scopes read:issue")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission
reset()
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission")
})
} }

View File

@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "scopes", Name: "scopes",
Value: "", Value: "all",
Usage: "Comma separated list of scopes to apply to access token", Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
}, },
}, },
Action: runGenerateAccessToken, Action: runGenerateAccessToken,
@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
func runGenerateAccessToken(c *cli.Context) error { func runGenerateAccessToken(c *cli.Context) error {
if !c.IsSet("username") { if !c.IsSet("username") {
return errors.New("You must provide a username to generate a token for") return errors.New("you must provide a username to generate a token for")
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()
@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err) return fmt.Errorf("invalid access token scope provided: %w", err)
} }
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
t.Scope = accessTokenScope t.Scope = accessTokenScope
// create the token // create the token

View File

@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
return bitmap.toScope(), nil return bitmap.toScope(), nil
} }
func (s AccessTokenScope) HasPermissionScope() bool {
return s != "" && s != AccessTokenScopePublicOnly
}
// PublicOnly checks if this token scope is limited to public resources // PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) { func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse() bitmap, err := s.parse()