mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 17:09:18 -04:00 
			
		
		
		
	Backport #34080 Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 check struct{ IsAdmin, MustChangePassword bool } | ||||||
|  | 		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)))) | ||||||
|  | 			u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) | ||||||
|  | 			return check{u.IsAdmin, u.MustChangePassword} | ||||||
|  | 		} | ||||||
|  | 		reset() | ||||||
|  | 		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password") | ||||||
|  |  | ||||||
|  | 		reset() | ||||||
|  | 		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password") | ||||||
|  |  | ||||||
|  | 		reset() | ||||||
|  | 		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password")) | ||||||
|  | 		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin")) | ||||||
|  | 		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false")) | ||||||
|  | 		assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", "")) | ||||||
|  | 		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))) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	type createCheck struct{ IsAdmin, MustChangePassword bool } | 	t.Run("AccessToken", func(t *testing.T) { | ||||||
| 	createUser := func(name, args string) createCheck { | 		// no generated access token | ||||||
| 		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)))) | 		reset() | ||||||
| 		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) | 		assert.NoError(t, createUser("u", "--random-password")) | ||||||
| 		return createCheck{u.IsAdmin, u.MustChangePassword} | 		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) | ||||||
| 	} | 		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) | ||||||
| 	reset() |  | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password") |  | ||||||
|  |  | ||||||
| 	reset() | 		// using "--access-token" only means "all" access | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") | 		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) | ||||||
|  |  | ||||||
| 	reset() | 		// using "--access-token" with name & scopes | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) | 		reset() | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) | 		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, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) | 		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) | 		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) | ||||||
| 	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) | 		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") | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user