Skip to content

Fix bug when a token is given public only (#32204) #32218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ func (u *User) IsIndividual() bool {
return u.Type == UserTypeIndividual
}

func (u *User) IsUser() bool {
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}

// IsBot returns whether or not the user is of type bot
func (u *User) IsBot() bool {
return u.Type == UserTypeBot
Expand Down
14 changes: 14 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
return
}

// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
return
}

if publicOnly {
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
return
}
}
}
}

Expand Down
131 changes: 83 additions & 48 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,62 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
}
}

func checkTokenPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}

requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory)
if !ok || len(requiredScopeCategories) == 0 {
return
}

// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
return
}
}
}
}

// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
Expand All @@ -250,9 +306,6 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
return
}

ctx.Data["ApiTokenScopePublicRepoOnly"] = false
ctx.Data["ApiTokenScopePublicOrgOnly"] = false

// use the http method to determine the access level
requiredScopeLevel := auth_model.Read
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
Expand All @@ -261,29 +314,28 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC

// get the required scope for the given access level and category
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)

// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
return
}

// this context is used by the middleware in the specific route
ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository)
ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization)

allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
if !allow {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
return
}

if allow {
ctx.Data["requiredScopeCategories"] = requiredScopeCategories

// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
return
}

ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
// assign to true so that those searching should only filter public repositories/users/organizations
ctx.PublicOnly = publicOnly
}
}

Expand All @@ -295,25 +347,6 @@ func reqToken() func(ctx *context.APIContext) {
return
}

if true == ctx.Data["IsApiToken"] {
publicRepo, pubRepoExists := ctx.Data["ApiTokenScopePublicRepoOnly"]
publicOrg, pubOrgExists := ctx.Data["ApiTokenScopePublicOrgOnly"]

if pubRepoExists && publicRepo.(bool) &&
ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
return
}

if pubOrgExists && publicOrg.(bool) &&
ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}

return
}

if ctx.IsSigned {
return
}
Expand Down Expand Up @@ -879,11 +912,11 @@ func Routes() *web.Route {
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserIDAssignmentAPI())
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}

Expand Down Expand Up @@ -939,7 +972,7 @@ func Routes() *web.Route {
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())

m.Get("/activities/feeds", user.ListUserActivityFeeds)
}, context.UserAssignmentAPI(), individualPermsChecker)
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))

// Users (requires user scope)
Expand All @@ -957,7 +990,7 @@ func Routes() *web.Route {
m.Get("/starred", user.GetStarredRepos)

m.Get("/subscriptions", user.GetWatchedRepos)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())

// Users (requires user scope)
Expand Down Expand Up @@ -1044,7 +1077,7 @@ func Routes() *web.Route {
m.Get("", user.IsStarring)
m.Put("", user.Star)
m.Delete("", user.Unstar)
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
Expand All @@ -1069,18 +1102,20 @@ func Routes() *web.Route {
m.Get("", user.CheckUserBlock)
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())

// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
// FIXME: we need org in context
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
reqToken(),
bind(api.CreateRepoOption{}),
repo.CreateOrgRepoDeprecated)

// requires repo scope
// FIXME: Don't expose repository id outside of the system
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)

// Repos (requires repo scope)
Expand Down Expand Up @@ -1321,7 +1356,7 @@ func Routes() *web.Route {
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken())
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

// Notifications (requires notifications scope)
Expand All @@ -1330,7 +1365,7 @@ func Routes() *web.Route {
m.Combo("/notifications", reqToken()).
Get(notify.ListRepoNotifications).
Put(notify.ReadRepoNotifications)
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))

// Issue (requires issue scope)
Expand Down Expand Up @@ -1444,7 +1479,7 @@ func Routes() *web.Route {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))

// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
Expand All @@ -1455,14 +1490,14 @@ func Routes() *web.Route {
m.Get("/files", reqToken(), packages.ListPackageFiles)
})
m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())

// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Group("/orgs/{org}", func() {
Expand Down Expand Up @@ -1520,7 +1555,7 @@ func Routes() *web.Route {
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
Expand All @@ -1540,7 +1575,7 @@ func Routes() *web.Route {
Get(reqToken(), org.GetTeamRepo)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())

m.Group("/admin", func() {
m.Group("/cron", func() {
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"

vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned {
if ctx.IsSigned && !ctx.PublicOnly {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func SearchIssues(ctx *context.APIContext) {
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = true
opts.Private = !ctx.PublicOnly
opts.AllLimited = true
}
if ctx.FormString("owner") != "" {
Expand Down
7 changes: 6 additions & 1 deletion routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ func Search(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"

private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
if ctx.PublicOnly {
private = false
}

opts := &repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
Expand All @@ -138,7 +143,7 @@ func Search(ctx *context.APIContext) {
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
Collaborate: optional.None[bool](),
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
Private: private,
Template: optional.None[bool](),
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
Expand Down
6 changes: 6 additions & 0 deletions routers/api/v1/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
Expand Down Expand Up @@ -67,11 +68,16 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Type: user_model.UserTypeIndividual,
Visible: visible,
ListOptions: listOptions,
})
if err != nil {
Expand Down
7 changes: 4 additions & 3 deletions services/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ type APIContext struct {

ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer

Repo *Repository
Org *APIOrganization
Package *Package
Repo *Repository
Org *APIOrganization
Package *Package
PublicOnly bool // Whether the request is for a public endpoint
}

func init() {
Expand Down
Loading