From be666b03eef1e085adc0749837480e0db7f811ad Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Mon, 22 Apr 2019 21:40:51 +0100
Subject: [PATCH] Trace Logging on Permission Denied & ColorFormat (#6618)

* Add log.ColorFormat and log.ColorFormatted

Structs can now implement log.ColorFormatted to provide their own
colored format when logged with `%-v` or additional flags.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add basic ColorFormat to repository and user

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add basic ColorFormat to access and unit

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add ColorFormat to permission and on trace log it

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add log.NewColoredIDValue to make ID value coloring consistent

Signed-off-by: Andrew Thornton <art27@cantab.net>

* formatting changes

* Add some better tracing to permission denied for read issues/pulls

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add Trace logging on permission denied

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Remove isTrace() check from deferred func

* Adjust repo and allow logging of team

* use FormatInt instead of Itoa

* Add blank line

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Update access.go
---
 .../advanced/logging-documentation.en-us.md   | 13 +++-
 models/access.go                              | 14 +++-
 models/org_team.go                            | 10 +++
 models/repo.go                                | 19 ++++++
 models/repo_permission.go                     | 61 +++++++++++++++++
 models/unit.go                                | 30 +++++++++
 models/user.go                                |  7 ++
 modules/context/permission.go                 | 37 +++++++++++
 modules/log/colors.go                         | 66 ++++++++++++++++++-
 modules/log/logger.go                         |  6 +-
 routers/api/v1/api.go                         | 50 ++++++++++++++
 routers/api/v1/repo/pull.go                   |  7 +-
 routers/repo/issue.go                         | 54 +++++++++++++++
 routers/repo/issue_watch.go                   | 18 +++++
 routers/repo/pull.go                          | 19 +++++-
 routers/repo/wiki.go                          | 10 +++
 routers/user/home.go                          |  9 +++
 17 files changed, 418 insertions(+), 12 deletions(-)

diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md
index bef034e2e7..3af75c0d8f 100644
--- a/docs/content/doc/advanced/logging-documentation.en-us.md
+++ b/docs/content/doc/advanced/logging-documentation.en-us.md
@@ -362,6 +362,17 @@ also set the `resetBytes` to the cached `resetBytes`.
 The `colorBytes` and `resetBytes` are not exported to prevent
 accidental overwriting of internal values.
 
+## ColorFormat & ColorFormatted
+
+Structs may implement the `log.ColorFormatted` interface by implementing the `ColorFormat(fmt.State)` function.
+
+If a `log.ColorFormatted` struct is logged with `%-v` format, its `ColorFormat` will be used instead of the usual `%v`. The full `fmt.State` will be passed to allow implementers to look at additional flags.
+
+In order to help implementers provide `ColorFormat` methods. There is a
+`log.ColorFprintf(...)` function in the log module that will wrap values in `log.ColoredValue` and recognise `%-v`.
+
+In general it is recommended not to make the results of this function too verbose to help increase its versatility. Usually this should simply be an `ID`:`Name`. If you wish to make a more verbose result, it is recommended to use `%-+v` as your marker.
+
 ## Log Spoofing protection
 
 In order to protect the logs from being spoofed with cleverly
@@ -392,5 +403,5 @@ func newNewoneLogService() {
 }
 ```
 
-You should then add `newOneLogService` to `NewServices()` in 
+You should then add `newOneLogService` to `NewServices()` in
 `modules/setting/setting.go`
diff --git a/models/access.go b/models/access.go
index 34d76953f5..3cdfc62f21 100644
--- a/models/access.go
+++ b/models/access.go
@@ -1,10 +1,15 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
 package models
 
-import "fmt"
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/log"
+)
 
 // AccessMode specifies the users access mode
 type AccessMode int
@@ -37,6 +42,13 @@ func (mode AccessMode) String() string {
 	}
 }
 
+// ColorFormat provides a ColorFormatted version of this AccessMode
+func (mode AccessMode) ColorFormat(s fmt.State) {
+	log.ColorFprintf(s, "%d:%s",
+		log.NewColoredIDValue(mode),
+		mode)
+}
+
 // ParseAccessMode returns corresponding access mode to given permission string.
 func ParseAccessMode(permission string) AccessMode {
 	switch permission {
diff --git a/models/org_team.go b/models/org_team.go
index 4e496e2f72..49d06896e5 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -34,6 +34,16 @@ type Team struct {
 	Units       []*TeamUnit `xorm:"-"`
 }
 
+// ColorFormat provides a basic color format for a Team
+func (t *Team) ColorFormat(s fmt.State) {
+	log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",
+		log.NewColoredIDValue(t.ID),
+		t.Name,
+		log.NewColoredIDValue(t.OrgID),
+		t.Authorize)
+
+}
+
 // GetUnits return a list of available units for a team
 func (t *Team) GetUnits() error {
 	return t.getUnits(x)
diff --git a/models/repo.go b/models/repo.go
index 6069be1243..936ad2ae37 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -18,6 +18,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
@@ -210,6 +211,24 @@ type Repository struct {
 	UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
 }
 
+// ColorFormat returns a colored string to represent this repo
+func (repo *Repository) ColorFormat(s fmt.State) {
+	var ownerName interface{}
+
+	if repo.OwnerName != "" {
+		ownerName = repo.OwnerName
+	} else if repo.Owner != nil {
+		ownerName = repo.Owner.Name
+	} else {
+		ownerName = log.NewColoredIDValue(strconv.FormatInt(repo.OwnerID, 10))
+	}
+
+	log.ColorFprintf(s, "%d:%s/%s",
+		log.NewColoredIDValue(repo.ID),
+		ownerName,
+		repo.Name)
+}
+
 // AfterLoad is invoked from XORM after setting the values of all fields of this object.
 func (repo *Repository) AfterLoad() {
 	// FIXME: use models migration to solve all at once.
diff --git a/models/repo_permission.go b/models/repo_permission.go
index eb0628cbd1..b4bd1c30f5 100644
--- a/models/repo_permission.go
+++ b/models/repo_permission.go
@@ -4,6 +4,12 @@
 
 package models
 
+import (
+	"fmt"
+
+	"code.gitea.io/gitea/modules/log"
+)
+
 // Permission contains all the permissions related variables to a repository for a user
 type Permission struct {
 	AccessMode AccessMode
@@ -90,12 +96,67 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
 	return p.CanWrite(UnitTypeIssues)
 }
 
+// ColorFormat writes a colored string for these Permissions
+func (p *Permission) ColorFormat(s fmt.State) {
+	noColor := log.ColorBytes(log.Reset)
+
+	format := "AccessMode: %-v, %d Units, %d UnitsMode(s): [ "
+	args := []interface{}{
+		p.AccessMode,
+		log.NewColoredValueBytes(len(p.Units), &noColor),
+		log.NewColoredValueBytes(len(p.UnitsMode), &noColor),
+	}
+	if s.Flag('+') {
+		for i, unit := range p.Units {
+			config := ""
+			if unit.Config != nil {
+				configBytes, err := unit.Config.ToDB()
+				config = string(configBytes)
+				if err != nil {
+					config = string(err.Error())
+				}
+			}
+			format += "\nUnits[%d]: ID: %d RepoID: %d Type: %-v Config: %s"
+			args = append(args,
+				log.NewColoredValueBytes(i, &noColor),
+				log.NewColoredIDValue(unit.ID),
+				log.NewColoredIDValue(unit.RepoID),
+				unit.Type,
+				config)
+		}
+		for key, value := range p.UnitsMode {
+			format += "\nUnitMode[%-v]: %-v"
+			args = append(args,
+				key,
+				value)
+		}
+	} else {
+		format += "..."
+	}
+	format += " ]"
+	log.ColorFprintf(s, format, args...)
+}
+
 // GetUserRepoPermission returns the user permissions to the repository
 func GetUserRepoPermission(repo *Repository, user *User) (Permission, error) {
 	return getUserRepoPermission(x, repo, user)
 }
 
 func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permission, err error) {
+	if log.IsTrace() {
+		defer func() {
+			if user == nil {
+				log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v",
+					repo,
+					perm)
+				return
+			}
+			log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v",
+				user,
+				repo,
+				perm)
+		}()
+	}
 	// anonymous user visit private repo.
 	// TODO: anonymous user visit public unit of private repo???
 	if user == nil && repo.IsPrivate {
diff --git a/models/unit.go b/models/unit.go
index 9619232cf4..a8ddf285da 100644
--- a/models/unit.go
+++ b/models/unit.go
@@ -5,7 +5,10 @@
 package models
 
 import (
+	"fmt"
 	"strings"
+
+	"code.gitea.io/gitea/modules/log"
 )
 
 // UnitType is Unit's Type
@@ -22,6 +25,33 @@ const (
 	UnitTypeExternalTracker                     // 7 ExternalTracker
 )
 
+func (u UnitType) String() string {
+	switch u {
+	case UnitTypeCode:
+		return "UnitTypeCode"
+	case UnitTypeIssues:
+		return "UnitTypeIssues"
+	case UnitTypePullRequests:
+		return "UnitTypePullRequests"
+	case UnitTypeReleases:
+		return "UnitTypeReleases"
+	case UnitTypeWiki:
+		return "UnitTypeWiki"
+	case UnitTypeExternalWiki:
+		return "UnitTypeExternalWiki"
+	case UnitTypeExternalTracker:
+		return "UnitTypeExternalTracker"
+	}
+	return fmt.Sprintf("Unknown UnitType %d", u)
+}
+
+// ColorFormat provides a ColorFormatted version of this UnitType
+func (u UnitType) ColorFormat(s fmt.State) {
+	log.ColorFprintf(s, "%d:%s",
+		log.NewColoredIDValue(u),
+		u)
+}
+
 var (
 	// allRepUnitTypes contains all the unit types
 	allRepUnitTypes = []UnitType{
diff --git a/models/user.go b/models/user.go
index 93fdc6f4af..eafc53d6f1 100644
--- a/models/user.go
+++ b/models/user.go
@@ -146,6 +146,13 @@ type User struct {
 	Theme         string `xorm:"NOT NULL DEFAULT ''"`
 }
 
+// ColorFormat writes a colored string to identify this struct
+func (u *User) ColorFormat(s fmt.State) {
+	log.ColorFprintf(s, "%d:%s",
+		log.NewColoredIDValue(u.ID),
+		log.NewColoredValue(u.Name))
+}
+
 // BeforeUpdate is invoked from XORM before updating this object.
 func (u *User) BeforeUpdate() {
 	if u.MaxRepoCreation < -1 {
diff --git a/modules/context/permission.go b/modules/context/permission.go
index 70f8695300..6ac935686b 100644
--- a/modules/context/permission.go
+++ b/modules/context/permission.go
@@ -6,6 +6,8 @@ package context
 
 import (
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+
 	macaron "gopkg.in/macaron.v1"
 )
 
@@ -45,6 +47,22 @@ func RequireRepoWriterOr(unitTypes ...models.UnitType) macaron.Handler {
 func RequireRepoReader(unitType models.UnitType) macaron.Handler {
 	return func(ctx *Context) {
 		if !ctx.Repo.CanRead(unitType) {
+			if log.IsTrace() {
+				if ctx.IsSigned {
+					log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
+						"User in Repo has Permissions: %-+v",
+						ctx.User,
+						unitType,
+						ctx.Repo.Repository,
+						ctx.Repo.Permission)
+				} else {
+					log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
+						"Anonymous user in Repo has Permissions: %-+v",
+						unitType,
+						ctx.Repo.Repository,
+						ctx.Repo.Permission)
+				}
+			}
 			ctx.NotFound(ctx.Req.RequestURI, nil)
 			return
 		}
@@ -59,6 +77,25 @@ func RequireRepoReaderOr(unitTypes ...models.UnitType) macaron.Handler {
 				return
 			}
 		}
+		if log.IsTrace() {
+			var format string
+			var args []interface{}
+			if ctx.IsSigned {
+				format = "Permission Denied: User %-v cannot read ["
+				args = append(args, ctx.User)
+			} else {
+				format = "Permission Denied: Anonymous user cannot read ["
+			}
+			for _, unit := range unitTypes {
+				format += "%-v, "
+				args = append(args, unit)
+			}
+
+			format = format[:len(format)-2] + "] in Repo %-v\n" +
+				"User in Repo has Permissions: %-+v"
+			args = append(args, ctx.Repo.Repository, ctx.Repo.Permission)
+			log.Trace(format, args...)
+		}
 		ctx.NotFound(ctx.Req.RequestURI, nil)
 	}
 }
diff --git a/modules/log/colors.go b/modules/log/colors.go
index ed6477d431..0ec8ce4ba8 100644
--- a/modules/log/colors.go
+++ b/modules/log/colors.go
@@ -7,6 +7,7 @@ package log
 import (
 	"fmt"
 	"io"
+	"reflect"
 	"strconv"
 	"strings"
 )
@@ -195,11 +196,12 @@ normalLoop:
 		lasti := i
 
 		if c.mode == escapeAll {
-			for i < end && (bytes[i] >= ' ' || bytes[i] == '\n') {
+			for i < end && (bytes[i] >= ' ' || bytes[i] == '\n' || bytes[i] == '\t') {
 				i++
 			}
 		} else {
-			for i < end && bytes[i] >= ' ' {
+			// Allow tabs if we're not escaping everything
+			for i < end && (bytes[i] >= ' ' || bytes[i] == '\t') {
 				i++
 			}
 		}
@@ -266,6 +268,39 @@ normalLoop:
 	return totalWritten, nil
 }
 
+// ColorSprintf returns a colored string from a format and arguments
+// arguments will be wrapped in ColoredValues to protect against color spoofing
+func ColorSprintf(format string, args ...interface{}) string {
+	if len(args) > 0 {
+		v := make([]interface{}, len(args))
+		for i := 0; i < len(v); i++ {
+			v[i] = NewColoredValuePointer(&args[i])
+		}
+		return fmt.Sprintf(format, v...)
+	}
+	return fmt.Sprintf(format)
+}
+
+// ColorFprintf will write to the provided writer similar to ColorSprintf
+func ColorFprintf(w io.Writer, format string, args ...interface{}) (int, error) {
+	if len(args) > 0 {
+		v := make([]interface{}, len(args))
+		for i := 0; i < len(v); i++ {
+			v[i] = NewColoredValuePointer(&args[i])
+		}
+		return fmt.Fprintf(w, format, v...)
+	}
+	return fmt.Fprintf(w, format)
+}
+
+// ColorFormatted structs provide their own colored string when formatted with ColorSprintf
+type ColorFormatted interface {
+	// ColorFormat provides the colored representation of the value
+	ColorFormat(s fmt.State)
+}
+
+var colorFormattedType = reflect.TypeOf((*ColorFormatted)(nil)).Elem()
+
 // ColoredValue will Color the provided value
 type ColoredValue struct {
 	colorBytes *[]byte
@@ -316,8 +351,33 @@ func NewColoredValueBytes(value interface{}, colorBytes *[]byte) *ColoredValue {
 	}
 }
 
-// Format will format the provided value and protect against ANSI spoofing within the value
+// NewColoredIDValue is a helper function to create a ColoredValue from a Value
+// The Value will be colored with FgCyan
+// If a ColoredValue is provided it is not changed
+func NewColoredIDValue(value interface{}) *ColoredValue {
+	return NewColoredValueBytes(&value, &fgCyanBytes)
+}
+
+// Format will format the provided value and protect against ANSI color spoofing within the value
+// If the wrapped value is ColorFormatted and the format is "%-v" then its ColorString will
+// be used. It is presumed that this ColorString is safe.
 func (cv *ColoredValue) Format(s fmt.State, c rune) {
+	if c == 'v' && s.Flag('-') {
+		if val, ok := (*cv.Value).(ColorFormatted); ok {
+			val.ColorFormat(s)
+			return
+		}
+		v := reflect.ValueOf(*cv.Value)
+		t := v.Type()
+
+		if reflect.PtrTo(t).Implements(colorFormattedType) {
+			vp := reflect.New(t)
+			vp.Elem().Set(v)
+			val := vp.Interface().(ColorFormatted)
+			val.ColorFormat(s)
+			return
+		}
+	}
 	s.Write([]byte(*cv.colorBytes))
 	fmt.Fprintf(&protectedANSIWriter{w: s}, fmtString(s, c), *(cv.Value))
 	s.Write([]byte(*cv.resetBytes))
diff --git a/modules/log/logger.go b/modules/log/logger.go
index 925ab02b71..9704ffd3d8 100644
--- a/modules/log/logger.go
+++ b/modules/log/logger.go
@@ -69,11 +69,7 @@ func (l *Logger) Log(skip int, level Level, format string, v ...interface{}) err
 	}
 	msg := format
 	if len(v) > 0 {
-		args := make([]interface{}, len(v))
-		for i := 0; i < len(args); i++ {
-			args[i] = NewColoredValuePointer(&v[i])
-		}
-		msg = fmt.Sprintf(format, args...)
+		msg = ColorSprintf(format, v...)
 	}
 	stack := ""
 	if l.GetStacktraceLevel() <= level {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index ac92f7cd43..d201dff917 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -369,6 +369,22 @@ func orgAssignment(args ...bool) macaron.Handler {
 
 func mustEnableIssues(ctx *context.APIContext) {
 	if !ctx.Repo.CanRead(models.UnitTypeIssues) {
+		if log.IsTrace() {
+			if ctx.IsSigned {
+				log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					models.UnitTypeIssues,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
+					"Anonymous user in Repo has Permissions: %-+v",
+					models.UnitTypeIssues,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			}
+		}
 		ctx.NotFound()
 		return
 	}
@@ -376,6 +392,22 @@ func mustEnableIssues(ctx *context.APIContext) {
 
 func mustAllowPulls(ctx *context.APIContext) {
 	if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(models.UnitTypePullRequests)) {
+		if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
+			if ctx.IsSigned {
+				log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					models.UnitTypePullRequests,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
+					"Anonymous user in Repo has Permissions: %-+v",
+					models.UnitTypePullRequests,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			}
+		}
 		ctx.NotFound()
 		return
 	}
@@ -384,6 +416,24 @@ func mustAllowPulls(ctx *context.APIContext) {
 func mustEnableIssuesOrPulls(ctx *context.APIContext) {
 	if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
 		!(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(models.UnitTypePullRequests)) {
+		if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
+			if ctx.IsSigned {
+				log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					models.UnitTypeIssues,
+					models.UnitTypePullRequests,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Anonymous user cannot read %-v and %-v in Repo %-v\n"+
+					"Anonymous user in Repo has Permissions: %-+v",
+					models.UnitTypeIssues,
+					models.UnitTypePullRequests,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			}
+		}
 		ctx.NotFound()
 		return
 	}
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 7abe6241bd..4fbd024f8c 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -672,7 +672,12 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 		return nil, nil, nil, nil, "", ""
 	}
 	if !perm.CanReadIssuesOrPulls(true) {
-		log.Trace("ParseCompareInfo[%d]: cannot create/read pull requests", baseRepo.ID)
+		if log.IsTrace() {
+			log.Trace("Permission Denied: User %-v cannot create/read pull requests in Repo %-v\nUser in headRepo has Permissions: %-+v",
+				ctx.User,
+				headRepo,
+				perm)
+		}
 		ctx.NotFound()
 		return nil, nil, nil, nil, "", ""
 	}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 7a681033af..42661ef738 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -1163,6 +1163,24 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
 	}
 
 	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+		if log.IsTrace() {
+			if ctx.IsSigned {
+				issueType := "issues"
+				if issue.IsPull {
+					issueType = "pulls"
+				}
+				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					log.NewColoredIDValue(issue.PosterID),
+					issueType,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Not logged in")
+			}
+		}
+
 		ctx.Error(403)
 	}
 
@@ -1353,6 +1371,24 @@ func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) {
 	}
 
 	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+		if log.IsTrace() {
+			if ctx.IsSigned {
+				issueType := "issues"
+				if issue.IsPull {
+					issueType = "pulls"
+				}
+				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					log.NewColoredIDValue(issue.PosterID),
+					issueType,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Not logged in")
+			}
+		}
+
 		ctx.Error(403)
 		return
 	}
@@ -1432,6 +1468,24 @@ func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) {
 	}
 
 	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
+		if log.IsTrace() {
+			if ctx.IsSigned {
+				issueType := "issues"
+				if comment.Issue.IsPull {
+					issueType = "pulls"
+				}
+				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					log.NewColoredIDValue(comment.Issue.PosterID),
+					issueType,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Not logged in")
+			}
+		}
+
 		ctx.Error(403)
 		return
 	} else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
diff --git a/routers/repo/issue_watch.go b/routers/repo/issue_watch.go
index c6a436801a..eae663495a 100644
--- a/routers/repo/issue_watch.go
+++ b/routers/repo/issue_watch.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
 )
 
 // IssueWatch sets issue watching
@@ -21,6 +22,23 @@ func IssueWatch(ctx *context.Context) {
 	}
 
 	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
+		if log.IsTrace() {
+			if ctx.IsSigned {
+				issueType := "issues"
+				if issue.IsPull {
+					issueType = "pulls"
+				}
+				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
+					"User in Repo has Permissions: %-+v",
+					ctx.User,
+					log.NewColoredIDValue(issue.PosterID),
+					issueType,
+					ctx.Repo.Repository,
+					ctx.Repo.Permission)
+			} else {
+				log.Trace("Permission Denied: Not logged in")
+			}
+		}
 		ctx.Error(403)
 		return
 	}
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
index 0616ee2c77..70a1443e8a 100644
--- a/routers/repo/pull.go
+++ b/routers/repo/pull.go
@@ -64,6 +64,18 @@ func getForkRepository(ctx *context.Context) *models.Repository {
 	}
 
 	if forkRepo.IsEmpty || !perm.CanRead(models.UnitTypeCode) {
+		if log.IsTrace() {
+			if forkRepo.IsEmpty {
+				log.Trace("Empty fork repository %-v", forkRepo)
+			} else {
+				log.Trace("Permission Denied: User %-v cannot read %-v of forkRepo %-v\n"+
+					"User in forkRepo has Permissions: %-+v",
+					ctx.User,
+					models.UnitTypeCode,
+					ctx.Repo,
+					perm)
+			}
+		}
 		ctx.NotFound("getForkRepository", nil)
 		return nil
 	}
@@ -704,7 +716,12 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *
 		return nil, nil, nil, nil, "", ""
 	}
 	if !perm.CanReadIssuesOrPulls(true) {
-		log.Trace("ParseCompareInfo[%d]: cannot create/read pull requests", baseRepo.ID)
+		if log.IsTrace() {
+			log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in headRepo has Permissions: %-+v",
+				ctx.User,
+				headRepo,
+				perm)
+		}
 		ctx.NotFound("ParseCompareInfo", nil)
 		return nil, nil, nil, nil, "", ""
 	}
diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go
index c0fb370dd9..43149c0340 100644
--- a/routers/repo/wiki.go
+++ b/routers/repo/wiki.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/util"
@@ -32,6 +33,15 @@ const (
 func MustEnableWiki(ctx *context.Context) {
 	if !ctx.Repo.CanRead(models.UnitTypeWiki) &&
 		!ctx.Repo.CanRead(models.UnitTypeExternalWiki) {
+		if log.IsTrace() {
+			log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+
+				"User in repo has Permissions: %-+v",
+				ctx.User,
+				models.UnitTypeWiki,
+				models.UnitTypeExternalWiki,
+				ctx.Repo.Repository,
+				ctx.Repo.Permission)
+		}
 		ctx.NotFound("MustEnableWiki", nil)
 		return
 	}
diff --git a/routers/user/home.go b/routers/user/home.go
index 79377ac500..2293461f08 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
@@ -308,6 +309,14 @@ func Issues(ctx *context.Context) {
 			return
 		}
 		if !perm.CanRead(models.UnitTypeIssues) {
+			if log.IsTrace() {
+				log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
+					"User in repo has Permissions: %-+v",
+					ctxUser,
+					models.UnitTypeIssues,
+					repo,
+					perm)
+			}
 			ctx.Status(404)
 			return
 		}