Skip to content

Commit a40e15a

Browse files
wxiaoguangkemzeb
andauthored
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 <[email protected]>
1 parent 8f75f61 commit a40e15a

File tree

4 files changed

+125
-31
lines changed

4 files changed

+125
-31
lines changed

cmd/admin_user_create.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"strings"
1011

1112
auth_model "code.gitea.io/gitea/models/auth"
1213
"code.gitea.io/gitea/models/db"
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
6162
Name: "access-token",
6263
Usage: "Generate access token for the user",
6364
},
65+
&cli.StringFlag{
66+
Name: "access-token-name",
67+
Usage: `Name of the generated access token`,
68+
Value: "gitea-admin",
69+
},
70+
&cli.StringFlag{
71+
Name: "access-token-scopes",
72+
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
73+
Value: "all",
74+
},
6475
&cli.BoolFlag{
6576
Name: "restricted",
6677
Usage: "Make a restricted user account",
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
162173
IsRestricted: restricted,
163174
}
164175

165-
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
166-
return fmt.Errorf("CreateUser: %w", err)
176+
var accessTokenName string
177+
var accessTokenScope auth_model.AccessTokenScope
178+
if c.IsSet("access-token") {
179+
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
180+
if accessTokenName == "" {
181+
return errors.New("access-token-name cannot be empty")
182+
}
183+
var err error
184+
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
185+
if err != nil {
186+
return fmt.Errorf("invalid access token scope provided: %w", err)
187+
}
188+
if !accessTokenScope.HasPermissionScope() {
189+
return errors.New("access token does not have any permission")
190+
}
191+
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
192+
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
167193
}
168194

169-
if c.Bool("access-token") {
170-
t := &auth_model.AccessToken{
171-
Name: "gitea-admin",
172-
UID: u.ID,
173-
}
195+
// arguments should be prepared before creating the user & access token, in case there is anything wrong
174196

197+
// create the user
198+
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
199+
return fmt.Errorf("CreateUser: %w", err)
200+
}
201+
// create the access token
202+
if accessTokenScope != "" {
203+
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
175204
if err := auth_model.NewAccessToken(ctx, t); err != nil {
176205
return err
177206
}
178-
179207
fmt.Printf("Access token was successfully created... %s\n", t.Token)
180208
}
181-
182209
fmt.Printf("New user '%s' has been successfully created!\n", username)
183210
return nil
184211
}

cmd/admin_user_create_test.go

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,97 @@ import (
88
"strings"
99
"testing"
1010

11+
auth_model "code.gitea.io/gitea/models/auth"
1112
"code.gitea.io/gitea/models/db"
1213
"code.gitea.io/gitea/models/unittest"
1314
user_model "code.gitea.io/gitea/models/user"
1415

1516
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
1618
)
1719

1820
func TestAdminUserCreate(t *testing.T) {
1921
app := NewMainApp(AppVersion{})
2022

2123
reset := func() {
22-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
23-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
24+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
25+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
26+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
2427
}
28+
t.Run("MustChangePassword", func(t *testing.T) {
29+
type check struct{ IsAdmin, MustChangePassword bool }
30+
createCheck := func(name, args string) check {
31+
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s --password foobar", name, name, args))))
32+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
33+
return check{u.IsAdmin, u.MustChangePassword}
34+
}
35+
reset()
36+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
2537

26-
type createCheck struct{ IsAdmin, MustChangePassword bool }
27-
createUser := func(name, args string) createCheck {
28-
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s --password foobar", name, name, args))))
29-
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
30-
return createCheck{u.IsAdmin, u.MustChangePassword}
38+
reset()
39+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
40+
41+
reset()
42+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
43+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
44+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
45+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
46+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
47+
})
48+
49+
createUser := func(name, args string) error {
50+
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s", name, name, args)))
3151
}
32-
reset()
33-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
34-
35-
reset()
36-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
37-
38-
reset()
39-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
40-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
41-
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
42-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
43-
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
52+
53+
t.Run("AccessToken", func(t *testing.T) {
54+
// no generated access token
55+
reset()
56+
assert.NoError(t, createUser("u", "--random-password"))
57+
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
58+
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
59+
60+
// using "--access-token" only means "all" access
61+
reset()
62+
assert.NoError(t, createUser("u", "--random-password --access-token"))
63+
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
64+
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
65+
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
66+
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
67+
assert.NoError(t, err)
68+
assert.True(t, hasScopes)
69+
70+
// using "--access-token" with name & scopes
71+
reset()
72+
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
73+
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
74+
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
75+
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
76+
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
77+
assert.NoError(t, err)
78+
assert.True(t, hasScopes)
79+
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
80+
assert.NoError(t, err)
81+
assert.False(t, hasScopes)
82+
83+
// using "--access-token-name" without "--access-token"
84+
reset()
85+
err = createUser("u", "--random-password --access-token-name new-token-name")
86+
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
87+
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
88+
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
89+
90+
// using "--access-token-scopes" without "--access-token"
91+
reset()
92+
err = createUser("u", "--random-password --access-token-scopes read:issue")
93+
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
94+
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
95+
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
96+
97+
// empty permission
98+
reset()
99+
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
100+
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
101+
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
102+
assert.ErrorContains(t, err, "access token does not have any permission")
103+
})
44104
}

cmd/admin_user_generate_access_token.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@ var microcmdUserGenerateAccessToken = &cli.Command{
3434
},
3535
&cli.StringFlag{
3636
Name: "scopes",
37-
Value: "",
38-
Usage: "Comma separated list of scopes to apply to access token",
37+
Value: "all",
38+
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
3939
},
4040
},
4141
Action: runGenerateAccessToken,
4242
}
4343

4444
func runGenerateAccessToken(c *cli.Context) error {
4545
if !c.IsSet("username") {
46-
return errors.New("You must provide a username to generate a token for")
46+
return errors.New("you must provide a username to generate a token for")
4747
}
4848

4949
ctx, cancel := installSignals()
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
7777
if err != nil {
7878
return fmt.Errorf("invalid access token scope provided: %w", err)
7979
}
80+
if !accessTokenScope.HasPermissionScope() {
81+
return errors.New("access token does not have any permission")
82+
}
8083
t.Scope = accessTokenScope
8184

8285
// create the token

models/auth/access_token_scope.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
283283
return bitmap.toScope(), nil
284284
}
285285

286+
func (s AccessTokenScope) HasPermissionScope() bool {
287+
return s != "" && s != AccessTokenScopePublicOnly
288+
}
289+
286290
// PublicOnly checks if this token scope is limited to public resources
287291
func (s AccessTokenScope) PublicOnly() (bool, error) {
288292
bitmap, err := s.parse()

0 commit comments

Comments
 (0)