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 }