mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 06:03:51 +03:00
Merge pull request '[gitea] week 2024-39 cherry pick (gitea/main -> forgejo)' (#5372) from earl-warren/wcp/2024-39 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5372 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
89d9307d56
27 changed files with 229 additions and 244 deletions
|
@ -529,7 +529,8 @@ INTERNAL_TOKEN =
|
||||||
;; HMAC to encode urls with, it **is required** if camo is enabled.
|
;; HMAC to encode urls with, it **is required** if camo is enabled.
|
||||||
;HMAC_KEY =
|
;HMAC_KEY =
|
||||||
;; Set to true to use camo for https too lese only non https urls are proxyed
|
;; Set to true to use camo for https too lese only non https urls are proxyed
|
||||||
;ALLWAYS = false
|
;; ALLWAYS is deprecated and will be removed in the future
|
||||||
|
;ALWAYS = false
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -34,6 +34,7 @@ type ActivityStats struct {
|
||||||
OpenedPRAuthorCount int64
|
OpenedPRAuthorCount int64
|
||||||
MergedPRs issues_model.PullRequestList
|
MergedPRs issues_model.PullRequestList
|
||||||
MergedPRAuthorCount int64
|
MergedPRAuthorCount int64
|
||||||
|
ActiveIssues issues_model.IssueList
|
||||||
OpenedIssues issues_model.IssueList
|
OpenedIssues issues_model.IssueList
|
||||||
OpenedIssueAuthorCount int64
|
OpenedIssueAuthorCount int64
|
||||||
ClosedIssues issues_model.IssueList
|
ClosedIssues issues_model.IssueList
|
||||||
|
@ -172,7 +173,7 @@ func (stats *ActivityStats) MergedPRPerc() int {
|
||||||
|
|
||||||
// ActiveIssueCount returns total active issue count
|
// ActiveIssueCount returns total active issue count
|
||||||
func (stats *ActivityStats) ActiveIssueCount() int {
|
func (stats *ActivityStats) ActiveIssueCount() int {
|
||||||
return stats.OpenedIssueCount() + stats.ClosedIssueCount()
|
return len(stats.ActiveIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenedIssueCount returns open issue count
|
// OpenedIssueCount returns open issue count
|
||||||
|
@ -285,13 +286,21 @@ func (stats *ActivityStats) FillIssues(ctx context.Context, repoID int64, fromTi
|
||||||
stats.ClosedIssueAuthorCount = count
|
stats.ClosedIssueAuthorCount = count
|
||||||
|
|
||||||
// New issues
|
// New issues
|
||||||
sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false)
|
sess = newlyCreatedIssues(ctx, repoID, fromTime)
|
||||||
sess.OrderBy("issue.created_unix ASC")
|
sess.OrderBy("issue.created_unix ASC")
|
||||||
stats.OpenedIssues = make(issues_model.IssueList, 0)
|
stats.OpenedIssues = make(issues_model.IssueList, 0)
|
||||||
if err = sess.Find(&stats.OpenedIssues); err != nil {
|
if err = sess.Find(&stats.OpenedIssues); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active issues
|
||||||
|
sess = activeIssues(ctx, repoID, fromTime)
|
||||||
|
sess.OrderBy("issue.created_unix ASC")
|
||||||
|
stats.ActiveIssues = make(issues_model.IssueList, 0)
|
||||||
|
if err = sess.Find(&stats.ActiveIssues); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Opened issue authors
|
// Opened issue authors
|
||||||
sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false)
|
sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false)
|
||||||
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
|
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
|
||||||
|
@ -317,6 +326,23 @@ func (stats *ActivityStats) FillUnresolvedIssues(ctx context.Context, repoID int
|
||||||
return sess.Find(&stats.UnresolvedIssues)
|
return sess.Find(&stats.UnresolvedIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
||||||
|
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||||
|
And("issue.is_pull = ?", false). // Retain the is_pull check to exclude pull requests
|
||||||
|
And("issue.created_unix >= ?", fromTime.Unix()) // Include all issues created after fromTime
|
||||||
|
|
||||||
|
return sess
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
||||||
|
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||||
|
And("issue.is_pull = ?", false).
|
||||||
|
And("issue.created_unix >= ?", fromTime.Unix()).
|
||||||
|
Or("issue.closed_unix >= ?", fromTime.Unix())
|
||||||
|
|
||||||
|
return sess
|
||||||
|
}
|
||||||
|
|
||||||
func issuesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session {
|
func issuesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session {
|
||||||
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||||
And("issue.is_closed = ?", closed)
|
And("issue.is_closed = ?", closed)
|
||||||
|
|
|
@ -76,7 +76,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
|
||||||
w.Header().Set("Etag", etag)
|
w.Header().Set("Etag", etag)
|
||||||
}
|
}
|
||||||
if lastModified != nil && !lastModified.IsZero() {
|
if lastModified != nil && !lastModified.IsZero() {
|
||||||
w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
|
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||||
|
w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(etag) > 0 {
|
if len(etag) > 0 {
|
||||||
|
|
|
@ -79,6 +79,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||||
httpcache.SetCacheControlInHeader(header, duration)
|
httpcache.SetCacheControlInHeader(header, duration)
|
||||||
|
|
||||||
if !opts.LastModified.IsZero() {
|
if !opts.LastModified.IsZero() {
|
||||||
|
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ func camoHandleLink(link string) string {
|
||||||
if setting.Camo.Enabled {
|
if setting.Camo.Enabled {
|
||||||
lnkURL, err := url.Parse(link)
|
lnkURL, err := url.Parse(link)
|
||||||
if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
|
if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
|
||||||
(setting.Camo.Allways || lnkURL.Scheme != "https") {
|
(setting.Camo.Always || lnkURL.Scheme != "https") {
|
||||||
return CamoEncode(link)
|
return CamoEncode(link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func TestCamoHandleLink(t *testing.T) {
|
||||||
"https://image.proxy/eivin43gJwGVIjR9MiYYtFIk0mw/aHR0cDovL3Rlc3RpbWFnZXMub3JnL2ltZy5qcGc",
|
"https://image.proxy/eivin43gJwGVIjR9MiYYtFIk0mw/aHR0cDovL3Rlc3RpbWFnZXMub3JnL2ltZy5qcGc",
|
||||||
camoHandleLink("http://testimages.org/img.jpg"))
|
camoHandleLink("http://testimages.org/img.jpg"))
|
||||||
|
|
||||||
setting.Camo.Allways = true
|
setting.Camo.Always = true
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
"https://gitea.com/img.jpg",
|
"https://gitea.com/img.jpg",
|
||||||
camoHandleLink("https://gitea.com/img.jpg"))
|
camoHandleLink("https://gitea.com/img.jpg"))
|
||||||
|
|
|
@ -48,6 +48,7 @@ type Metadata struct {
|
||||||
Homepage string `json:"homepage,omitempty"`
|
Homepage string `json:"homepage,omitempty"`
|
||||||
License Licenses `json:"license,omitempty"`
|
License Licenses `json:"license,omitempty"`
|
||||||
Authors []Author `json:"authors,omitempty"`
|
Authors []Author `json:"authors,omitempty"`
|
||||||
|
Bin []string `json:"bin,omitempty"`
|
||||||
Autoload map[string]any `json:"autoload,omitempty"`
|
Autoload map[string]any `json:"autoload,omitempty"`
|
||||||
AutoloadDev map[string]any `json:"autoload-dev,omitempty"`
|
AutoloadDev map[string]any `json:"autoload-dev,omitempty"`
|
||||||
Extra map[string]any `json:"extra,omitempty"`
|
Extra map[string]any `json:"extra,omitempty"`
|
||||||
|
|
|
@ -3,18 +3,28 @@
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
|
|
||||||
import "code.gitea.io/gitea/modules/log"
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
var Camo = struct {
|
var Camo = struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ServerURL string `ini:"SERVER_URL"`
|
ServerURL string `ini:"SERVER_URL"`
|
||||||
HMACKey string `ini:"HMAC_KEY"`
|
HMACKey string `ini:"HMAC_KEY"`
|
||||||
Allways bool
|
Always bool
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
func loadCamoFrom(rootCfg ConfigProvider) {
|
func loadCamoFrom(rootCfg ConfigProvider) {
|
||||||
mustMapSetting(rootCfg, "camo", &Camo)
|
mustMapSetting(rootCfg, "camo", &Camo)
|
||||||
if Camo.Enabled {
|
if Camo.Enabled {
|
||||||
|
oldValue := rootCfg.Section("camo").Key("ALLWAYS").MustString("")
|
||||||
|
if oldValue != "" {
|
||||||
|
log.Warn("camo.ALLWAYS is deprecated, use camo.ALWAYS instead")
|
||||||
|
Camo.Always, _ = strconv.ParseBool(oldValue)
|
||||||
|
}
|
||||||
|
|
||||||
if Camo.ServerURL == "" || Camo.HMACKey == "" {
|
if Camo.ServerURL == "" || Camo.HMACKey == "" {
|
||||||
log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`)
|
log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML {
|
||||||
name = "avatar"
|
name = "avatar"
|
||||||
}
|
}
|
||||||
|
|
||||||
return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
return template.HTML(`<img loading="lazy" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avatar renders user avatars. args: user, size (int), class (string)
|
// Avatar renders user avatars. args: user, size (int), class (string)
|
||||||
|
|
|
@ -225,6 +225,15 @@ func Iif[T any](condition bool, trueVal, falseVal T) T {
|
||||||
return falseVal
|
return falseVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IfZero returns "def" if "v" is a zero value, otherwise "v"
|
||||||
|
func IfZero[T comparable](v, def T) T {
|
||||||
|
var zero T
|
||||||
|
if v == zero {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func ReserveLineBreakForTextarea(input string) string {
|
func ReserveLineBreakForTextarea(input string) string {
|
||||||
// Since the content is from a form which is a textarea, the line endings are \r\n.
|
// Since the content is from a form which is a textarea, the line endings are \r\n.
|
||||||
// It's a standard behavior of HTML.
|
// It's a standard behavior of HTML.
|
||||||
|
|
|
@ -231,7 +231,6 @@ string.desc = Z - A
|
||||||
[error]
|
[error]
|
||||||
occurred = An error occurred
|
occurred = An error occurred
|
||||||
report_message = If you believe that this is a Forgejo bug, please search for issues on <a href="%s" target="_blank">Codeberg</a> or open a new issue if necessary.
|
report_message = If you believe that this is a Forgejo bug, please search for issues on <a href="%s" target="_blank">Codeberg</a> or open a new issue if necessary.
|
||||||
invalid_csrf = Bad Request: invalid CSRF token
|
|
||||||
not_found = The target couldn't be found.
|
not_found = The target couldn't be found.
|
||||||
network_error = Network error
|
network_error = Network error
|
||||||
server_internal = Internal server error
|
server_internal = Internal server error
|
||||||
|
|
5
release-notes/5372.md
Normal file
5
release-notes/5372.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/9d3473119893ffde0ab36d98e7a0e41c5d0ba9a3) Add bin to Composer Metadata.
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/f709de24039ab7e605d3e09e3b61240836381603) Fix wrong last modify time.
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2675a24649af2fff34f5c7e416d6ff78591d8d9c) Repo Activity: count new issues that were closed.
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/526054332acb221e061d3900bba2dc6e012da52d) Fix incorrect /tokens api.
|
||||||
|
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/0cafec4c7a2faf810953e9d522faf5dc019e1522) Do not escape relative path in RPM primary index.
|
|
@ -117,7 +117,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
|
||||||
xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
|
xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
|
||||||
|
|
||||||
latest := pds[len(pds)-1]
|
latest := pds[len(pds)-1]
|
||||||
ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat))
|
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||||
|
lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat)
|
||||||
|
ctx.Resp.Header().Set("Last-Modified", lastModifed)
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(params.Filename))
|
ext := strings.ToLower(filepath.Ext(params.Filename))
|
||||||
if isChecksumExtension(ext) {
|
if isChecksumExtension(ext) {
|
||||||
|
|
|
@ -839,11 +839,17 @@ func EditIssue(ctx *context.APIContext) {
|
||||||
if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
|
if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
|
||||||
var deadlineUnix timeutil.TimeStamp
|
var deadlineUnix timeutil.TimeStamp
|
||||||
|
|
||||||
if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
|
if form.RemoveDeadline == nil || !*form.RemoveDeadline {
|
||||||
|
if form.Deadline == nil {
|
||||||
|
ctx.Error(http.StatusBadRequest, "", "The due_date cannot be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !form.Deadline.IsZero() {
|
||||||
deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
|
deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
|
||||||
23, 59, 59, 0, form.Deadline.Location())
|
23, 59, 59, 0, form.Deadline.Location())
|
||||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
|
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
|
||||||
|
|
|
@ -118,6 +118,10 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||||
ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err))
|
ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if scope == "" {
|
||||||
|
ctx.Error(http.StatusBadRequest, "AccessTokenScope", "access token must have a scope")
|
||||||
|
return
|
||||||
|
}
|
||||||
t.Scope = scope
|
t.Scope = scope
|
||||||
|
|
||||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||||
|
@ -129,6 +133,7 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||||
Token: t.Token,
|
Token: t.Token,
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
TokenLastEight: t.TokenLastEight,
|
TokenLastEight: t.TokenLastEight,
|
||||||
|
Scopes: t.Scope.StringSlice(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -395,7 +395,8 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
|
||||||
|
|
||||||
ctx.Resp.Header().Set("Content-Type", contentType)
|
ctx.Resp.Header().Set("Content-Type", contentType)
|
||||||
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
||||||
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
|
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||||
|
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
|
||||||
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
|
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,8 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
|
||||||
// ensure the session uid is deleted
|
// ensure the session uid is deleted
|
||||||
_ = ctx.Session.Delete("uid")
|
_ = ctx.Session.Delete("uid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,10 +127,8 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
csrfOpts := CsrfOptions{
|
csrfOpts := CsrfOptions{
|
||||||
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
||||||
Cookie: setting.CSRFCookieName,
|
Cookie: setting.CSRFCookieName,
|
||||||
SetCookie: true,
|
|
||||||
Secure: setting.SessionConfig.Secure,
|
Secure: setting.SessionConfig.Secure,
|
||||||
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
||||||
Header: "X-Csrf-Token",
|
|
||||||
CookieDomain: setting.SessionConfig.Domain,
|
CookieDomain: setting.SessionConfig.Domain,
|
||||||
CookiePath: setting.SessionConfig.CookiePath,
|
CookiePath: setting.SessionConfig.CookiePath,
|
||||||
SameSite: setting.SessionConfig.SameSite,
|
SameSite: setting.SessionConfig.SameSite,
|
||||||
|
@ -156,7 +154,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
ctx.Base.AppendContextValue(WebContextKey, ctx)
|
ctx.Base.AppendContextValue(WebContextKey, ctx)
|
||||||
ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
||||||
|
|
||||||
ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx)
|
ctx.Csrf = NewCSRFProtector(csrfOpts)
|
||||||
|
|
||||||
// Get the last flash message from cookie
|
// Get the last flash message from cookie
|
||||||
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
||||||
|
@ -193,8 +191,6 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||||
|
|
||||||
ctx.Data["SystemConfig"] = setting.Config()
|
ctx.Data["SystemConfig"] = setting.Config()
|
||||||
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
|
|
||||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
|
||||||
|
|
||||||
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
||||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||||
|
|
|
@ -20,64 +20,43 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base32"
|
"html/template"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CsrfHeaderName = "X-Csrf-Token"
|
||||||
|
CsrfFormName = "_csrf"
|
||||||
|
CsrfErrorString = "Invalid CSRF token."
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
||||||
type CSRFProtector interface {
|
type CSRFProtector interface {
|
||||||
// GetHeaderName returns HTTP header to search for token.
|
// PrepareForSessionUser prepares the csrf protector for the current session user.
|
||||||
GetHeaderName() string
|
PrepareForSessionUser(ctx *Context)
|
||||||
// GetFormName returns form value to search for token.
|
// Validate validates the csrf token in http context.
|
||||||
GetFormName() string
|
|
||||||
// GetToken returns the token.
|
|
||||||
GetToken() string
|
|
||||||
// Validate validates the token in http context.
|
|
||||||
Validate(ctx *Context)
|
Validate(ctx *Context)
|
||||||
// DeleteCookie deletes the cookie
|
// DeleteCookie deletes the csrf cookie
|
||||||
DeleteCookie(ctx *Context)
|
DeleteCookie(ctx *Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type csrfProtector struct {
|
type csrfProtector struct {
|
||||||
opt CsrfOptions
|
opt CsrfOptions
|
||||||
// Token generated to pass via header, cookie, or hidden form value.
|
// id must be unique per user.
|
||||||
Token string
|
id string
|
||||||
// This value must be unique per user.
|
// token is the valid one which wil be used by end user and passed via header, cookie, or hidden form value.
|
||||||
ID string
|
token string
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaderName returns the name of the HTTP header for csrf token.
|
|
||||||
func (c *csrfProtector) GetHeaderName() string {
|
|
||||||
return c.opt.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFormName returns the name of the form value for csrf token.
|
|
||||||
func (c *csrfProtector) GetFormName() string {
|
|
||||||
return c.opt.Form
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToken returns the current token. This is typically used
|
|
||||||
// to populate a hidden form in an HTML template.
|
|
||||||
func (c *csrfProtector) GetToken() string {
|
|
||||||
return c.Token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CsrfOptions maintains options to manage behavior of Generate.
|
// CsrfOptions maintains options to manage behavior of Generate.
|
||||||
type CsrfOptions struct {
|
type CsrfOptions struct {
|
||||||
// The global secret value used to generate Tokens.
|
// The global secret value used to generate Tokens.
|
||||||
Secret string
|
Secret string
|
||||||
// HTTP header used to set and get token.
|
|
||||||
Header string
|
|
||||||
// Form value used to set and get token.
|
|
||||||
Form string
|
|
||||||
// Cookie value used to set and get token.
|
// Cookie value used to set and get token.
|
||||||
Cookie string
|
Cookie string
|
||||||
// Cookie domain.
|
// Cookie domain.
|
||||||
|
@ -87,103 +66,64 @@ type CsrfOptions struct {
|
||||||
CookieHTTPOnly bool
|
CookieHTTPOnly bool
|
||||||
// SameSite set the cookie SameSite type
|
// SameSite set the cookie SameSite type
|
||||||
SameSite http.SameSite
|
SameSite http.SameSite
|
||||||
// Key used for getting the unique ID per user.
|
|
||||||
SessionKey string
|
|
||||||
// oldSessionKey saves old value corresponding to SessionKey.
|
|
||||||
oldSessionKey string
|
|
||||||
// If true, send token via X-Csrf-Token header.
|
|
||||||
SetHeader bool
|
|
||||||
// If true, send token via _csrf cookie.
|
|
||||||
SetCookie bool
|
|
||||||
// Set the Secure flag to true on the cookie.
|
// Set the Secure flag to true on the cookie.
|
||||||
Secure bool
|
Secure bool
|
||||||
// Disallow Origin appear in request header.
|
// sessionKey is the key used for getting the unique ID per user.
|
||||||
Origin bool
|
sessionKey string
|
||||||
// Cookie lifetime. Default is 0
|
// oldSessionKey saves old value corresponding to sessionKey.
|
||||||
CookieLifeTime int
|
oldSessionKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions {
|
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
|
||||||
if opt.Secret == "" {
|
|
||||||
randBytes, err := util.CryptoRandomBytes(8)
|
|
||||||
if err != nil {
|
|
||||||
// this panic can be handled by the recover() in http handlers
|
|
||||||
panic(fmt.Errorf("failed to generate random bytes: %w", err))
|
|
||||||
}
|
|
||||||
opt.Secret = base32.StdEncoding.EncodeToString(randBytes)
|
|
||||||
}
|
|
||||||
if opt.Header == "" {
|
|
||||||
opt.Header = "X-Csrf-Token"
|
|
||||||
}
|
|
||||||
if opt.Form == "" {
|
|
||||||
opt.Form = "_csrf"
|
|
||||||
}
|
|
||||||
if opt.Cookie == "" {
|
|
||||||
opt.Cookie = "_csrf"
|
|
||||||
}
|
|
||||||
if opt.CookiePath == "" {
|
|
||||||
opt.CookiePath = "/"
|
|
||||||
}
|
|
||||||
if opt.SessionKey == "" {
|
|
||||||
opt.SessionKey = "uid"
|
|
||||||
}
|
|
||||||
if opt.CookieLifeTime == 0 {
|
|
||||||
opt.CookieLifeTime = int(CsrfTokenTimeout.Seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
opt.oldSessionKey = "_old_" + opt.SessionKey
|
|
||||||
return opt
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCsrfCookie(c *csrfProtector, value string) *http.Cookie {
|
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: c.opt.Cookie,
|
Name: opt.Cookie,
|
||||||
Value: value,
|
Value: value,
|
||||||
Path: c.opt.CookiePath,
|
Path: opt.CookiePath,
|
||||||
Domain: c.opt.CookieDomain,
|
Domain: opt.CookieDomain,
|
||||||
MaxAge: c.opt.CookieLifeTime,
|
MaxAge: int(CsrfTokenTimeout.Seconds()),
|
||||||
Secure: c.opt.Secure,
|
Secure: opt.Secure,
|
||||||
HttpOnly: c.opt.CookieHTTPOnly,
|
HttpOnly: opt.CookieHTTPOnly,
|
||||||
SameSite: c.opt.SameSite,
|
SameSite: opt.SameSite,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareCSRFProtector returns a CSRFProtector to be used for every request.
|
func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
|
||||||
// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie.
|
if opt.Secret == "" {
|
||||||
func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
|
panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
|
||||||
opt = prepareDefaultCsrfOptions(opt)
|
}
|
||||||
x := &csrfProtector{opt: opt}
|
opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
|
||||||
|
opt.CookiePath = util.IfZero(opt.CookiePath, "/")
|
||||||
if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 {
|
opt.sessionKey = "uid"
|
||||||
return x
|
opt.oldSessionKey = "_old_" + opt.sessionKey
|
||||||
|
return &csrfProtector{opt: opt}
|
||||||
}
|
}
|
||||||
|
|
||||||
x.ID = "0"
|
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
|
||||||
uidAny := ctx.Session.Get(opt.SessionKey)
|
c.id = "0"
|
||||||
if uidAny != nil {
|
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
|
||||||
switch uidVal := uidAny.(type) {
|
switch uidVal := uidAny.(type) {
|
||||||
case string:
|
case string:
|
||||||
x.ID = uidVal
|
c.id = uidVal
|
||||||
case int64:
|
case int64:
|
||||||
x.ID = strconv.FormatInt(uidVal, 10)
|
c.id = strconv.FormatInt(uidVal, 10)
|
||||||
default:
|
default:
|
||||||
log.Error("invalid uid type in session: %T", uidAny)
|
log.Error("invalid uid type in session: %T", uidAny)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
oldUID := ctx.Session.Get(opt.oldSessionKey)
|
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
|
||||||
uidChanged := oldUID == nil || oldUID.(string) != x.ID
|
uidChanged := oldUID == nil || oldUID.(string) != c.id
|
||||||
cookieToken := ctx.GetSiteCookie(opt.Cookie)
|
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
|
||||||
|
|
||||||
needsNew := true
|
needsNew := true
|
||||||
if uidChanged {
|
if uidChanged {
|
||||||
_ = ctx.Session.Set(opt.oldSessionKey, x.ID)
|
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
|
||||||
} else if cookieToken != "" {
|
} else if cookieToken != "" {
|
||||||
// If cookie token presents, reuse existing unexpired token, else generate a new one.
|
// If cookie token presents, reuse existing unexpired token, else generate a new one.
|
||||||
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
||||||
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
||||||
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
||||||
x.Token = cookieToken
|
c.token = cookieToken
|
||||||
needsNew = false
|
needsNew = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,42 +131,33 @@ func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
|
||||||
|
|
||||||
if needsNew {
|
if needsNew {
|
||||||
// FIXME: actionId.
|
// FIXME: actionId.
|
||||||
x.Token = GenerateCsrfToken(x.opt.Secret, x.ID, "POST", time.Now())
|
c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
|
||||||
if opt.SetCookie {
|
cookie := newCsrfCookie(&c.opt, c.token)
|
||||||
cookie := newCsrfCookie(x, x.Token)
|
|
||||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if opt.SetHeader {
|
ctx.Data["CsrfToken"] = c.token
|
||||||
ctx.Resp.Header().Add(opt.Header, x.Token)
|
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
||||||
if !ValidCsrfToken(token, c.opt.Secret, c.ID, "POST", time.Now()) {
|
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
|
||||||
c.DeleteCookie(ctx)
|
c.DeleteCookie(ctx)
|
||||||
if middleware.IsAPIPath(ctx.Req) {
|
|
||||||
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
||||||
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
|
// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
|
||||||
} else {
|
http.Error(ctx.Resp, CsrfErrorString, http.StatusBadRequest)
|
||||||
ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
|
|
||||||
ctx.Redirect(setting.AppSubURL + "/")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
||||||
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
||||||
// If this validation fails, custom Error is sent in the reply.
|
// If this validation fails, http.StatusBadRequest is sent.
|
||||||
// If neither a header nor form value is found, http.StatusBadRequest is sent.
|
|
||||||
func (c *csrfProtector) Validate(ctx *Context) {
|
func (c *csrfProtector) Validate(ctx *Context) {
|
||||||
if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" {
|
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
|
||||||
c.validateToken(ctx, token)
|
c.validateToken(ctx, token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if token := ctx.Req.FormValue(c.GetFormName()); token != "" {
|
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
|
||||||
c.validateToken(ctx, token)
|
c.validateToken(ctx, token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -234,9 +165,7 @@ func (c *csrfProtector) Validate(ctx *Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
||||||
if c.opt.SetCookie {
|
cookie := newCsrfCookie(&c.opt, "")
|
||||||
cookie := newCsrfCookie(c, "")
|
|
||||||
cookie.MaxAge = -1
|
cookie.MaxAge = -1
|
||||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -440,7 +439,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
|
||||||
Archive: pd.FileMetadata.ArchiveSize,
|
Archive: pd.FileMetadata.ArchiveSize,
|
||||||
},
|
},
|
||||||
Location: Location{
|
Location: Location{
|
||||||
Href: fmt.Sprintf("package/%s/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(packageVersion), url.PathEscape(pd.FileMetadata.Architecture), url.PathEscape(fmt.Sprintf("%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture))),
|
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
|
||||||
},
|
},
|
||||||
Format: Format{
|
Format: Format{
|
||||||
License: pd.VersionMetadata.License,
|
License: pd.VersionMetadata.License,
|
||||||
|
|
|
@ -37,6 +37,7 @@ func TestPackageComposer(t *testing.T) {
|
||||||
packageType := "composer-plugin"
|
packageType := "composer-plugin"
|
||||||
packageAuthor := "Gitea Authors"
|
packageAuthor := "Gitea Authors"
|
||||||
packageLicense := "MIT"
|
packageLicense := "MIT"
|
||||||
|
packageBin := "./bin/script"
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
archive := zip.NewWriter(&buf)
|
archive := zip.NewWriter(&buf)
|
||||||
|
@ -50,6 +51,9 @@ func TestPackageComposer(t *testing.T) {
|
||||||
{
|
{
|
||||||
"name": "` + packageAuthor + `"
|
"name": "` + packageAuthor + `"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"bin": [
|
||||||
|
"` + packageBin + `"
|
||||||
]
|
]
|
||||||
}`))
|
}`))
|
||||||
archive.Close()
|
archive.Close()
|
||||||
|
@ -211,6 +215,8 @@ func TestPackageComposer(t *testing.T) {
|
||||||
assert.Len(t, pkgs[0].Authors, 1)
|
assert.Len(t, pkgs[0].Authors, 1)
|
||||||
assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name)
|
assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name)
|
||||||
assert.Equal(t, "zip", pkgs[0].Dist.Type)
|
assert.Equal(t, "zip", pkgs[0].Dist.Type)
|
||||||
assert.Equal(t, "7b40bfd6da811b2b78deec1e944f156dbb2c747b", pkgs[0].Dist.Checksum)
|
assert.Equal(t, "4f5fa464c3cb808a1df191dbf6cb75363f8b7072", pkgs[0].Dist.Checksum)
|
||||||
|
assert.Len(t, pkgs[0].Bin, 1)
|
||||||
|
assert.Equal(t, packageBin, pkgs[0].Bin[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
|
||||||
newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, nil)
|
newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||||
deleteAPIAccessToken(t, newAccessToken, user)
|
deleteAPIAccessToken(t, newAccessToken, user)
|
||||||
|
|
||||||
newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, nil)
|
newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||||
deleteAPIAccessToken(t, newAccessToken, user)
|
deleteAPIAccessToken(t, newAccessToken, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,19 +72,19 @@ func TestAPIDeleteTokensPermission(t *testing.T) {
|
||||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
|
||||||
// admin can delete tokens for other users
|
// admin can delete tokens for other users
|
||||||
createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, nil)
|
createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||||
req := NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-1").
|
req := NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-1").
|
||||||
AddBasicAuth(admin.Name)
|
AddBasicAuth(admin.Name)
|
||||||
MakeRequest(t, req, http.StatusNoContent)
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
// non-admin can delete tokens for himself
|
// non-admin can delete tokens for himself
|
||||||
createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, nil)
|
createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||||
req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-2").
|
req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-2").
|
||||||
AddBasicAuth(user2.Name)
|
AddBasicAuth(user2.Name)
|
||||||
MakeRequest(t, req, http.StatusNoContent)
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
// non-admin can't delete tokens for other users
|
// non-admin can't delete tokens for other users
|
||||||
createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, nil)
|
createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||||
req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-3").
|
req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-3").
|
||||||
AddBasicAuth(user4.Name)
|
AddBasicAuth(user4.Name)
|
||||||
MakeRequest(t, req, http.StatusForbidden)
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
@ -520,7 +520,7 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
|
||||||
unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
|
unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, &unauthorizedScopes)
|
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes)
|
||||||
defer deleteAPIAccessToken(t, accessToken, user)
|
defer deleteAPIAccessToken(t, accessToken, user)
|
||||||
|
|
||||||
// Request the endpoint. Verify that permission is denied.
|
// Request the endpoint. Verify that permission is denied.
|
||||||
|
@ -532,20 +532,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
|
||||||
|
|
||||||
// createAPIAccessTokenWithoutCleanUp Create an API access token and assert that
|
// createAPIAccessTokenWithoutCleanUp Create an API access token and assert that
|
||||||
// creation succeeded. The caller is responsible for deleting the token.
|
// creation succeeded. The caller is responsible for deleting the token.
|
||||||
func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes *[]auth_model.AccessTokenScope) api.AccessToken {
|
func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes []auth_model.AccessTokenScope) api.AccessToken {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"name": tokenName,
|
"name": tokenName,
|
||||||
|
"scopes": scopes,
|
||||||
}
|
}
|
||||||
if scopes != nil {
|
|
||||||
for _, scope := range *scopes {
|
|
||||||
scopes, scopesExists := payload["scopes"].([]string)
|
|
||||||
if !scopesExists {
|
|
||||||
scopes = make([]string, 0)
|
|
||||||
}
|
|
||||||
scopes = append(scopes, string(scope))
|
|
||||||
payload["scopes"] = scopes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Debug("Requesting creation of token with scopes: %v", scopes)
|
log.Debug("Requesting creation of token with scopes: %v", scopes)
|
||||||
req := NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload).
|
req := NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
|
@ -563,8 +555,7 @@ func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *us
|
||||||
return newAccessToken
|
return newAccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// createAPIAccessTokenWithoutCleanUp Delete an API access token and assert that
|
// deleteAPIAccessToken deletes an API access token and assert that deletion succeeded.
|
||||||
// deletion succeeded.
|
|
||||||
func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) {
|
func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) {
|
||||||
req := NewRequestf(t, "DELETE", "/api/v1/users/"+user.LoginName+"/tokens/%d", accessToken.ID).
|
req := NewRequestf(t, "DELETE", "/api/v1/users/"+user.LoginName+"/tokens/%d", accessToken.ID).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
|
|
|
@ -60,7 +60,8 @@ func createAttachment(t *testing.T, session *TestSession, repoURL, filename stri
|
||||||
func TestCreateAnonymousAttachment(t *testing.T) {
|
func TestCreateAnonymousAttachment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := emptyTestSession(t)
|
session := emptyTestSession(t)
|
||||||
createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusSeeOther)
|
// this test is not right because it just doesn't pass the CSRF validation
|
||||||
|
createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateIssueAttachment(t *testing.T) {
|
func TestCreateIssueAttachment(t *testing.T) {
|
||||||
|
|
|
@ -5,12 +5,10 @@ package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"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"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -25,28 +23,12 @@ func TestCsrfProtection(t *testing.T) {
|
||||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||||
"_csrf": "fake_csrf",
|
"_csrf": "fake_csrf",
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
|
||||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
||||||
loc := resp.Header().Get("Location")
|
|
||||||
assert.Equal(t, setting.AppSubURL+"/", loc)
|
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
assert.Equal(t, "Bad Request: invalid CSRF token",
|
|
||||||
strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// test web form csrf via header. TODO: should use an UI api to test
|
// test web form csrf via header. TODO: should use an UI api to test
|
||||||
req = NewRequest(t, "POST", "/user/settings")
|
req = NewRequest(t, "POST", "/user/settings")
|
||||||
req.Header.Add("X-Csrf-Token", "fake_csrf")
|
req.Header.Add("X-Csrf-Token", "fake_csrf")
|
||||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
|
||||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
||||||
loc = resp.Header().Get("Location")
|
|
||||||
assert.Equal(t, setting.AppSubURL+"/", loc)
|
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
|
|
||||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
|
||||||
assert.Equal(t, "Bad Request: invalid CSRF token",
|
|
||||||
strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
forgejo_context "code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -190,11 +191,6 @@ func TestRedirectsWebhooks(t *testing.T) {
|
||||||
{from: "/user/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
{from: "/user/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
||||||
{from: "/admin/system-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
{from: "/admin/system-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
||||||
{from: "/admin/default-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
{from: "/admin/default-hooks/" + kind + "/new", to: "/user/login", verb: "GET"},
|
||||||
{from: "/user2/repo1/settings/hooks/" + kind + "/new", to: "/", verb: "POST"},
|
|
||||||
{from: "/admin/system-hooks/" + kind + "/new", to: "/", verb: "POST"},
|
|
||||||
{from: "/admin/default-hooks/" + kind + "/new", to: "/", verb: "POST"},
|
|
||||||
{from: "/user2/repo1/settings/hooks/1", to: "/", verb: "POST"},
|
|
||||||
{from: "/admin/hooks/1", to: "/", verb: "POST"},
|
|
||||||
}
|
}
|
||||||
for _, info := range redirects {
|
for _, info := range redirects {
|
||||||
req := NewRequest(t, info.verb, info.from)
|
req := NewRequest(t, info.verb, info.from)
|
||||||
|
@ -202,6 +198,24 @@ func TestRedirectsWebhooks(t *testing.T) {
|
||||||
assert.EqualValues(t, path.Join(setting.AppSubURL, info.to), test.RedirectURL(resp), info.from)
|
assert.EqualValues(t, path.Join(setting.AppSubURL, info.to), test.RedirectURL(resp), info.from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, kind := range []string{"forgejo", "gitea"} {
|
||||||
|
csrf := []struct {
|
||||||
|
from string
|
||||||
|
verb string
|
||||||
|
}{
|
||||||
|
{from: "/user2/repo1/settings/hooks/" + kind + "/new", verb: "POST"},
|
||||||
|
{from: "/admin/hooks/1", verb: "POST"},
|
||||||
|
{from: "/admin/system-hooks/" + kind + "/new", verb: "POST"},
|
||||||
|
{from: "/admin/default-hooks/" + kind + "/new", verb: "POST"},
|
||||||
|
{from: "/user2/repo1/settings/hooks/1", verb: "POST"},
|
||||||
|
}
|
||||||
|
for _, info := range csrf {
|
||||||
|
req := NewRequest(t, info.verb, info.from)
|
||||||
|
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
assert.Contains(t, resp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepoLinks(t *testing.T) {
|
func TestRepoLinks(t *testing.T) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/routers/web/auth"
|
"code.gitea.io/gitea/routers/web/auth"
|
||||||
|
forgejo_context "code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
|
@ -803,6 +805,16 @@ func TestOAuthIntrospection(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireCookieCSRF(t *testing.T, resp http.ResponseWriter) string {
|
||||||
|
for _, c := range resp.(*httptest.ResponseRecorder).Result().Cookies() {
|
||||||
|
if c.Name == "_csrf" {
|
||||||
|
return c.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.True(t, false, "_csrf not found in cookies")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func TestOAuth_GrantScopesReadUser(t *testing.T) {
|
func TestOAuth_GrantScopesReadUser(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
@ -840,19 +852,18 @@ func TestOAuth_GrantScopesReadUser(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
@ -921,19 +932,18 @@ func TestOAuth_GrantScopesFailReadRepository(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
@ -1000,19 +1010,18 @@ func TestOAuth_GrantScopesReadRepository(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
@ -1082,19 +1091,18 @@ func TestOAuth_GrantScopesReadPrivateGroups(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
@ -1164,19 +1172,18 @@ func TestOAuth_GrantScopesReadOnlyPublicGroups(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
@ -1260,19 +1267,18 @@ func TestOAuth_GrantScopesReadPublicGroupsWithTheReadScope(t *testing.T) {
|
||||||
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
|
||||||
|
|
||||||
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0]
|
||||||
htmlDoc := NewHTMLParser(t, authorizeResp.Body)
|
|
||||||
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"redirect_uri": "a",
|
"redirect_uri": "a",
|
||||||
"state": "thestate",
|
"state": "thestate",
|
||||||
"granted": "true",
|
"granted": "true",
|
||||||
})
|
})
|
||||||
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
|
grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest)
|
||||||
htmlDocGrant := NewHTMLParser(t, grantResp.Body)
|
assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString)
|
||||||
|
|
||||||
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"_csrf": htmlDocGrant.GetCSRF(),
|
"_csrf": requireCookieCSRF(t, authorizeResp),
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"client_id": app.ClientID,
|
"client_id": app.ClientID,
|
||||||
"client_secret": app.ClientSecret,
|
"client_secret": app.ClientSecret,
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
@ -157,15 +156,8 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
|
||||||
"_csrf": "fake_csrf",
|
"_csrf": "fake_csrf",
|
||||||
"new_branch_name": "test",
|
"new_branch_name": "test",
|
||||||
})
|
})
|
||||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
loc := resp.Header().Get("Location")
|
assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
|
||||||
assert.Equal(t, setting.AppSubURL+"/", loc)
|
|
||||||
resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
|
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
||||||
assert.Equal(t,
|
|
||||||
"Bad Request: invalid CSRF token",
|
|
||||||
strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDatabaseMissingABranch(t *testing.T) {
|
func TestDatabaseMissingABranch(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue