Merge pull request 'Fix bug when a token is given public only' (#5515) from earl-warren/forgejo:wip-public-scope into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5515 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
4cb01ba5da
12 changed files with 175 additions and 53 deletions
|
@ -421,6 +421,10 @@ func (u *User) IsIndividual() bool {
|
||||||
return u.Type == UserTypeIndividual
|
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
|
// IsBot returns whether or not the user is of type bot
|
||||||
func (u *User) IsBot() bool {
|
func (u *User) IsBot() bool {
|
||||||
return u.Type == UserTypeBot
|
return u.Type == UserTypeBot
|
||||||
|
|
1
release-notes/5515.md
Normal file
1
release-notes/5515.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
**Fixing this bug is a breaking change because existing tokens with a public scope will no longer return private resources. They have to be deleted and re-created without the public scope to restore their original behavior**. The public scope of an application token does not filter out private repositories, organizations or packages in some cases. This scope is not the default, it has to be manually set via the web UI or the API. When the public scope is explicitly added to an application token that is allowed to list the repositories and packages of a user or an organization, it is meant as a restriction. For instance if a user has two repositories, one private and the other publicly visible, a token with the public scope used with the API endpoint listing the repositories that belong to this user must only return the publicly visible one and not reveal the existence of the private one.
|
|
@ -65,6 +65,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")
|
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -274,6 +274,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 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
|
// if a token is not being used, reqToken will enforce other sign in methods
|
||||||
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
||||||
|
@ -289,9 +345,6 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["ApiTokenScopePublicRepoOnly"] = false
|
|
||||||
ctx.Data["ApiTokenScopePublicOrgOnly"] = false
|
|
||||||
|
|
||||||
// use the http method to determine the access level
|
// use the http method to determine the access level
|
||||||
requiredScopeLevel := auth_model.Read
|
requiredScopeLevel := auth_model.Read
|
||||||
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
|
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
|
||||||
|
@ -300,6 +353,18 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
||||||
|
|
||||||
// get the required scope for the given access level and category
|
// get the required scope for the given access level and category
|
||||||
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
|
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
|
||||||
|
allow, err := scope.HasScope(requiredScopes...)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allow {
|
||||||
|
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["requiredScopeCategories"] = requiredScopeCategories
|
||||||
|
|
||||||
// check if scope only applies to public resources
|
// check if scope only applies to public resources
|
||||||
publicOnly, err := scope.PublicOnly()
|
publicOnly, err := scope.PublicOnly()
|
||||||
|
@ -308,21 +373,8 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this context is used by the middleware in the specific route
|
// assign to true so that those searching should only filter public repositories/users/organizations
|
||||||
ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository)
|
ctx.PublicOnly = publicOnly
|
||||||
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())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if allow {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,25 +386,6 @@ func reqToken() func(ctx *context.APIContext) {
|
||||||
return
|
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 {
|
if ctx.IsSigned {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -800,11 +833,11 @@ func Routes() *web.Route {
|
||||||
m.Group("/user/{username}", func() {
|
m.Group("/user/{username}", func() {
|
||||||
m.Get("", activitypub.Person)
|
m.Get("", activitypub.Person)
|
||||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||||
}, context.UserAssignmentAPI())
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||||
m.Group("/user-id/{user-id}", func() {
|
m.Group("/user-id/{user-id}", func() {
|
||||||
m.Get("", activitypub.Person)
|
m.Get("", activitypub.Person)
|
||||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||||
}, context.UserIDAssignmentAPI())
|
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
||||||
m.Group("/actor", func() {
|
m.Group("/actor", func() {
|
||||||
m.Get("", activitypub.Actor)
|
m.Get("", activitypub.Actor)
|
||||||
m.Post("/inbox", activitypub.ActorInbox)
|
m.Post("/inbox", activitypub.ActorInbox)
|
||||||
|
@ -871,7 +904,7 @@ func Routes() *web.Route {
|
||||||
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
||||||
|
|
||||||
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
||||||
}, context.UserAssignmentAPI(), individualPermsChecker)
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
||||||
|
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
|
@ -891,7 +924,7 @@ func Routes() *web.Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Get("/subscriptions", user.GetWatchedRepos)
|
m.Get("/subscriptions", user.GetWatchedRepos)
|
||||||
}, context.UserAssignmentAPI())
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||||
|
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
|
@ -988,7 +1021,7 @@ func Routes() *web.Route {
|
||||||
m.Get("", user.IsStarring)
|
m.Get("", user.IsStarring)
|
||||||
m.Put("", user.Star)
|
m.Put("", user.Star)
|
||||||
m.Delete("", user.Unstar)
|
m.Delete("", user.Unstar)
|
||||||
}, repoAssignment())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||||
}
|
}
|
||||||
m.Get("/times", repo.ListMyTrackedTimes)
|
m.Get("/times", repo.ListMyTrackedTimes)
|
||||||
|
@ -1019,12 +1052,14 @@ func Routes() *web.Route {
|
||||||
|
|
||||||
// Repositories (requires repo scope, org scope)
|
// Repositories (requires repo scope, org scope)
|
||||||
m.Post("/org/{org}/repos",
|
m.Post("/org/{org}/repos",
|
||||||
|
// FIXME: we need org in context
|
||||||
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
|
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
|
||||||
reqToken(),
|
reqToken(),
|
||||||
bind(api.CreateRepoOption{}),
|
bind(api.CreateRepoOption{}),
|
||||||
repo.CreateOrgRepoDeprecated)
|
repo.CreateOrgRepoDeprecated)
|
||||||
|
|
||||||
// requires repo scope
|
// 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)
|
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)
|
||||||
|
|
||||||
// Repos (requires repo scope)
|
// Repos (requires repo scope)
|
||||||
|
@ -1305,7 +1340,7 @@ func Routes() *web.Route {
|
||||||
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
|
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
|
||||||
m.Delete("", repo.DeleteAvatar)
|
m.Delete("", repo.DeleteAvatar)
|
||||||
}, reqAdmin(), reqToken())
|
}, reqAdmin(), reqToken())
|
||||||
}, repoAssignment())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||||
|
|
||||||
// Notifications (requires notifications scope)
|
// Notifications (requires notifications scope)
|
||||||
|
@ -1314,7 +1349,7 @@ func Routes() *web.Route {
|
||||||
m.Combo("/notifications", reqToken()).
|
m.Combo("/notifications", reqToken()).
|
||||||
Get(notify.ListRepoNotifications).
|
Get(notify.ListRepoNotifications).
|
||||||
Put(notify.ReadRepoNotifications)
|
Put(notify.ReadRepoNotifications)
|
||||||
}, repoAssignment())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
||||||
|
|
||||||
// Issue (requires issue scope)
|
// Issue (requires issue scope)
|
||||||
|
@ -1428,7 +1463,7 @@ func Routes() *web.Route {
|
||||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||||
})
|
})
|
||||||
}, repoAssignment())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||||
|
|
||||||
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
||||||
|
@ -1439,14 +1474,14 @@ func Routes() *web.Route {
|
||||||
m.Get("/files", reqToken(), packages.ListPackageFiles)
|
m.Get("/files", reqToken(), packages.ListPackageFiles)
|
||||||
})
|
})
|
||||||
m.Get("/", reqToken(), packages.ListPackages)
|
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
|
// Organizations
|
||||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
||||||
m.Group("/users/{username}/orgs", func() {
|
m.Group("/users/{username}/orgs", func() {
|
||||||
m.Get("", reqToken(), org.ListUserOrgs)
|
m.Get("", reqToken(), org.ListUserOrgs)
|
||||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
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.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
||||||
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
|
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
|
||||||
m.Group("/orgs/{org}", func() {
|
m.Group("/orgs/{org}", func() {
|
||||||
|
@ -1513,7 +1548,7 @@ func Routes() *web.Route {
|
||||||
m.Put("/unblock/{username}", org.UnblockUser)
|
m.Put("/unblock/{username}", org.UnblockUser)
|
||||||
}, context.UserAssignmentAPI())
|
}, context.UserAssignmentAPI())
|
||||||
}, reqToken(), reqOrgOwnership())
|
}, reqToken(), reqOrgOwnership())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
||||||
m.Group("/teams/{teamid}", func() {
|
m.Group("/teams/{teamid}", func() {
|
||||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||||
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
|
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
|
||||||
|
@ -1533,7 +1568,7 @@ func Routes() *web.Route {
|
||||||
Get(reqToken(), org.GetTeamRepo)
|
Get(reqToken(), org.GetTeamRepo)
|
||||||
})
|
})
|
||||||
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
|
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("/admin", func() {
|
||||||
m.Group("/cron", func() {
|
m.Group("/cron", func() {
|
||||||
|
|
|
@ -192,7 +192,7 @@ func GetAll(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/OrganizationList"
|
// "$ref": "#/responses/OrganizationList"
|
||||||
|
|
||||||
vMode := []api.VisibleType{api.VisibleTypePublic}
|
vMode := []api.VisibleType{api.VisibleTypePublic}
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned && !ctx.PublicOnly {
|
||||||
vMode = append(vMode, api.VisibleTypeLimited)
|
vMode = append(vMode, api.VisibleTypeLimited)
|
||||||
if ctx.Doer.IsAdmin {
|
if ctx.Doer.IsAdmin {
|
||||||
vMode = append(vMode, api.VisibleTypePrivate)
|
vMode = append(vMode, api.VisibleTypePrivate)
|
||||||
|
|
|
@ -149,7 +149,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
}
|
}
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
opts.Private = true
|
opts.Private = !ctx.PublicOnly
|
||||||
opts.AllLimited = true
|
opts.AllLimited = true
|
||||||
}
|
}
|
||||||
if ctx.FormString("owner") != "" {
|
if ctx.FormString("owner") != "" {
|
||||||
|
|
|
@ -130,6 +130,11 @@ func Search(ctx *context.APIContext) {
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
|
||||||
|
if ctx.PublicOnly {
|
||||||
|
private = false
|
||||||
|
}
|
||||||
|
|
||||||
opts := &repo_model.SearchRepoOptions{
|
opts := &repo_model.SearchRepoOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
|
@ -139,7 +144,7 @@ func Search(ctx *context.APIContext) {
|
||||||
TeamID: ctx.FormInt64("team_id"),
|
TeamID: ctx.FormInt64("team_id"),
|
||||||
TopicOnly: ctx.FormBool("topic"),
|
TopicOnly: ctx.FormBool("topic"),
|
||||||
Collaborate: optional.None[bool](),
|
Collaborate: optional.None[bool](),
|
||||||
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
|
Private: private,
|
||||||
Template: optional.None[bool](),
|
Template: optional.None[bool](),
|
||||||
StarredByID: ctx.FormInt64("starredBy"),
|
StarredByID: ctx.FormInt64("starredBy"),
|
||||||
IncludeDescription: ctx.FormBool("includeDesc"),
|
IncludeDescription: ctx.FormBool("includeDesc"),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/routers/api/v1/utils"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
@ -68,12 +69,17 @@ func Search(ctx *context.APIContext) {
|
||||||
maxResults = 1
|
maxResults = 1
|
||||||
users = []*user_model.User{user_model.NewActionsUser()}
|
users = []*user_model.User{user_model.NewActionsUser()}
|
||||||
default:
|
default:
|
||||||
|
var visible []structs.VisibleType
|
||||||
|
if ctx.PublicOnly {
|
||||||
|
visible = []structs.VisibleType{structs.VisibleTypePublic}
|
||||||
|
}
|
||||||
users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
|
users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Keyword: ctx.FormTrim("q"),
|
Keyword: ctx.FormTrim("q"),
|
||||||
UID: uid,
|
UID: uid,
|
||||||
Type: user_model.UserTypeIndividual,
|
Type: user_model.UserTypeIndividual,
|
||||||
SearchByEmail: true,
|
SearchByEmail: true,
|
||||||
|
Visible: visible,
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -45,6 +45,7 @@ type APIContext struct {
|
||||||
Package *Package
|
Package *Package
|
||||||
QuotaGroup *quota_model.Group
|
QuotaGroup *quota_model.Group
|
||||||
QuotaRule *quota_model.Rule
|
QuotaRule *quota_model.Rule
|
||||||
|
PublicOnly bool // Whether the request is for a public endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -76,6 +76,34 @@ func TestAPIListIssues(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIListIssuesPublicOnly(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, owner1.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||||
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner1.Name, repo1.Name))
|
||||||
|
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
||||||
|
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID})
|
||||||
|
|
||||||
|
session = loginUser(t, owner2.Name)
|
||||||
|
token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||||
|
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner2.Name, repo2.Name))
|
||||||
|
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
||||||
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||||
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPICreateIssue(t *testing.T) {
|
func TestAPICreateIssue(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
const body, title = "apiTestBody", "apiTestTitle"
|
const body, title = "apiTestBody", "apiTestTitle"
|
||||||
|
@ -404,6 +432,12 @@ func TestAPISearchIssues(t *testing.T) {
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
assert.Len(t, apiIssues, expectedIssueCount)
|
assert.Len(t, apiIssues, expectedIssueCount)
|
||||||
|
|
||||||
|
publicOnlyToken := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||||
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
assert.Len(t, apiIssues, 15) // 15 public issues
|
||||||
|
|
||||||
since := "2000-01-01T00:50:01+00:00" // 946687801
|
since := "2000-01-01T00:50:01+00:00" // 946687801
|
||||||
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
||||||
query.Add("since", since)
|
query.Add("since", since)
|
||||||
|
|
|
@ -29,9 +29,13 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
session := loginUser(t, user1.LowerName)
|
session := loginUser(t, user1.LowerName)
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
||||||
|
|
||||||
|
// public only token should be forbidden
|
||||||
|
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
|
||||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||||
|
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||||
bs, err := io.ReadAll(resp.Body)
|
bs, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -43,6 +47,8 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||||
assert.EqualValues(t, "master", branches[1].Name)
|
assert.EqualValues(t, "master", branches[1].Name)
|
||||||
|
|
||||||
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
|
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||||
|
|
||||||
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
|
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
|
||||||
bs, err = io.ReadAll(resp.Body)
|
bs, err = io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -50,6 +56,8 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||||
require.NoError(t, json.Unmarshal(bs, &branch))
|
require.NoError(t, json.Unmarshal(bs, &branch))
|
||||||
assert.EqualValues(t, "test_branch", branch.Name)
|
assert.EqualValues(t, "test_branch", branch.Name)
|
||||||
|
|
||||||
|
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||||
|
|
||||||
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
|
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.Body = io.NopCloser(bytes.NewBufferString(`{"new_branch_name":"test_branch2", "old_branch_name": "test_branch", "old_ref_name":"refs/heads/test_branch"}`))
|
req.Body = io.NopCloser(bytes.NewBufferString(`{"new_branch_name":"test_branch2", "old_branch_name": "test_branch", "old_ref_name":"refs/heads/test_branch"}`))
|
||||||
|
@ -74,6 +82,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||||
|
|
||||||
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
|
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
|
||||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
|
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
|
||||||
|
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||||
|
|
||||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
|
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -40,6 +40,19 @@ func TestAPIUserSearchLoggedIn(t *testing.T) {
|
||||||
assert.Contains(t, user.UserName, query)
|
assert.Contains(t, user.UserName, query)
|
||||||
assert.NotEmpty(t, user.Email)
|
assert.NotEmpty(t, user.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query).
|
||||||
|
AddTokenAuth(publicToken)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
results = SearchResults{}
|
||||||
|
DecodeJSON(t, resp, &results)
|
||||||
|
assert.NotEmpty(t, results.Data)
|
||||||
|
for _, user := range results.Data {
|
||||||
|
assert.Contains(t, user.UserName, query)
|
||||||
|
assert.NotEmpty(t, user.Email)
|
||||||
|
assert.Equal(t, "public", user.Visibility)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIUserSearchNotLoggedIn(t *testing.T) {
|
func TestAPIUserSearchNotLoggedIn(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue