Skip to content

Commit a32db18

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Feature: Support workflow event dispatch via API (go-gitea#32059) Remove "class-name" from svg icon (go-gitea#33540) Add "No data available" display when list is empty (go-gitea#33517) Add a option "--user-type bot" to admin user create, improve role display (go-gitea#27885)
2 parents 34a84af + 523751d commit a32db18

29 files changed

+1839
-245
lines changed

cmd/admin_user_create.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ var microcmdUserCreate = &cli.Command{
3131
Name: "username",
3232
Usage: "Username",
3333
},
34+
&cli.StringFlag{
35+
Name: "user-type",
36+
Usage: "Set user's type: individual or bot",
37+
Value: "individual",
38+
},
3439
&cli.StringFlag{
3540
Name: "password",
3641
Usage: "User password",
@@ -77,6 +82,22 @@ func runCreateUser(c *cli.Context) error {
7782
return err
7883
}
7984

85+
userTypes := map[string]user_model.UserType{
86+
"individual": user_model.UserTypeIndividual,
87+
"bot": user_model.UserTypeBot,
88+
}
89+
userType, ok := userTypes[c.String("user-type")]
90+
if !ok {
91+
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
92+
}
93+
if userType != user_model.UserTypeIndividual {
94+
// Some other commands like "change-password" also only support individual users.
95+
// It needs to clarify the "password" behavior for bot users in the future.
96+
// At the moment, we do not allow setting password for bot users.
97+
if c.IsSet("password") || c.IsSet("random-password") {
98+
return errors.New("password can only be set for individual users")
99+
}
100+
}
80101
if c.IsSet("name") && c.IsSet("username") {
81102
return errors.New("cannot set both --name and --username flags")
82103
}
@@ -118,16 +139,19 @@ func runCreateUser(c *cli.Context) error {
118139
return err
119140
}
120141
fmt.Printf("generated random password is '%s'\n", password)
121-
} else {
142+
} else if userType == user_model.UserTypeIndividual {
122143
return errors.New("must set either password or random-password flag")
123144
}
124145

125146
isAdmin := c.Bool("admin")
126147
mustChangePassword := true // always default to true
127148
if c.IsSet("must-change-password") {
149+
if userType != user_model.UserTypeIndividual {
150+
return errors.New("must-change-password flag can only be set for individual users")
151+
}
128152
// if the flag is set, use the value provided by the user
129153
mustChangePassword = c.Bool("must-change-password")
130-
} else {
154+
} else if userType == user_model.UserTypeIndividual {
131155
// check whether there are users in the database
132156
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
133157
if err != nil {
@@ -151,8 +175,9 @@ func runCreateUser(c *cli.Context) error {
151175
u := &user_model.User{
152176
Name: username,
153177
Email: c.String("email"),
154-
Passwd: password,
155178
IsAdmin: isAdmin,
179+
Type: userType,
180+
Passwd: password,
156181
MustChangePassword: mustChangePassword,
157182
Visibility: visibility,
158183
}

cmd/admin_user_create_test.go

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,54 @@ import (
1313
user_model "code.gitea.io/gitea/models/user"
1414

1515
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
1617
)
1718

1819
func TestAdminUserCreate(t *testing.T) {
1920
app := NewMainApp(AppVersion{})
2021

2122
reset := func() {
22-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
23-
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
23+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
24+
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
2425
}
2526

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}
31-
}
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"))
27+
t.Run("MustChangePassword", func(t *testing.T) {
28+
type check struct {
29+
IsAdmin bool
30+
MustChangePassword bool
31+
}
32+
createCheck := func(name, args string) check {
33+
require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s --password foobar", name, name, args))))
34+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
35+
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
36+
}
37+
reset()
38+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
39+
40+
reset()
41+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
42+
43+
reset()
44+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
45+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
46+
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
47+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
48+
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
49+
})
50+
51+
t.Run("UserType", func(t *testing.T) {
52+
createUser := func(name, args string) error {
53+
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %[email protected] %s", name, name, args)))
54+
}
55+
56+
reset()
57+
assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
58+
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
59+
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
60+
61+
assert.NoError(t, createUser("u", "--user-type bot"))
62+
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
63+
assert.Equal(t, user_model.UserTypeBot, u.Type)
64+
assert.Equal(t, "", u.Passwd)
65+
})
4466
}

models/user/user.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
385385
}
386386

387387
// IsPasswordSet checks if the password is set or left empty
388+
// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
388389
func (u *User) IsPasswordSet() bool {
389-
return len(u.Passwd) != 0
390+
return u.Passwd != ""
390391
}
391392

392-
// IsOrganization returns true if user is actually a organization.
393+
// IsOrganization returns true if user is actually an organization.
393394
func (u *User) IsOrganization() bool {
394395
return u.Type == UserTypeOrganization
395396
}
@@ -399,13 +400,14 @@ func (u *User) IsIndividual() bool {
399400
return u.Type == UserTypeIndividual
400401
}
401402

402-
func (u *User) IsUser() bool {
403-
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
403+
// IsTypeBot returns whether the user is of type bot
404+
func (u *User) IsTypeBot() bool {
405+
return u.Type == UserTypeBot
404406
}
405407

406-
// IsBot returns whether or not the user is of type bot
407-
func (u *User) IsBot() bool {
408-
return u.Type == UserTypeBot
408+
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
409+
func (u *User) IsTokenAccessAllowed() bool {
410+
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
409411
}
410412

411413
// DisplayName returns full name if it's not empty,

models/user/user_system.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func NewActionsUser() *User {
5656
Email: ActionsUserEmail,
5757
KeepEmailPrivate: true,
5858
LoginName: ActionsUserName,
59-
Type: UserTypeIndividual,
59+
Type: UserTypeBot,
6060
AllowCreateOrganization: true,
6161
Visibility: structs.VisibleTypePublic,
6262
}

modules/structs/repo_actions.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,36 @@ type ActionTaskResponse struct {
3232
Entries []*ActionTask `json:"workflow_runs"`
3333
TotalCount int64 `json:"total_count"`
3434
}
35+
36+
// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event
37+
// swagger:model
38+
type CreateActionWorkflowDispatch struct {
39+
// required: true
40+
// example: refs/heads/main
41+
Ref string `json:"ref" binding:"Required"`
42+
// required: false
43+
Inputs map[string]any `json:"inputs,omitempty"`
44+
}
45+
46+
// ActionWorkflow represents a ActionWorkflow
47+
type ActionWorkflow struct {
48+
ID string `json:"id"`
49+
Name string `json:"name"`
50+
Path string `json:"path"`
51+
State string `json:"state"`
52+
// swagger:strfmt date-time
53+
CreatedAt time.Time `json:"created_at"`
54+
// swagger:strfmt date-time
55+
UpdatedAt time.Time `json:"updated_at"`
56+
URL string `json:"url"`
57+
HTMLURL string `json:"html_url"`
58+
BadgeURL string `json:"badge_url"`
59+
// swagger:strfmt date-time
60+
DeletedAt time.Time `json:"deleted_at,omitempty"`
61+
}
62+
63+
// ActionWorkflowResponse returns a ActionWorkflow
64+
type ActionWorkflowResponse struct {
65+
Workflows []*ActionWorkflow `json:"workflows"`
66+
TotalCount int64 `json:"total_count"`
67+
}

routers/api/v1/api.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,12 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
268268
return
269269
}
270270
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
271-
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
271+
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
272272
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
273273
return
274274
}
275275
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
276-
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
276+
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
277277
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
278278
return
279279
}
@@ -915,6 +915,21 @@ func Routes() *web.Router {
915915
})
916916
}
917917

918+
addActionsWorkflowRoutes := func(
919+
m *web.Router,
920+
actw actions.WorkflowAPI,
921+
) {
922+
m.Group("/actions", func() {
923+
m.Group("/workflows", func() {
924+
m.Get("", reqToken(), actw.ListRepositoryWorkflows)
925+
m.Get("/{workflow_id}", reqToken(), actw.GetWorkflow)
926+
m.Put("/{workflow_id}/disable", reqToken(), reqRepoWriter(unit.TypeActions), actw.DisableWorkflow)
927+
m.Post("/{workflow_id}/dispatches", reqToken(), reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), actw.DispatchWorkflow)
928+
m.Put("/{workflow_id}/enable", reqToken(), reqRepoWriter(unit.TypeActions), actw.EnableWorkflow)
929+
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeActions))
930+
})
931+
}
932+
918933
m.Group("", func() {
919934
// Miscellaneous (no scope required)
920935
if setting.API.EnableSwagger {
@@ -1160,6 +1175,10 @@ func Routes() *web.Router {
11601175
reqOwner(),
11611176
repo.NewAction(),
11621177
)
1178+
addActionsWorkflowRoutes(
1179+
m,
1180+
repo.NewActionWorkflow(),
1181+
)
11631182
m.Group("/hooks/git", func() {
11641183
m.Combo("").Get(repo.ListGitHooks)
11651184
m.Group("/{id}", func() {

0 commit comments

Comments
 (0)