mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
Currently authentication methods return information in two forms: they return who was authenticated as a `*user_model.User`, and then they insert key-values into `ctx.Data` which has critical impact on how the authenticated request is treated. This PR changes the authentication methods to return structured data in the form of an `AuthenticationResult`, with all the key-value information in `ctx.Data` being moved into methods on the `AuthenticationResult` interface. Authentication workflows in Forgejo are a real mess. This is the first step in trying to clean it up and make the code predictable and reasonable, and is both follow-up work that was identified from the repo-specific access tokens (where the `"ApiTokenReducer"` key-value was added), and is pre-requisite work to future JWT enhancements that are [being discussed](https://codeberg.org/forgejo/forgejo/issues/3571#issuecomment-13268004). ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - All changes, at least in theory, are refactors of existing logic and are not expected to have functional deviations -- existing regression tests are the only planned testing. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12202 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
262 lines
8.8 KiB
Go
262 lines
8.8 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"forgejo.org/models/unit"
|
|
user_model "forgejo.org/models/user"
|
|
mc "forgejo.org/modules/cache"
|
|
"forgejo.org/modules/gitrepo"
|
|
"forgejo.org/modules/httpcache"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/templates"
|
|
"forgejo.org/modules/translation"
|
|
"forgejo.org/modules/web"
|
|
"forgejo.org/modules/web/middleware"
|
|
web_types "forgejo.org/modules/web/types"
|
|
"forgejo.org/services/auth"
|
|
|
|
"code.forgejo.org/go-chi/cache"
|
|
"code.forgejo.org/go-chi/session"
|
|
)
|
|
|
|
// Render represents a template render
|
|
type Render interface {
|
|
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
|
|
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
|
|
}
|
|
|
|
// Context represents context of a request.
|
|
type Context struct {
|
|
*Base
|
|
|
|
TemplateContext *templates.Context
|
|
|
|
Render Render
|
|
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
|
|
|
Cache cache.Cache
|
|
Flash *middleware.Flash
|
|
Session session.Store
|
|
|
|
Link string // current request URL (without query string)
|
|
|
|
Doer *user_model.User // current signed-in user
|
|
IsSigned bool
|
|
Authentication auth.AuthenticationResult
|
|
|
|
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
|
|
|
Repo *Repository
|
|
Org *Organization
|
|
Package *Package
|
|
}
|
|
|
|
func init() {
|
|
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
|
return req.Context().Value(WebContextKey).(*Context)
|
|
})
|
|
}
|
|
|
|
type webContextKeyType struct{}
|
|
|
|
var WebContextKey = webContextKeyType{}
|
|
|
|
func GetWebContext(req *http.Request) *Context {
|
|
ctx, _ := req.Context().Value(WebContextKey).(*Context)
|
|
return ctx
|
|
}
|
|
|
|
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
|
|
type ValidateContext struct {
|
|
*Base
|
|
}
|
|
|
|
// GetValidateContext gets a context for middleware form validation
|
|
func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
|
|
if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
|
|
ctx = &ValidateContext{Base: ctxAPI.Base}
|
|
} else if ctxWeb, ok := req.Context().Value(WebContextKey).(*Context); ok {
|
|
ctx = &ValidateContext{Base: ctxWeb.Base}
|
|
} else {
|
|
panic("invalid context, expect either APIContext or Context")
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func NewTemplateContextForWeb(ctx *Context) *templates.Context {
|
|
tmplCtx := templates.NewContext(ctx)
|
|
tmplCtx.Locale = ctx.Locale
|
|
tmplCtx.AvatarUtils = templates.NewAvatarUtils(ctx)
|
|
tmplCtx.Data = ctx.Data
|
|
return tmplCtx
|
|
}
|
|
|
|
func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
|
ctx := &Context{
|
|
Base: base,
|
|
Render: render,
|
|
Session: session,
|
|
|
|
Cache: mc.GetCache(),
|
|
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
|
|
Repo: &Repository{PullRequest: &PullRequest{}},
|
|
Org: &Organization{},
|
|
|
|
Authentication: &auth.UnauthenticatedResult{},
|
|
}
|
|
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
|
|
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
|
return ctx
|
|
}
|
|
|
|
func (ctx *Context) AddPluralStringsToPageData(keys []string) {
|
|
for _, key := range keys {
|
|
array, fallback := ctx.Locale.TrPluralStringAllForms(key)
|
|
|
|
ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)[key] = array
|
|
|
|
if fallback != nil {
|
|
ctx.PageData["PLURALSTRINGS_FALLBACK"].(map[string][]string)[key] = fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Contexter initializes a classic context for a request.
|
|
func Contexter() func(next http.Handler) http.Handler {
|
|
rnd := templates.HTMLRenderer()
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
base, baseCleanUp := NewBaseContext(resp, req)
|
|
defer baseCleanUp()
|
|
ctx := NewWebContext(base, rnd, session.GetSession(req))
|
|
|
|
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
|
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
|
|
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
|
|
ctx.Data["Link"] = ctx.Link
|
|
|
|
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
|
|
ctx.PageData = map[string]any{}
|
|
ctx.Data["PageData"] = ctx.PageData
|
|
|
|
ctx.AppendContextValue(WebContextKey, ctx)
|
|
ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
|
|
|
// Get the last flash message from cookie
|
|
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
|
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
|
// store last Flash message into the template data, to render it
|
|
ctx.Data["Flash"] = &middleware.Flash{
|
|
DataStore: ctx,
|
|
Values: vals,
|
|
ErrorMsg: vals.Get("error"),
|
|
SuccessMsg: vals.Get("success"),
|
|
InfoMsg: vals.Get("info"),
|
|
WarningMsg: vals.Get("warning"),
|
|
}
|
|
}
|
|
|
|
// if there are new messages in the ctx.Flash, write them into cookie
|
|
ctx.Resp.Before(func(resp ResponseWriter) {
|
|
if val := ctx.Flash.Encode(); val != "" {
|
|
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0)
|
|
} else if lastFlashCookie != "" {
|
|
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, "", -1)
|
|
}
|
|
})
|
|
|
|
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
|
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
|
|
|
ctx.Data["SystemConfig"] = setting.Config()
|
|
|
|
// 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["DisableStars"] = setting.Repository.DisableStars
|
|
ctx.Data["DisableForks"] = setting.Repository.DisableForks
|
|
ctx.Data["EnableActions"] = setting.Actions.Enabled
|
|
ctx.Data["EnableFederation"] = setting.Federation.Enabled
|
|
|
|
ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
|
|
ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
|
|
ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled()
|
|
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
|
|
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
|
|
|
|
ctx.Data["AllLangs"] = translation.AllLangs()
|
|
|
|
ctx.PageData["PLURAL_RULE_LANG"] = translation.GetPluralRule(ctx.Locale)
|
|
ctx.PageData["PLURAL_RULE_FALLBACK"] = translation.GetDefaultPluralRule()
|
|
ctx.PageData["PLURALSTRINGS_LANG"] = map[string][]string{}
|
|
ctx.PageData["PLURALSTRINGS_FALLBACK"] = map[string][]string{}
|
|
|
|
ctx.AddPluralStringsToPageData([]string{"relativetime.mins", "relativetime.hours", "relativetime.days", "relativetime.weeks", "relativetime.months", "relativetime.years"})
|
|
|
|
ctx.PageData["DATETIMESTRINGS"] = map[string]string{
|
|
"FUTURE": ctx.Locale.TrString("relativetime.future"),
|
|
"NOW": ctx.Locale.TrString("relativetime.now"),
|
|
}
|
|
for _, key := range []string{"relativetime.1day", "relativetime.1week", "relativetime.1month", "relativetime.1year"} {
|
|
// These keys are used for special-casing some time words. We only add keys that are actually translated, so that we
|
|
// can fall back to the generic pluralized time word in the correct language if the special case is untranslated.
|
|
if ctx.Locale.HasKey(key) {
|
|
ctx.PageData["DATETIMESTRINGS"].(map[string]string)[key] = ctx.Locale.TrString(key)
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// HasError returns true if error occurs in form validation.
|
|
// Attention: this function changes ctx.Data and ctx.Flash
|
|
func (ctx *Context) HasError() bool {
|
|
hasErr, ok := ctx.Data["HasError"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
|
ctx.Data["Flash"] = ctx.Flash
|
|
return hasErr.(bool)
|
|
}
|
|
|
|
// GetErrMsg returns error message in form validation.
|
|
func (ctx *Context) GetErrMsg() string {
|
|
msg, _ := ctx.Data["ErrorMsg"].(string)
|
|
if msg == "" {
|
|
msg = "invalid form data"
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (ctx *Context) JSONRedirect(redirect string) {
|
|
ctx.JSON(http.StatusOK, map[string]any{"redirect": redirect})
|
|
}
|
|
|
|
func (ctx *Context) JSONOK() {
|
|
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
|
|
}
|
|
|
|
func (ctx *Context) JSONError(msg any) {
|
|
switch v := msg.(type) {
|
|
case string:
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
|
|
case template.HTML:
|
|
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
|
|
default:
|
|
panic(fmt.Sprintf("unsupported type: %T", msg))
|
|
}
|
|
}
|