mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-19 08:51:10 +00:00
resolves #8549 This PR add a config to enforce 2FA for the whole Forgejo instance. It can be configured to `none`, `admin` or `all`. A user who is required to enable 2FA is like a disabled user. He can only see the `/user/settings/security`-Page to enable 2FA, this should be similar to a user which needs to change his password. Also api and git-commands are not allowed. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. 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 - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. I will do it, if the general idea of this PR is a good feature. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Security features - [PR](https://codeberg.org/forgejo/forgejo/pulls/8753): <!--number 8753 --><!--line 0 --><!--description R2xvYmFsIDJGQSBlbmZvcmNlbWVudA==-->Global 2FA enforcement<!--description--> <!--end release-notes-assistant--> Co-authored-by: 0ko <0ko@noreply.codeberg.org> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8753 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Ellen Εμιλία Άννα Zscheile <fogti@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: zokki <zokki.softwareschmiede@gmail.com> Co-committed-by: zokki <zokki.softwareschmiede@gmail.com>
215 lines
6.4 KiB
Go
215 lines
6.4 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"forgejo.org/models/auth"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/httplib"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/templates"
|
|
"forgejo.org/modules/web/middleware"
|
|
)
|
|
|
|
// RedirectToUser redirect to a differently-named user
|
|
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
|
|
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "unable to get user")
|
|
return
|
|
}
|
|
|
|
redirectPath := strings.Replace(
|
|
ctx.Req.URL.EscapedPath(),
|
|
url.PathEscape(userName),
|
|
url.PathEscape(user.Name),
|
|
1,
|
|
)
|
|
if ctx.Req.URL.RawQuery != "" {
|
|
redirectPath += "?" + ctx.Req.URL.RawQuery
|
|
}
|
|
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
// RedirectToFirst redirects to first not empty URL which likely belongs to current site.
|
|
// If no suitable redirection is found, it redirects to the home.
|
|
// It returns the location it redirected to.
|
|
func (ctx *Context) RedirectToFirst(location ...string) string {
|
|
for _, loc := range location {
|
|
if len(loc) == 0 {
|
|
continue
|
|
}
|
|
|
|
if httplib.IsRiskyRedirectURL(loc) {
|
|
continue
|
|
}
|
|
|
|
ctx.Redirect(loc)
|
|
return loc
|
|
}
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/")
|
|
return setting.AppSubURL + "/"
|
|
}
|
|
|
|
const (
|
|
tplStatus404 base.TplName = "status/404"
|
|
tplStatus500 base.TplName = "status/500"
|
|
)
|
|
|
|
// HTML calls Context.HTML and renders the template to HTTP response
|
|
func (ctx *Context) HTML(status int, name base.TplName) {
|
|
log.Debug("Template: %s", name)
|
|
|
|
tmplStartTime := time.Now()
|
|
if !setting.IsProd {
|
|
ctx.Data["TemplateName"] = name
|
|
}
|
|
ctx.Data["TemplateLoadTimes"] = func() string {
|
|
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
|
|
}
|
|
|
|
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
|
|
if err == nil || errors.Is(err, syscall.EPIPE) {
|
|
return
|
|
}
|
|
|
|
// if rendering fails, show error page
|
|
if name != tplStatus500 {
|
|
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
|
|
ctx.ServerError("Render failed", err) // show the 500 error page
|
|
} else {
|
|
ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
|
|
return
|
|
}
|
|
}
|
|
|
|
// JSONTemplate renders the template as JSON response
|
|
// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
|
|
func (ctx *Context) JSONTemplate(tmpl base.TplName) {
|
|
t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
|
|
if err != nil {
|
|
ctx.ServerError("unable to find template", err)
|
|
return
|
|
}
|
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
|
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
|
ctx.ServerError("unable to execute template", err)
|
|
}
|
|
}
|
|
|
|
// RenderToHTML renders the template content to a HTML string
|
|
func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
|
|
var buf strings.Builder
|
|
err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
|
|
return template.HTML(buf.String()), err
|
|
}
|
|
|
|
// RenderWithErr used for page has form validation but need to prompt error to users.
|
|
func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
|
|
if form != nil {
|
|
middleware.AssignForm(form, ctx.Data)
|
|
}
|
|
ctx.Flash.Error(msg, true)
|
|
ctx.HTML(http.StatusOK, tpl)
|
|
}
|
|
|
|
// validateTwoFactorRequirement sets ctx-data to hide/show ui-elements depending on the GlobalTwoFactorRequirement
|
|
func (ctx *Context) validateTwoFactorRequirement() {
|
|
if ctx.Doer == nil || !ctx.Doer.MustHaveTwoFactor() {
|
|
return
|
|
}
|
|
|
|
hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Doer.ID)
|
|
if err != nil {
|
|
log.ErrorWithSkip(2, "Error getting 2fa: %s", err)
|
|
// fallthrough to set the variables
|
|
}
|
|
ctx.Data["MustEnableTwoFactor"] = !hasTwoFactor
|
|
ctx.Data["HideNavbarLinks"] = !hasTwoFactor
|
|
}
|
|
|
|
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
|
|
func (ctx *Context) NotFound(logMsg string, logErr error) {
|
|
ctx.notFoundInternal(logMsg, logErr)
|
|
}
|
|
|
|
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
|
if logErr != nil {
|
|
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
|
|
if !setting.IsProd {
|
|
ctx.Data["ErrorMsg"] = logErr
|
|
}
|
|
}
|
|
|
|
// response simple message if Accept isn't text/html
|
|
showHTML := false
|
|
for _, part := range ctx.Req.Header["Accept"] {
|
|
if strings.Contains(part, "text/html") {
|
|
showHTML = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !showHTML {
|
|
ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
|
|
return
|
|
}
|
|
|
|
ctx.validateTwoFactorRequirement()
|
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
|
ctx.Data["Title"] = ctx.Locale.TrString("error.not_found.title")
|
|
ctx.HTML(http.StatusNotFound, tplStatus404)
|
|
}
|
|
|
|
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
|
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
|
ctx.serverErrorInternal(logMsg, logErr)
|
|
}
|
|
|
|
func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
|
|
if logErr != nil {
|
|
log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
|
|
if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) {
|
|
// This is an error within the underlying connection
|
|
// and further rendering will not work so just return
|
|
return
|
|
}
|
|
|
|
// it's safe to show internal error to admin users, and it helps
|
|
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
|
ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr)
|
|
}
|
|
}
|
|
|
|
ctx.validateTwoFactorRequirement()
|
|
ctx.HTML(http.StatusInternalServerError, tplStatus500)
|
|
}
|
|
|
|
// NotFoundOrServerError use error check function to determine if the error
|
|
// is about not found. It responds with 404 status code for not found error,
|
|
// or error context description for logging purpose of 500 server error.
|
|
// TODO: remove the "errCheck" and use util.ErrNotFound to check
|
|
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
|
if errCheck(logErr) {
|
|
ctx.notFoundInternal(logMsg, logErr)
|
|
return
|
|
}
|
|
ctx.serverErrorInternal(logMsg, logErr)
|
|
}
|