16
7
Fork
You've already forked design
4

Facilitate and improve the testing of the REST API permissions checks #63

Closed
opened 2026年05月07日 17:46:08 +02:00 by limiting-factor · 33 comments

Pull request: forgejo/forgejo#12512/files

The situation

The REST API permissions checks are partly implemented in middlewares inserted in the routes leading to each endpoint. In addition the function implementing the endpoint sometimes implements permissions checks of their own. A number of tests are implemented as integration tests that create various scenarios to verify the permissions are enforced when a REST API call happens, for instance when trying to access an issue in a private repository. The test coverage for these checks improved over time, most notably when a security bug is fixed.

The problem

With hundreds of REST API endpoints and the current strategy, implementing exhaustive tests ensuring the permissions checks for each REST API endpoint are enforced would require creating variations (private repository, etc.) for each of them. It is:

  • Exhausting for the developer (identifying which variations are relevant for a given REST API endpoint is tedious and error prone)
  • Very challenging to maintain (when a new permission check is added, the existing tests may lack coverage)
  • More difficult to fix permissions enforcement related security fixes (tests must be added to cover all related REST API endpoints)

Because of this, the odds that some permissions are not properly enforced is higher than it should be if testing REST API permissions checks was easier.

The solution

The short version is:

  • Move the permission checks that are embedded in functions in middlewares into their own package
  • Add minimal tooling to make it significantly easier to test the functions in this package compared to how it is currently done
  • Write tests and fixtures to improve test coverage

The two key elements of this solution are to:

  • Reduce the space to test from hundreds of REST API endpoints to a few dozen middlewares
  • Create only meaningfull variations, based on the sequence of middleware created by the routes

Implementation scope and future steps

  1. Implement the design for all the middleware found in the REST API routes file.
  2. Convert permissions checks into specific middleware and add tests (fork destination check for instance)
  3. When reviewing and implementing features that require new permission checks, isolate them in the permission package instead of inlining them in the function or creating a middleware

Technical details

See the pull request implementing this design at forgejo/forgejo#12512/files

Collecting middleware sequences from routes

It happens in testing only and is built in memory when the test or the server is initialized.

limiting-factor/forgejo@156e999ed9/modules/web/route.go (L114)

For instance,

DELETE /repos/{username}/{reponame}/issues/{index}/pin implemented by function repo.UnpinIssue is checked with the middlewares (in that order):

  • v1.tokenRequiresScopes Write Issue
  • v1.repoAccess
  • v1.checkTokenPublicOnly
  • v1.mustEnableIssuesOrPulls
  • v1.mustEnableLocalIssuesIfIsIssue
  • v1.reqToken

Fixtures for testing a permission function

For each permission function backing the corresponding middleware (for instance ReqOrgOwnership)

funcReqOrgOwnership(ctxContext){
ifIsUserSiteAdmin(ctx){
return
}
varorgIDint64
ifctx.GetOrg()!=nil{
orgID=ctx.GetOrg().ID
}elseifctx.GetTeam()!=nil{
orgID=ctx.GetTeam().OrgID
}else{
ctx.Error(http.StatusInternalServerError,"","reqOrgOwnership: unprepared context")
return
}
isOwner,err:=organization.IsOrganizationOwner(ctx.GetContext(),orgID,ctx.GetDoer().ID)
iferr!=nil{
ctx.Error(http.StatusInternalServerError,"IsOrganizationOwner",err)
return
}elseif!isOwner{
ifctx.GetOrg()!=nil{
ctx.Error(http.StatusForbidden,"","Must be an organization owner")
}else{
ctx.NotFound()
}
return
}
}

a set of fixtures is registered to create all the scenario to provide full coverage of the function.

fixtures:[]*fixtureType{
{
data:newFixtureData(map[string]string{
"org":"ReqOrgOwnershipOrg",
"setOrg":"true",
}),
},
{
data:newFixtureData(map[string]string{
"doer":"doeradmin",
"setOrg":"true",
}),
},

Testing

A single test:

  • Collect all middleware sequences created when building the routes
  • For each permission middleware loop over all the fixtures that were registered to assert their result
  • For each sequence run the permission checks in the expected order to assert the sequence can be successfully run to completion
funcTestAPIv1Permissions(t*testing.T){
defertest.MockVariableValue(&setting.Service.DefaultAllowCreateOrganization,true)()
defertest.MockVariableValue(&setting.IsInTesting,true)()
defertest.MockVariableValue(&setting.DisableGitHooks,false)()
unittest.PrepareTestEnv(t)
// because setting.IsInTesting == true, it will record the
// middleware sequence of each route it builds
apiv1.Routes()
buildSignatureStringToFunctionTest(t)
runs:=0
t.Logf("running all fixtures for each permission function")
for_,sequence:=rangegetPermissionSequencesForFunctions(t){
runs+=testSequence(t,sequence,false)
}
t.Logf("verify all unique permission sequences can run successfully")
uniqueSequences:=apiv1_permissions_tests.GetUniquePermissionsSequences()
for_,sequence:=rangeuniqueSequences{
runs+=testSequence(t,sequence,true)
}
**Pull request:** https://codeberg.org/forgejo/forgejo/pulls/12512/files ## The situation The REST API permissions checks are partly implemented in middlewares inserted in the routes leading to each endpoint. In addition the function implementing the endpoint sometimes implements permissions checks of their own. A number of tests are implemented as integration tests that create various scenarios to verify the permissions are enforced when a REST API call happens, for instance when trying to access an issue in a private repository. The test coverage for these checks improved over time, most notably when a security bug is fixed. ## The problem With hundreds of REST API endpoints and the current strategy, implementing exhaustive tests ensuring the permissions checks for each REST API endpoint are enforced would require creating variations (private repository, etc.) for each of them. It is: - Exhausting for the developer (identifying which variations are relevant for a given REST API endpoint is tedious and error prone) - Very challenging to maintain (when a new permission check is added, the existing tests may lack coverage) - More difficult to fix permissions enforcement related security fixes (tests must be added to cover all related REST API endpoints) Because of this, the odds that some permissions are not properly enforced is higher than it should be if testing REST API permissions checks was easier. ## The solution The short version is: - Move the permission checks that are embedded in functions in middlewares into their own package - Add minimal tooling to make it significantly easier to test the functions in this package compared to how it is currently done - Write tests and fixtures to improve test coverage The two key elements of this solution are to: - Reduce the space to test from hundreds of REST API endpoints to a few dozen middlewares - Create only meaningfull variations, based on the sequence of middleware created by the routes ## Implementation scope and future steps 1. [Implement the design](https://codeberg.org/forgejo/forgejo/pulls/12512/files ) for all the middleware found in the REST API routes file. 1. Convert permissions checks into specific middleware and add tests (fork destination check for instance) 1. When reviewing and implementing features that require new permission checks, isolate them in the permission package instead of inlining them in the function or creating a middleware ## Technical details See the pull request implementing this design at https://codeberg.org/forgejo/forgejo/pulls/12512/files ### Collecting middleware sequences from routes It happens in testing only and is built in memory when the test or the server is initialized. https://codeberg.org/limiting-factor/forgejo/src/commit/156e999ed9de2dd639d8f2cdc1f732ce8538e87a/modules/web/route.go#L114 For instance, `DELETE /repos/{username}/{reponame}/issues/{index}/pin` implemented by function `repo.UnpinIssue` is checked with the middlewares (in that order): - `v1.tokenRequiresScopes Write Issue` - `v1.repoAccess` - `v1.checkTokenPublicOnly` - `v1.mustEnableIssuesOrPulls` - `v1.mustEnableLocalIssuesIfIsIssue` - `v1.reqToken` ### Fixtures for testing a permission function For each permission function backing the corresponding middleware (for instance ReqOrgOwnership) https://codeberg.org/limiting-factor/forgejo/src/commit/216f2d81fac39f9e9c0d43e98210b357cdf137e2/routers/api/v1/permissions/req_org_ownership.go#L12-L39 a set of fixtures is registered to create all the scenario to provide full coverage of the function. https://codeberg.org/limiting-factor/forgejo/src/commit/216f2d81fac39f9e9c0d43e98210b357cdf137e2/routers/api/v1/permissions/tests/req_org_ownership_test.go#L48-L60 ### Testing A single test: - Collect all middleware sequences created when building the routes - For each permission middleware loop over all the fixtures that were registered to assert their result - For each sequence run the permission checks in the expected order to assert the sequence can be successfully run to completion https://codeberg.org/limiting-factor/forgejo/src/commit/216f2d81fac39f9e9c0d43e98210b357cdf137e2/routers/api/v1/permissions/tests/functions_test.go#L214-L236
Author
Member
Copy link

For the record.

I looked over @mfenniak plan to Split oauth2.go and basic.go into all the different types of authentication they support - access token, action task, action task JWT, oauth JWT, and username/password.

Refactor: Remove URL-based authentication detection in auth methods
Auth methods currently have URL allowlists built into them because they're not configured on a per-route basis in the middleware -- it would be great to clean this up

  • Currently can't figure out a good way to do this -- because there's top-level authn, then authz, then things like require-sign-in, the auth can't be moved to the route-level without duplicating all these other related middlewares (possible, but messy) or finding a route-tagging metadata concept. As this isn't strictly needed for trusted issuers, I'm thinking to defer it.

This may have an impact, if not deferred.

As for the rest I think it is transparent to how permissions are currently verified.

For the record. I looked over @mfenniak plan to [Split oauth2.go and basic.go into all the different types of authentication they support - access token, action task, action task JWT, oauth JWT, and username/password](https://cryptpad.fr/sheet/#/2/sheet/view/ME3esYQ6sH2hYN98BuYDWoLLvrHXMtF975fnYOPk1+Q/). > Refactor: Remove URL-based authentication detection in auth methods > Auth methods currently have URL allowlists built into them because they're not configured on a per-route basis in the middleware -- it would be great to clean this up > - Currently can't figure out a good way to do this -- because there's top-level authn, then authz, then things like require-sign-in, the auth can't be moved to the route-level without duplicating all these other related middlewares (possible, but messy) or finding a route-tagging metadata concept. As this isn't strictly needed for trusted issuers, I'm thinking to defer it. This may have an impact, if not deferred. As for the rest I think it is transparent to how permissions are currently verified.
Author
Member
Copy link

There are 47 middleware (that includes all variations of parameters) checking for the permissions of 444 API endpoints. They are used in 89 unique sequences.

API endpoints: 444
unique permissions sequences: 89
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.mustEnableAttachments,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes Organization User,v1.checkTokenPublicOnly,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeReleases
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.reqValidCommentID
v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoWriter TypeCode,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes User,v1.reqToken,v1.checkTokenPublicOnly
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode
v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.tokenRequiresScopes Repository,v1.reqExploreSignIn
v1.tokenRequiresScopes User,v1.reqToken,v1.tokenRequiresScopes Repository
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions,v1.reqToken,v1.reqRepoWriter TypeActions
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgOwnership,v1.reqWebhooksEnabled
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.mustEnableAttachments
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions,v1.reqToken,v1.reqRepoWriter TypeActions,v1.mustNotBeArchived
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly
v1.tokenRequiresScopes User,v1.reqToken,v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly
v1.tokenRequiresScopes Organization,v1.reqToken,v1.reqTeamMembership,v1.checkTokenPublicOnly
v1.repoAccess,v1.tokenRequiresRepoOwnerScope,v1.Routes.func2.reqOwner.18
v1.tokenRequiresScopes Package,v1.checkTokenPublicOnly,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqValidCommentID
v1.tokenRequiresScopes Organization Repository,v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken
v1.tokenRequiresScopes Issue
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustNotBeArchived,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqValidCommentID,v1.reqToken
v1.reqToken,v1.tokenRequiresScopes Repository
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgMembership
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki,v1.mustNotBeArchived,v1.reqToken,v1.reqRepoWriter TypeWiki
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue
v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqSelfOrAdmin,v1.reqBasicOrRevProxyAuth,v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoWriter TypeIssues TypePullRequests
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoWriter TypeCode
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAdmin,v1.reqToken
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgOwnership
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAdmin,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes Notification,v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.reqAdmin
v1.tokenRequiresScopes Repository
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoReader TypeCode
v1.reqToken,v1.tokenRequiresScopes Organization User
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoReader TypeCode,v1.checkForkDestination
v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqRepoReader TypeIssues
v1.tokenRequiresScopes User,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader,v1.reqToken,v1.reqAdmin
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOwner
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAnyRepoReader
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssues,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.mustNotBeArchived
v1.tokenRequiresScopes Package,v1.checkTokenPublicOnly
v1.tokenRequiresScopes User,v1.reqToken,v1.reqWebhooksEnabled
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqToken
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustNotBeArchived,v1.reqRepoWriter TypeCode
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.reqGitHook
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeReleases,v1.reqToken,v1.reqRepoWriter TypeReleases
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOwner,v1.reqAdmin
v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoWriter TypeCode,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoBranchWriter,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader,v1.reqAdmin,v1.reqToken
v1.tokenRequiresScopes Repository,v1.reqToken
v1.tokenRequiresScopes Organization,v1.reqToken
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.reqToken
v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqSelfOrAdmin
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustEnableAttachments,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqOwner,v1.reqAdmin
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki,v1.reqToken,v1.mustNotBeArchived,v1.reqRepoWriter TypeWiki
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustEnableAttachments
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID
v1.tokenRequiresScopes Notification,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqToken,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoWriter TypeCode,v1.mustNotBeArchived
v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.reqWebhooksEnabled
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived,v1.reqRepoReader TypeIssues
v1.tokenRequiresScopes Organization,v1.reqToken,v1.reqTeamMembership,v1.checkTokenPublicOnly,v1.reqOrgOwnership
v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqExploreSignIn
v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgMembership,v1.reqOrgOwnership
v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls
v1.tokenRequiresScopes User,v1.reqExploreSignIn,v1.reqUsersExploreEnabled
middlewares: 47
v1.reqOrgMembership
v1.reqRepoWriter TypeReleases
v1.mustEnableWiki
v1.reqAdmin
v1.reqBasicOrRevProxyAuth
v1.reqUsersExploreEnabled
v1.reqToken
v1.mustAllowPulls
v1.reqExploreSignIn
v1.tokenRequiresScopes Admin
v1.reqRepoReader TypeIssues
v1.reqRepoWriter TypeWiki
v1.mustNotBeArchived
v1.reqValidCommentID
v1.mustEnableLocalIssuesIfIsIssue
v1.reqRepoReader TypeReleases
v1.tokenRequiresScopes Organization User
v1.repoAccess
v1.checkForkDestination
v1.reqSelfOrAdmin
v1.tokenRequiresRepoOwnerScope
v1.checkTokenPublicOnly
v1.tokenRequiresScopes Issue
v1.reqOrgOwnership
v1.individualPermsChecker
v1.reqAnyRepoReader
v1.reqOwner
v1.mustEnableIssues
v1.reqRepoWriter TypeCode
v1.reqGitHook
v1.tokenRequiresScopes Organization
v1.reqSiteAdmin
v1.reqTeamMembership
v1.reqRepoBranchWriter
v1.tokenRequiresScopes Package
v1.tokenRequiresScopes Organization Repository
v1.tokenRequiresScopes User
v1.reqRepoReader TypeActions
v1.reqRepoWriter TypeActions
v1.reqRepoReader TypeCode
v1.mustEnableIssuesOrPulls
v1.Routes.func2.reqOwner.18
v1.reqWebhooksEnabled
v1.tokenRequiresScopes Repository
v1.mustEnableAttachments
v1.tokenRequiresScopes Notification
v1.reqRepoWriter TypeIssues TypePullRequests
There are 47 middleware (that includes all variations of parameters) checking for the permissions of 444 API endpoints. They are used in 89 unique sequences. ``` API endpoints: 444 unique permissions sequences: 89 v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.mustEnableAttachments,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes Organization User,v1.checkTokenPublicOnly,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeReleases v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.reqValidCommentID v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoWriter TypeCode,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes User,v1.reqToken,v1.checkTokenPublicOnly v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.tokenRequiresScopes Repository,v1.reqExploreSignIn v1.tokenRequiresScopes User,v1.reqToken,v1.tokenRequiresScopes Repository v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions,v1.reqToken,v1.reqRepoWriter TypeActions v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgOwnership,v1.reqWebhooksEnabled v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.mustEnableAttachments v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeActions,v1.reqToken,v1.reqRepoWriter TypeActions,v1.mustNotBeArchived v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly v1.tokenRequiresScopes User,v1.reqToken,v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly v1.tokenRequiresScopes Organization,v1.reqToken,v1.reqTeamMembership,v1.checkTokenPublicOnly v1.repoAccess,v1.tokenRequiresRepoOwnerScope,v1.Routes.func2.reqOwner.18 v1.tokenRequiresScopes Package,v1.checkTokenPublicOnly,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqValidCommentID v1.tokenRequiresScopes Organization Repository,v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken v1.tokenRequiresScopes Issue v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustNotBeArchived,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqValidCommentID,v1.reqToken v1.reqToken,v1.tokenRequiresScopes Repository v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgMembership v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki,v1.mustNotBeArchived,v1.reqToken,v1.reqRepoWriter TypeWiki v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqSelfOrAdmin,v1.reqBasicOrRevProxyAuth,v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoWriter TypeIssues TypePullRequests v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoWriter TypeCode v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAdmin,v1.reqToken v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgOwnership v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAdmin,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes Notification,v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.reqAdmin v1.tokenRequiresScopes Repository v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoReader TypeCode v1.reqToken,v1.tokenRequiresScopes Organization User v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoReader TypeCode,v1.checkForkDestination v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqRepoReader TypeIssues v1.tokenRequiresScopes User,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader,v1.reqToken,v1.reqAdmin v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOwner v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAnyRepoReader v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssues,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.mustNotBeArchived v1.tokenRequiresScopes Package,v1.checkTokenPublicOnly v1.tokenRequiresScopes User,v1.reqToken,v1.reqWebhooksEnabled v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqToken v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustNotBeArchived,v1.reqRepoWriter TypeCode v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.reqGitHook v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeReleases,v1.reqToken,v1.reqRepoWriter TypeReleases v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOwner,v1.reqAdmin v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoWriter TypeCode,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqRepoReader TypeCode,v1.reqToken,v1.reqRepoBranchWriter,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader,v1.reqAdmin,v1.reqToken v1.tokenRequiresScopes Repository,v1.reqToken v1.tokenRequiresScopes Organization,v1.reqToken v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.reqToken v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqSelfOrAdmin v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.mustEnableLocalIssuesIfIsIssue,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustEnableAttachments,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqOwner,v1.reqAdmin v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableWiki,v1.reqToken,v1.mustNotBeArchived,v1.reqRepoWriter TypeWiki v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqAnyRepoReader v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID,v1.mustEnableAttachments v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqValidCommentID v1.tokenRequiresScopes Notification,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustAllowPulls,v1.reqRepoReader TypeCode,v1.reqToken,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqRepoWriter TypeCode,v1.mustNotBeArchived v1.tokenRequiresScopes Repository,v1.repoAccess,v1.checkTokenPublicOnly,v1.reqToken,v1.reqAdmin,v1.reqWebhooksEnabled v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived,v1.reqRepoReader TypeIssues v1.tokenRequiresScopes Organization,v1.reqToken,v1.reqTeamMembership,v1.checkTokenPublicOnly,v1.reqOrgOwnership v1.tokenRequiresScopes User,v1.checkTokenPublicOnly,v1.individualPermsChecker,v1.reqExploreSignIn v1.tokenRequiresScopes Organization,v1.checkTokenPublicOnly,v1.reqToken,v1.reqOrgMembership,v1.reqOrgOwnership v1.tokenRequiresScopes Issue,v1.repoAccess,v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls v1.tokenRequiresScopes User,v1.reqExploreSignIn,v1.reqUsersExploreEnabled middlewares: 47 v1.reqOrgMembership v1.reqRepoWriter TypeReleases v1.mustEnableWiki v1.reqAdmin v1.reqBasicOrRevProxyAuth v1.reqUsersExploreEnabled v1.reqToken v1.mustAllowPulls v1.reqExploreSignIn v1.tokenRequiresScopes Admin v1.reqRepoReader TypeIssues v1.reqRepoWriter TypeWiki v1.mustNotBeArchived v1.reqValidCommentID v1.mustEnableLocalIssuesIfIsIssue v1.reqRepoReader TypeReleases v1.tokenRequiresScopes Organization User v1.repoAccess v1.checkForkDestination v1.reqSelfOrAdmin v1.tokenRequiresRepoOwnerScope v1.checkTokenPublicOnly v1.tokenRequiresScopes Issue v1.reqOrgOwnership v1.individualPermsChecker v1.reqAnyRepoReader v1.reqOwner v1.mustEnableIssues v1.reqRepoWriter TypeCode v1.reqGitHook v1.tokenRequiresScopes Organization v1.reqSiteAdmin v1.reqTeamMembership v1.reqRepoBranchWriter v1.tokenRequiresScopes Package v1.tokenRequiresScopes Organization Repository v1.tokenRequiresScopes User v1.reqRepoReader TypeActions v1.reqRepoWriter TypeActions v1.reqRepoReader TypeCode v1.mustEnableIssuesOrPulls v1.Routes.func2.reqOwner.18 v1.reqWebhooksEnabled v1.tokenRequiresScopes Repository v1.mustEnableAttachments v1.tokenRequiresScopes Notification v1.reqRepoWriter TypeIssues TypePullRequests ```

I have a few thoughts noted on forgejo/forgejo#12398 (comment).

Collecting middlewares for debug output is very cool. Would it be possible to do this in a way that outputs each route and the associated middlewares? I feel like dropping that into a spreadsheet and reviewing it would be an interesting way to spot potential check gaps.

@limiting-factor wrote in #63 (comment):

Move the permission checks that are embedded in functions in middlewares

As the outcome of a permission check can only vary based on the parameters provided when calling the REST API endpoint, they can be converted into middlewares. There is no situation when such a permission check cannot happen before the function is called. In the worst case additional work needs to be done to extract the information required for the check.

I'm not quite sure what you wanted to communicate with these sentences. A lot of Forgejo's permission checks occur in middleware today, and I'm sure that there are some more that could move into middleware, gaining the ability to be reused for different implementations, and gaining the ability to be tested in a unit test / isolated test more easily. But I'm skeptical of the idea that all permission checks can occur in middleware.

For example:

  • Most read APIs do some form of data filtering to restrict the user's visibility into data. This is typically done by imperatively defining list options or database query filters that are relevant for the data being filtered. Theoretically middleware that wraps the request and inspects the response could, possibly, implement data filtering -- it's difficult because it would need to be able to know the types of a response and identify the right filtering to apply -- but it would break every paginated or search-based API.
  • The behaviour of some APIs varies depending on permissions, such as /repos/{owner}/{repo}/transfer.
  • Some APIs juggle multiple objects, where the amount of context required to uplift the logic into a middleware doesn't seem practical.
    • For example, GET /repos/{owner}/{repo}/issues/{index}/blocks and GET /repos/{owner}/{repo}/issues/{index}/dependencies have to filter their responses based upon visibility both the {owner}/{repo} repo, and the repo that is attached to the issue dependency.
    • POST /repos/{owner}/{repo}/issues/{index}/blocks has a similar problem where the write check is against the repo in the JSON API body, and the read check is against the repo in the URL path, the opposite of .../dependencies. A middleware that needs to understand the contents of form submissions in this narrow context seems impractical.

Some of these examples could probably be handled with ultra-specific middleware, which still gains some benefit in terms of testability and reusability -- but the read API filtering seems to be a situation where it's hard to imagine separating the API implementation in this way.

I have a few thoughts noted on https://codeberg.org/forgejo/forgejo/pulls/12398#issuecomment-14680299. Collecting middlewares for debug output is very cool. Would it be possible to do this in a way that outputs each route and the associated middlewares? I feel like dropping that into a spreadsheet and reviewing it would be an interesting way to spot potential check gaps. @limiting-factor wrote in https://codeberg.org/forgejo/design/issues/63#issue-4908308: > Move the permission checks that are embedded in functions in middlewares > > As the outcome of a permission check can only vary based on the parameters provided when calling the REST API endpoint, they can be converted into middlewares. There is no situation when such a permission check cannot happen before the function is called. In the worst case additional work needs to be done to extract the information required for the check. I'm not quite sure what you wanted to communicate with these sentences. A *lot* of Forgejo's permission checks occur in middleware today, and I'm sure that there are *some* more that could move into middleware, gaining the ability to be reused for different implementations, and gaining the ability to be tested in a unit test / isolated test more easily. But I'm skeptical of the idea that all permission checks can occur in middleware. For example: - Most read APIs do some form of data filtering to restrict the user's visibility into data. This is typically done by imperatively defining list options or database query filters that are relevant for the data being filtered. *Theoretically* middleware that wraps the request and inspects the response could, possibly, implement data filtering -- it's difficult because it would need to be able to know the types of a response and identify the right filtering to apply -- but it would break every paginated or search-based API. - The behaviour of some APIs varies depending on permissions, such as `/repos/{owner}/{repo}/transfer`. - Some APIs juggle multiple objects, where the amount of context required to uplift the logic into a middleware doesn't seem practical. - For example, `GET /repos/{owner}/{repo}/issues/{index}/blocks` and `GET /repos/{owner}/{repo}/issues/{index}/dependencies` have to filter their responses based upon visibility both the `{owner}/{repo}` repo, and the repo that is attached to the issue dependency. - `POST /repos/{owner}/{repo}/issues/{index}/blocks` has a similar problem where the write check is against the repo in the JSON API body, and the read check is against the repo in the URL path, the opposite of `.../dependencies`. A middleware that needs to understand the contents of form submissions in this narrow context seems impractical. Some of these examples could probably be handled with ultra-specific middleware, which still gains some benefit in terms of testability and reusability -- but the read API filtering seems to be a situation where it's hard to imagine separating the API implementation in this way.
Author
Member
Copy link

@mfenniak wrote in #63 (comment):

Collecting middlewares for debug output is very cool. Would it be possible to do this in a way that outputs each route and the associated middlewares? I feel like dropping that into a spreadsheet and reviewing it would be an interesting way to spot potential check gaps.

That is a low hanging fruit and can even easily be done independently of this pull request. For instance, here is what the current draft outputs in debug mode when running tests:

...
POST /admin/unadopted/{username}/{reponame}: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.AdoptRepository
POST /admin/unadopted/{username}/{reponame}: middlewares [v1.Routes.sudo] for function admin.AdoptRepository
DELETE /admin/unadopted/{username}/{reponame}: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.DeleteUnadoptedRepository
DELETE /admin/unadopted/{username}/{reponame}: middlewares [v1.Routes.sudo] for function admin.DeleteUnadoptedRepository
GET /admin/hooks: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.ListHooks
GET /admin/hooks: middlewares [v1.Routes.sudo] for function admin.ListHooks
POST /admin/hooks: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.CreateHook
POST /admin/hooks: middlewares [v1.bind[...] v1.Routes.sudo] for function admin.CreateHook
...
@mfenniak wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-14681172: > Collecting middlewares for debug output is very cool. Would it be possible to do this in a way that outputs each route and the associated middlewares? I feel like dropping that into a spreadsheet and reviewing it would be an interesting way to spot potential check gaps. That is a low hanging fruit and can even easily be done independently of this pull request. For instance, here is what the current draft outputs in debug mode when running tests: ``` ... POST /admin/unadopted/{username}/{reponame}: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.AdoptRepository POST /admin/unadopted/{username}/{reponame}: middlewares [v1.Routes.sudo] for function admin.AdoptRepository DELETE /admin/unadopted/{username}/{reponame}: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.DeleteUnadoptedRepository DELETE /admin/unadopted/{username}/{reponame}: middlewares [v1.Routes.sudo] for function admin.DeleteUnadoptedRepository GET /admin/hooks: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.ListHooks GET /admin/hooks: middlewares [v1.Routes.sudo] for function admin.ListHooks POST /admin/hooks: permissions v1.tokenRequiresScopes Admin,v1.reqToken,v1.reqSiteAdmin for function admin.CreateHook POST /admin/hooks: middlewares [v1.bind[...] v1.Routes.sudo] for function admin.CreateHook ... ```
Author
Member
Copy link

@mfenniak wrote in #63 (comment):

I'm not quite sure what you wanted to communicate with these sentences.

You correctly inferred what I meant. Congratulations and my apologies for not being clear. The examples you provided are very useful, thank you. I will examine them closely and figure out if they contradict my assumptions 🤔

In the hope to clarify those assumptions...

Some of these examples could probably be handled with ultra-specific middleware,...

My assumption here is that such specific middleware can cover all permission checks. Another example similar to transfer is how fork needs to check access of the destination, based on body parameters.

Most read APIs do some form of data filtering to restrict the user's visibility into data.

My other assumption is that as long as such filtering changes the data returned but does not yield a permission denied error, it is not in scope. Ideally there would be a way to ensure that such code never leaks data, but this is a much more ambitious goal and I have no idea how to achieve it. The goal of this proposal is to exhaustively test the permission checks that are already clearly identified as such because they explicitly fail with an error if not met (either in a middleware or in the code).

@mfenniak wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-14681172: > I'm not quite sure what you wanted to communicate with these sentences. You correctly inferred what I meant. Congratulations and my apologies for not being clear. The examples you provided are very useful, thank you. I will examine them closely and figure out if they contradict my assumptions 🤔 In the hope to clarify those assumptions... > Some of these examples could probably be handled with ultra-specific middleware,... My assumption here is that such specific middleware can cover all permission checks. Another example similar to `transfer` is how fork needs to check access of the destination, based on body parameters. > Most read APIs do some form of data filtering to restrict the user's visibility into data. My other assumption is that as long as such filtering changes the data returned but does not yield a permission denied error, it is not in scope. Ideally there would be a way to ensure that such code never leaks data, but this is a much more ambitious goal and I have no idea how to achieve it. The goal of this proposal is to exhaustively test the permission checks that are already clearly identified as such because they explicitly fail with an error if not met (either in a middleware or in the code).

@limiting-factor wrote in #63 (comment):

My other assumption is that as long as such filtering changes the data returned but does not yield a permission denied error, it is not in scope. Ideally there would be a way to ensure that such code never leaks data, but this is a much more ambitious goal and I have no idea how to achieve it. The goal of this proposal is to exhaustively test the permission checks that are already clearly identified as such because they explicitly fail with an error if not met (either in a middleware or in the code).

That's fair; not every problem can be solved at once, and this is already ambitious and valuable. 👍

@limiting-factor wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-14687646: > My other assumption is that as long as such filtering changes the data returned but does not yield a permission denied error, it is not in scope. Ideally there would be a way to ensure that such code never leaks data, but this is a much more ambitious goal and I have no idea how to achieve it. The goal of this proposal is to exhaustively test the permission checks that are already clearly identified as such because they explicitly fail with an error if not met (either in a middleware or in the code). That's fair; not every problem can be solved at once, and this is already ambitious and valuable. 👍
Author
Member
Copy link

Here are my findings on @mfenniak provided examples:

/repos/{owner}/{repo}/transfer

The permission checks (on the destination organization and teams) can be moved to a specific middleware.

if!ctx.IsUserSiteAdmin()&&newOwner.Visibility==api.VisibleTypePrivate&&!organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx,ctx.Doer.ID){


for_,tID:=range*opts.TeamIDs{
team,err:=organization.GetTeamByID(ctx,tID)
iferr!=nil{
ctx.Error(http.StatusUnprocessableEntity,"team",fmt.Errorf("team %d not found",tID))
return
}
ifteam.OrgID!=org.ID{
ctx.Error(http.StatusForbidden,"team",fmt.Errorf("team %d belongs not to org %d",tID,org.ID))
return
}

GET /repos/{owner}/{repo}/issues/{index}/blocks ...

The permissions for the target repository are obtained

targetPerm:=getPermissionForRepo(ctx,target.Repo)
ifctx.Written(){
return
}

and used at

if!targetPerm.CanWriteIssuesOrPulls(target.IsPull){
// We can't write to the target
ctx.NotFound()
return
}
if!dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull){
// We can't read the dependency
ctx.NotFound()
return
}

this can also be moved to a middleware that knows to extract the target repository from the body.

... and GET /repos/{owner}/{repo}/issues/{index}/dependencies have to filter their responses based upon visibility both the {owner}/{repo} repo, and the repo that is attached to the issue dependency.

it uses the same checks / body form as user blocking and can share the permission checking middleware

dependencyPerm:=getPermissionForRepo(ctx,dependency.Repo)
ifctx.Written(){
return
}
createIssueDependency(ctx,target,dependency,ctx.Repo.Permission,*dependencyPerm)
ifctx.Written(){
return
}

A middleware that needs to understand the contents of form submissions in this narrow context seems impractical.

The downside of such a middleware is that it is separated from the API endpoint implementation. It is easier to understand when reading the code that both implements the action and the permission checks. The upside is that it extracts the permission logic and clearly separates it from the action logic. The action logic still has the burden of not reaching out beyond what is allowed. There is nothing preventing a function implementing an endpoint from making all the database queries it wants.

As a conclusion I think the examples you provided do not exhibit code that would be unfit to extract permissions from. Please let me know if you think I missed something significant. Otherwise I'll just keep going with the implementation.

Here are my findings on @mfenniak provided examples: > /repos/{owner}/{repo}/transfer The permission checks (on the destination organization and teams) can be moved to a specific middleware. https://codeberg.org/forgejo/forgejo/src/commit/0e577ed6c97cb5648adb43402abcc7dd52a2feed/routers/api/v1/repo/transfer.go#L75 https://codeberg.org/forgejo/forgejo/src/commit/0e577ed6c97cb5648adb43402abcc7dd52a2feed/routers/api/v1/repo/transfer.go#L94-L104 > GET /repos/{owner}/{repo}/issues/{index}/blocks ... The permissions for the target repository are obtained https://codeberg.org/forgejo/forgejo/src/commit/0e577ed6c97cb5648adb43402abcc7dd52a2feed/routers/api/v1/repo/issue_dependency.go#L430-L433 and used at https://codeberg.org/forgejo/forgejo/src/commit/0e577ed6c97cb5648adb43402abcc7dd52a2feed/routers/api/v1/repo/issue_dependency.go#L570-L580 this can also be moved to a middleware that knows to extract the target repository from the body. > ... and GET /repos/{owner}/{repo}/issues/{index}/dependencies have to filter their responses based upon visibility both the {owner}/{repo} repo, and the repo that is attached to the issue dependency. it uses the same checks / body form as user blocking and can share the permission checking middleware https://codeberg.org/forgejo/forgejo/src/commit/0e577ed6c97cb5648adb43402abcc7dd52a2feed/routers/api/v1/repo/issue_dependency.go#L206-L214 > A middleware that needs to understand the contents of form submissions in this narrow context seems impractical. The downside of such a middleware is that it is separated from the API endpoint implementation. It is easier to understand when reading the code that both implements the action and the permission checks. The upside is that it extracts the permission logic and clearly separates it from the action logic. The action logic still has the burden of not reaching out beyond what is allowed. There is nothing preventing a function implementing an endpoint from making all the database queries it wants. As a conclusion I think the examples you provided do not exhibit code that would be unfit to extract permissions from. Please let me know if you think I missed something significant. Otherwise I'll just keep going with the implementation.
Author
Member
Copy link

All the pieces to run tests based on a combination of parameters (from the body of the request and a range of doer, repostory, private, public etc.) are in place and running.

There still are two blocking problems:

  • The generation of the relevant fixtures for testing each permission function in a sequence is not good
  • The test takes too long (currently 7m and not all middleware are in play)

Each permission function is currently associated with a list of fixture specifications which are expected to be generated and used to call the permission function to obtain the expected output (success or a specific error message).

However, a permission function is currently not independent. Other permission functions must be run before it otherwise it will fail. For instance they all require APIAuthorization to be called first. It is therefore necessary to generate the fixtures for all permissions functions in a given sequence in order to reach the last one. Creating fixtures specifications must therefore take into account the fixtures specifications of the functions that are called before it in the sequence. It is very difficult to think about knowing that a given function can happen in a variety of sequences.

I'm going to try another approach which consists of:

  • For each permission function,
    • define a set of fixtures (a permissions context and a set of database objects) and their expected outcome (error or success)
    • define a function that, given a fixture, make it so the permission function passes (the goal is not to verify its behavior but to make it so it does not get in the way, does not block the sequence of permission function when it is not the last)
  • Identifying the shortest sequence that ends with a permission function. For instance, out of:
    APIAuthorization,TokenRequiresScopes_Repository
    APIAuthorization,TokenRequiresScopes_Repository,RepoAccess,CheckTokenPublicOnly
    
    the shortest sequence to the TokenRequiresScopes_Repository function is
    APIAuthorization,TokenRequiresScopes_Repository
    
  • For permission function, run the shortest sequence that leads to it with each fixture it provides (possibly modified by the permission functions that are to run first) and verify the outcome is as expected (success or a specific error)
  • For all sequences (except those that are the shortest because they have already been checked), verify there is at least one fixture of the last function in the sequence can be used to successfully pass all permission checks in the sequence

If successful this approach will allow the developer to focus on creating the fixtures for each function in isolation, ensuring they provide the expected code coverage without complicated considerations about what follows in the sequence or what comes before it.

What I'm still unsure about is the complexity of writing the function that adds to the existing fixture to ensure the function passes. It may get complicated when and if some functions in the sequence have conflicting ways of doing that.

If it works out this should lead to a relatively small number of runs. There are less than 100 permission functions. The sum of all their fixtures is probably under 500. That is 500 runs. The sequences that are only meant to be verified to not be a deadend are also around 100. In total that's less than 1000 runs. They may take around five minutes, maybe less.

All the pieces to run tests based on a combination of parameters (from the body of the request and a range of doer, repostory, private, public etc.) are [in place and running](https://code.forgejo.org/forgefriends/forgefriends/src/commit/c8ba952c5a13ba1d069feb19f17cf4a74bbe8ab2/tests/integration/apiv1_permissions_test.go). There still are two blocking problems: - The generation of the relevant fixtures for testing each permission function in a sequence is not good - The test takes too long (currently 7m and not all middleware are in play) Each permission function is currently associated with a list of fixture specifications which are expected to be generated and used to call the permission function to obtain the expected output (success or a specific error message). However, a permission function is currently not independent. Other permission functions must be run before it otherwise it will fail. For instance they all require `APIAuthorization` to be called first. It is therefore necessary to generate the fixtures for all permissions functions in a given sequence in order to reach the last one. Creating fixtures specifications must therefore take into account the fixtures specifications of the functions that are called before it in the sequence. It is very difficult to think about knowing that a given function can happen in a variety of sequences. I'm going to try another approach which consists of: - For each permission function, - define a set of fixtures (a permissions context and a set of database objects) and their expected outcome (error or success) - define a function that, given a fixture, make it so the permission function passes (the goal is not to verify its behavior but to make it so it does not get in the way, does not block the sequence of permission function when it is not the last) - Identifying the shortest sequence that ends with a permission function. For instance, out of: ``` APIAuthorization,TokenRequiresScopes_Repository APIAuthorization,TokenRequiresScopes_Repository,RepoAccess,CheckTokenPublicOnly ``` the shortest sequence to the `TokenRequiresScopes_Repository` function is ``` APIAuthorization,TokenRequiresScopes_Repository ``` - For permission function, run the shortest sequence that leads to it with each fixture it provides (possibly modified by the permission functions that are to run first) and verify the outcome is as expected (success or a specific error) - For all sequences (except those that are the shortest because they have already been checked), verify there is at least one fixture of the last function in the sequence can be used to successfully pass all permission checks in the sequence If successful this approach will allow the developer to focus on creating the fixtures for each function in isolation, ensuring they provide the expected code coverage without complicated considerations about what follows in the sequence or what comes before it. What I'm still unsure about is the complexity of writing the function that adds to the existing fixture to ensure the function passes. It may get complicated when and if some functions in the sequence have conflicting ways of doing that. If it works out this should lead to a relatively small number of runs. There are less than 100 permission functions. The sum of all their fixtures is probably under 500. That is 500 runs. The sequences that are only meant to be verified to not be a deadend are also around 100. In total that's less than 1000 runs. They may take around five minutes, maybe less.
Author
Member
Copy link

code.forgejo.org/forgefriends/forgefriends@ddc85314b5/tests/integration/apiv1_permissions_test.go (L323-L345)

The implementation of the approach described in the previous comment is promising.

  • It runs under 30 seconds
  • It simplifies the writing of all test cases for each function

The complexity that remains is that fixtures have to be designed for a given function

  • not knowing which other permissions functions will run before it and must ensure it won't create a situation that make the previous functions fail, defeating the purpose of the feature for this particular function
  • when a fixture creates a user, comment, issue etc. it has to be named (a user by its name, an issue by its title) because that's the only way a sequence of permission function will be able to preserve the resources created by another fixture does not override them

For a sequence of permission functions under test, a fixture is created by:

  • Selecting one fixture per function
  • Merging all fixtures into one, based on the last in the sequence (the focus is always on the last function of the sequence)
  • When a fixture is merged into the existing one, it does so by examining the map[string]string that describes which resources will be created (for instance "doer": "regularuser", "repository": "ownerorg/repo1". If something it needs is missing, it will add it (for instance "scope": "read:repository" to indicate that the token created for the doer only has read permissions on the repository.
  • Each function in the sequence then interprets the merged fixture to create the resources it contains.
  • Each function in the sequence are run, in order. All but the last one are expected to succeed. The last one may fail because the fixture is designed in this way and the error it returns is asserted to be the expected one.

It is not convenient in this context to use the existing resource creating helpers for fixtures. They are all designed to be used by tests that precisely control which resource is created at a given time, because the caller lays out a scenario that is precisely described. Because these tests need to generate fixtures that may vary depending on how the permission functions are called and that these sequences are not known in advance, they are lacking. It may be that the function to create a resource is called multiple times and it should:

  • fail if called twice
  • succeed because it is idempotent
  • verify the symbolic name (user name, etc.) of the resource is exactly the same and succeed if called more than once (the doer for instance) or fail (because having two permissions functions that expect two different doers won't work)

The current tests only cover part of the permission functions and all tests pass, all fixtures are used and verified to be used at least once. The next steps are:

  • add more fixture for each existing functions to get full coverage. Some are very simple but CheckTokenPublicOnly is more challenging
  • refactor the remaining permissions functions of the REST API and add fixtures for all of them

The expected challenges are:

  • merging of fixtures in a sequence may get too complex
  • obtaining 100% code coverage is difficult because the architectural choice to run the tests or the assumptions it is based on are not right
https://code.forgejo.org/forgefriends/forgefriends/src/commit/ddc85314b57a55423ece52637011677705816844/tests/integration/apiv1_permissions_test.go#L323-L345 The implementation of the approach described in the previous comment is promising. - It runs [under 30 seconds](https://code.forgejo.org/forgefriends/forgefriends/actions/runs/682/jobs/3/attempt/1#jobstep-6-1811) - It simplifies the writing of all test cases for each function The complexity that remains is that fixtures have to be designed for a given function - not knowing which other permissions functions will run before it and must ensure it won't create a situation that make the previous functions fail, defeating the purpose of the feature for this particular function - when a fixture creates a user, comment, issue etc. it has to be named (a user by its name, an issue by its title) because that's the only way a sequence of permission function will be able to preserve the resources created by another fixture does not override them For a sequence of permission functions under test, a fixture is created by: - Selecting one fixture per function - Merging all fixtures into one, based on the last in the sequence (the focus is always on the last function of the sequence) - When a fixture is merged into the existing one, it does so by examining the `map[string]string` that describes which resources will be created (for instance `"doer": "regularuser", "repository": "ownerorg/repo1"`. If something it needs is missing, it will add it (for instance `"scope": "read:repository"` to indicate that the token created for the doer only has read permissions on the repository. - Each function in the sequence then interprets the merged fixture to create the resources it contains. - Each function in the sequence are run, in order. All but the last one are expected to succeed. The last one may fail because the fixture is designed in this way and the error it returns is asserted to be the expected one. It is not convenient in this context to use the existing resource creating helpers for fixtures. They are all designed to be used by tests that precisely control which resource is created at a given time, because the caller lays out a scenario that is precisely described. Because these tests need to generate fixtures that may vary depending on how the permission functions are called and that these sequences are not known in advance, they are lacking. It may be that the function to create a resource is called multiple times and it should: - fail if called twice - succeed because it is idempotent - verify the symbolic name (user name, etc.) of the resource is exactly the same and succeed if called more than once (the doer for instance) or fail (because having two permissions functions that expect two different doers won't work) The current tests only cover part of the permission functions and all tests pass, all fixtures are used and verified to be used at least once. The next steps are: - add more fixture for each existing functions to get full coverage. Some are very simple but `CheckTokenPublicOnly` is more challenging - refactor the remaining permissions functions of the REST API and add fixtures for all of them The expected challenges are: - merging of fixtures in a sequence may get too complex - obtaining 100% code coverage is difficult because the architectural choice to run the tests or the assumptions it is based on are not right
Author
Member
Copy link

code.forgejo.org/forgefriends/forgefriends@1ddf5688c3/tests/integration/apiv1_permissions_test.go

The implementation is complete although it still misses tests to get full coverage. There are:

96 unique sequence of permission functions
51 permissions functions

The tests still run under 30 seconds for a total of 199 fixtures (a big part of the speedup is to

  • only populate the git of a repository used in a fixture when it is required to exercise the permission check).
  • call LoadFixture() to only reset the database instead of resetting the entire test context

@limiting-factor wrote in #63 (comment):

The expected challenges are:

  • merging of fixtures in a sequence may get too complex

It turned out ok, although in some cases thinking about the interactions between the function under test and the functions that come before it in the sequence turned out to not be easy a few times.

The next steps are:

  • cleanup
  • update the pull request and ask for a first feedback
  • while waiting add more fixtures and measure coverage
https://code.forgejo.org/forgefriends/forgefriends/src/commit/1ddf5688c3f30737722e0e6e79f70165deb4b508/tests/integration/apiv1_permissions_test.go The implementation is complete although it still misses tests to get full coverage. There are: 96 unique sequence of permission functions 51 permissions functions The tests still run under 30 seconds for a total of 199 fixtures (a big part of the speedup is to - only populate the git of a repository used in a fixture when it is required to exercise the permission check). - call `LoadFixture()` to only reset the database instead of resetting the entire test context @limiting-factor wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-15184509: > The expected challenges are: > > * merging of fixtures in a sequence may get too complex It turned out ok, although in some cases thinking about the interactions between the function under test and the functions that come before it in the sequence turned out to not be easy a few times. The next steps are: - cleanup - update the pull request and ask for a first feedback - while waiting add more fixtures and measure coverage
Author
Member
Copy link

The cleanup is done at forgejo/forgejo#12512/files

The cleanup is done at https://codeberg.org/forgejo/forgejo/pulls/12512/files
limiting-factor changed title from (削除) Exhaustive testing of the REST API permissions checks (削除ここまで) to Facilitate and improve the testing of the REST API permissions checks 2026年05月21日 18:42:50 +02:00
Author
Member
Copy link

The "Exhaustive" word in the title was too ambitious 😁 I replaced it with a title that reflects a goal that is both more realistic and equally useful.

The "Exhaustive" word in the title was too ambitious 😁 I replaced it with a title that reflects a goal that is both more realistic and equally useful.
Author
Member
Copy link

I'm quite happy about the implementation forgejo/forgejo#12512/files and updated the design to match. It is quite large but also rather uncomplicated.

The size of the pull request is mostly because all of the 36 permission middleware in the REST API were refactored to use the interface and have testing. The intention is to demonstrate the design actually covers all cases. If only part of the functions were converted and tested, there would still be a doubt: will this design be a good fit for the remaining ones?

While waiting for a preliminary review, I will work on adding more fixtures to improve coverage. Even if it is not 100% (because testing fatal conditions such as database failure is not a reasonable goal) I hope it will be an improvement that is attractive enough to motivate reviewers to spend the time for a detailed review.

I will improve the commit series to make it easy to visually verify the refactor of each middleware function is only syntactic and not moving code around. I've not done it yet because it takes a long time and will make it more difficult to implement suggestions that requires architectural changes in the current pull request.

I'm quite happy about the implementation https://codeberg.org/forgejo/forgejo/pulls/12512/files and updated the design to match. It is quite large but also rather uncomplicated. The size of the pull request is mostly because all of the 36 permission middleware in the REST API were refactored to use the interface and have testing. The intention is to demonstrate the design actually covers all cases. If only part of the functions were converted and tested, there would still be a doubt: will this design be a good fit for the remaining ones? While waiting for a preliminary review, I will work on adding more fixtures to improve coverage. Even if it is not 100% (because testing fatal conditions such as database failure is not a reasonable goal) I hope it will be an improvement that is attractive enough to motivate reviewers to spend the time for a detailed review. I will improve the commit series to make it easy to visually verify the refactor of each middleware function is only syntactic and not moving code around. I've not done it yet because it takes a long time and will make it more difficult to implement suggestions that requires architectural changes in the current pull request.

Thanks @limiting-factor for the hard work!

The PR is quite challenging to read. Stupid idea: do you think that it would be possible to generate code for the test cases? So that reviewers can check the generated fixture, to see if it is sensible.

This could also serve as a kind of documentation.

Thinking out loud, the code seem to:

  • collects all the middleware chains protecting all the routes
  • performs deduplication
  • performs a test on the shortest chains, by auto-generating test data
  • performs other test on the other chains, by auto-generating test data

I am not sure of the value of the auto-generated tests; whereas I think that gathering the permission chains is a great idea!

I think it would be easier to review unit and integration tests, for the following definition of unit and integration:

  • a unit test would test one permission function
  • an integration test would test a permission chain (ideally by referring to the HTTP Method + Path)

Pseudo-code for a "unit" test:

funcTestAPIAuthorization(t*testing.T){// some setup code for permissionsAPIAuthorization(permissions)assert.Zero(t,permissions.GetStatus(),permissions.GetMessage())// other setup codeAPIAuthorization(permissions)assert.NotZero(t,permissions.GetStatus())}

Pseudo-code for an "integration" test:

funcTestSomethingSpecificRegardingUnadoption(t*testing.T){chain:=getMiddlewareChain(t,"POST","/admin/unadopted/{username}/{reponame}")// some setup codechain.MustAccept(doer,repo,scope,level,...)// other setup codechain.MustReject(doer,repo,scope,level,...)}

I think this would greatly ease review and increase confidence (it is harder to be convinced of the coverage report, if I can't easily understand how each branch was reached).

Anyway I have to sleep on this, before I am able to make an informed opinion. Just wanted to let you know my first impressions (and again, this is impressive work!)

Thanks @limiting-factor for the hard work! The PR is quite challenging to read. Stupid idea: do you think that it would be possible to generate code for the test cases? So that reviewers can check the generated fixture, to see if it is sensible. This could also serve as a kind of documentation. Thinking out loud, the code seem to: - collects all the middleware chains protecting all the routes - performs deduplication - performs a test on the shortest chains, by auto-generating test data - performs other test on the other chains, by auto-generating test data I am not sure of the value of the auto-generated tests; whereas I think that gathering the permission chains is a great idea! I think it would be easier to review unit and integration tests, for the following definition of unit and integration: - a unit test would test one permission function - an integration test would test a permission chain (ideally by referring to the HTTP Method + Path) Pseudo-code for a "unit" test: ```go func TestAPIAuthorization(t *testing.T){ // some setup code for permissions APIAuthorization(permissions) assert.Zero(t, permissions.GetStatus(), permissions.GetMessage()) // other setup code APIAuthorization(permissions) assert.NotZero(t, permissions.GetStatus()) } ``` Pseudo-code for an "integration" test: ```go func TestSomethingSpecificRegardingUnadoption(t *testing.T){ chain:=getMiddlewareChain(t, "POST", "/admin/unadopted/{username}/{reponame}") // some setup code chain.MustAccept(doer, repo, scope, level, ...) // other setup code chain.MustReject(doer, repo, scope, level, ...) } ``` I think this would greatly ease review and increase confidence (it is harder to be convinced of the coverage report, if I can't easily understand how each branch was reached). Anyway I have to sleep on this, before I am able to make an informed opinion. Just wanted to let you know my first impressions (and again, this is impressive work!)
Author
Member
Copy link

@oliverpool wrote in #63 (comment):

The PR is quite challenging to read.

It is very challenging and I intend to make it easier by splitting the refactor in two:

  • Refactor the middleware in the api.go file so that the diff shows only what changed (which is very little, only using accessors instead of data members for the most part)
  • Move the refactored functions verbatim to another file (not changing anything, just moving them)
@oliverpool wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-15739241: > The PR is quite challenging to read. It is very challenging and I intend to make it easier by splitting the refactor in two: - Refactor the middleware in the api.go file so that the diff shows only what changed (which is very little, only using accessors instead of data members for the most part) - Move the refactored functions verbatim to another file (not changing anything, just moving them)
Author
Member
Copy link

I am very grateful that you spent the time taking a look. 🙏

I am **very** grateful that you spent the time taking a look. 🙏
Author
Member
Copy link

@oliverpool wrote in #63 (comment):

  • performs a test on the shortest chains, by auto-generating test data

There is a misunderstanding: all the test data is hand made. I wish it would be possible to generate it but it is not. I tried that as a first approach but it leads to results that are very difficult to exploit, fail is various mysterious ways and take a long time to run.

whereas I think that gathering the permission chains is a great idea!

I'm glad you like the idea, it is the backbone of this refactor. Each chain / sequence of permissions is known to be tested (maybe not exhaustively but there is a quite decent coverage as can be seen in the description). The current tests for those chains are quite hard to find because they are mixed in a zillion integration tests. But they do exist somewhere as demonstrated by the coverage report.

a unit test would test one permission function

I think that's a good goal to have. But it is not possible without refactoring the semantics of the permissions functions because some of them have side effects that other permission functions down the sequence rely on.

The value of this pull request is, I think, to have all the tests in one place, easily identified and running quickly. As opposed to scattered all over the place and requiring a full run of all unit + integration tests to get good coverage.

This creates the conditions for a semantic refactor of those permission functions to improve their consistency and, for instance, to make them independent of each other so they can be unit tested.

@oliverpool wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-15739241: > * performs a test on the shortest chains, by auto-generating test data There is a misunderstanding: all the test data is hand made. I wish it would be possible to generate it but it is not. I tried that as a first approach but it leads to results that are very difficult to exploit, fail is various mysterious ways and take a long time to run. > whereas I think that gathering the permission chains is a great idea! I'm glad you like the idea, it is the backbone of this refactor. Each chain / sequence of permissions is known to be tested (maybe not exhaustively but there is a quite decent coverage as can be seen in the description). The current tests for those chains are quite hard to find because they are mixed in a zillion integration tests. But they do exist somewhere as demonstrated by the coverage report. > a unit test would test one permission function I think that's a good goal to have. But it is not possible without refactoring the semantics of the permissions functions because some of them have side effects that other permission functions down the sequence rely on. The value of this pull request is, I think, to have all the tests in one place, easily identified and running quickly. As opposed to scattered all over the place and requiring a full run of all unit + integration tests to get good coverage. This creates the conditions for a semantic refactor of those permission functions to improve their consistency and, for instance, to make them independent of each other so they can be unit tested.
Author
Member
Copy link

I updated the description of the pull request to show:

  • How each test can be reviewed for each fixture
  • Where errors in tests show to be readable to the developer (it shows which data was used and which functions fails)
I updated the [description of the pull request](https://codeberg.org/forgejo/forgejo/pulls/12512) to show: - How each test can be reviewed for each fixture - Where errors in tests show to be readable to the developer (it shows which data was used and which functions fails)
Author
Member
Copy link

@oliverpool wrote in #63 (comment):

I think this would greatly ease review and increase confidence (it is harder to be convinced of the coverage report, if I can't easily understand how each branch was reached).

The verbose logs of the test must show the state of the context before each call. It currently shows which data (map[string]string) is used to build that context. And I am very familiar with it right now, having no problem to go from their to what the context contains. But this requires an understanding that my future self (or another developer) does not have.

I'll work on improving the debug output, which will also ease the review: given the concrete context and the function, the reviewer can figure out which branch is covered by this particular fixture.

@oliverpool wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-15739241: > I think this would greatly ease review and increase confidence (it is harder to be convinced of the coverage report, if I can't easily understand how each branch was reached). The verbose logs of the test must show the state of the context before each call. It currently shows which data (`map[string]string`) is used to build that context. And I am very familiar with it right now, having no problem to go from their to what the context contains. But this requires an understanding that my future self (or another developer) does not have. I'll work on improving the debug output, which will also ease the review: given the concrete context and the function, the reviewer can figure out which branch is covered by this particular fixture.
Author
Member
Copy link

I have updated the links from the description of this design to be on the latest iteration of the draft pull request. It matters in the sense that the enumeration of fixtures and sequences under tests changed in between. The principle is the same but the details are quite different.

I have updated the links from the description of this design to be on the latest iteration of the draft pull request. It matters in the sense that the enumeration of fixtures and sequences under tests changed in between. The principle is the same but the details are quite different.
Author
Member
Copy link

@oliverpool I thought more about your code generation idea and I see the appeal:

  • Generating code that runs a test scenario for a given fixture and compiling it will catch errors at compile time that would otherwise only be caught when dynamically calling the function using reflect
  • Reviewing changes in tests can be done from the diff of the generated tests instead of comparing the verbose run output before and after the change
  • Isolating code coverage can be done by running one of the generated tests instead of commenting out the other fixtures

However it has its challenges:

  • The generation of the source file that creates the fixture and runs the sequence under test is complicated
  • It could be simplified if the code to create the fixture is not generated in the source file but that would partly defeat the purpose since the complexity is not in calling the sequence but in generating the fixture designed to exercise a given code path in the function under test

And may not be worth the effort because:

  • The REST API permission check sequences grew organically over the years and are in great need of refactoring
  • The tests proposed by that design will make such a refactoring possible by making it a lot easier to review and verify there is no regression
  • After a refactor happens and (for instance) no permission functions mutate the context they are given, it will be possible to refactor the test themselves and remove the complexity they contain to deal with this unsound behavior (permission checking mixed with side effects on the context they work on)

That said, maybe you see something simpler that I don't and I'll wait on your answer before doing anything. In the meantime I will keep adding fixtures and improving coverage. That won't be lost, whatever is decided in the end.

@oliverpool I thought more about your code generation idea and I see the appeal: - Generating code that runs a test scenario for a given fixture and compiling it will catch errors at compile time that would otherwise only be caught when dynamically calling the function using `reflect` - Reviewing changes in tests can be done from the diff of the generated tests instead of comparing the verbose run output before and after the change - Isolating code coverage can be done by running one of the generated tests instead of commenting out the other fixtures However it has its challenges: - The generation of the source file that creates the fixture and runs the sequence under test is complicated - It could be simplified if the code to create the fixture is not generated in the source file but that would partly defeat the purpose since the complexity is not in calling the sequence but in generating the fixture designed to exercise a given code path in the function under test And may not be worth the effort because: - The REST API permission check sequences grew organically over the years and are in great need of refactoring - The tests proposed by that design will make such a refactoring possible by making it a lot easier to review and verify there is no regression - After a refactor happens and (for instance) no permission functions mutate the context they are given, it will be possible to refactor the test themselves and remove the complexity they contain to deal with this unsound behavior (permission checking mixed with side effects on the context they work on) That said, maybe you see something simpler that I don't and I'll wait on your answer before doing anything. In the meantime I will keep adding fixtures and improving coverage. That won't be lost, whatever is decided in the end.
Author
Member
Copy link

A documentation was added in the test repository to explain the technical details for developers.

A documentation was added in the test repository to [explain the technical details](https://codeberg.org/forgejo/forgejo/src/commit/21416e1d60288e421fed6a430ecd3766ab242762/routers/api/v1/permissions/tests/README.md) for developers.
Author
Member
Copy link

All functions in the permissions package in the draft pull request now have maximum test coverage. They are at least as good as the one from running all unit / integration tests in the development branch and in a number of cases the coverage is better. No bug was discovered but there is deadcode in half a dozen functions, because of checks that are redundant with identical checks performed earlier in the sequence.

I think the draft pull request is in a good state and I'll wait on your feedback @oliverpool before doing more work. If a different architecture is preferred, I would rather not be too committed before engaging in a different direction 😅

All functions in the permissions package in the draft pull request now have [maximum test coverage](https://codeberg.org/forgejo/forgejo/pulls/12512/files). They are at least as good as the one from running all unit / integration tests in the development branch and in a number of cases the coverage is better. No bug was discovered but there is deadcode in half a dozen functions, because of checks that are redundant with identical checks performed earlier in the sequence. I think the draft pull request is in a good state and I'll wait on your feedback @oliverpool before doing more work. If a different architecture is preferred, I would rather not be too committed before engaging in a different direction 😅
Author
Member
Copy link

@mfenniak with contributions from @oliverpool the pull request implementing the design matured significantly and was organized to allow for a first review. While I'm quite confident the method to implement the tests is solid and won't need major changes, the architecture of the refactor itself still makes assumptions that may be challenged. For this reason, while the pull request contains commits that are organized to help with the review, it could be improved to facilitate a detailed review. I'm happy to make that extra effort but it may be a waste of time if architectural changes are needed. Once the design is agreed to be good enough, I'll update the pull request accordingly. I'm trying to find the right balance between providing a concrete demonstration of the design and becoming so committed to the work done that I'll be unreasonably resistant to design changes 😁

@mfenniak with contributions from @oliverpool the pull request implementing the design matured significantly and was organized to allow for a [first review](https://codeberg.org/forgejo/forgejo/pulls/12512). While I'm quite confident the method to implement the tests is solid and won't need major changes, the architecture of the refactor itself still makes assumptions that may be challenged. For this reason, while the pull request contains commits that are organized to help with the review, it could be improved to facilitate a detailed review. I'm happy to make that extra effort but it may be a waste of time if architectural changes are needed. Once the design is agreed to be good enough, I'll update the pull request accordingly. I'm trying to find the right balance between providing a concrete demonstration of the design and becoming so committed to the work done that I'll be unreasonably resistant to design changes 😁
Author
Member
Copy link

Discussions in the draft pull request conclude this design is good enough and it is time to polish the draft pull request so it can be merged. After it is merged, the next steps will be to:

Discussions [in the draft pull request](https://codeberg.org/forgejo/forgejo/pulls/12512#issuecomment-17760251) conclude this design is good enough and it is time to polish the draft pull request so it can be merged. After it is merged, the next steps will be to: - [Refactor the APIContext data members with the setter/getter pattern](https://codeberg.org/forgejo/forgejo/pulls/13143) - [Refactor `Base.Data["requiredScopeCategories"]/` into a setter/getter](https://codeberg.org/forgejo/forgejo/pulls/13138) - Backport those three pull requests to v15 to help with backporting and testing API permissions
Author
Member
Copy link

forgejo/forgejo#12512 is merged, the design is implemented. That's not the end of it. But it's a start.

https://codeberg.org/forgejo/forgejo/pulls/12512 is merged, the design is implemented. That's not the end of it. But it's a start.
Author
Member
Copy link

@oliverpool @mfenniak thank you both for taking the time to review and improve that design+code. I'm quite happy about how it went and will probably refer to it as the best outcome one can hope when proposing a 1,000+ lines refactor. The main takeaway from my point of view are:

  • Be very patient (the refactor matters to me, that does not mean it matters to someone else and even if it does, there is no reason for it to be high in their task list)
  • Write and update the design and supporting pull request descriptions so they are both always up to date with the latest reviews
  • Maintain a nice to look at commit series
  • Do not assume the reviewer will read or remember the discussion backlog (it also helped me ensure the design and pull request are consistent and do not drift as time passes)
  • Add something that is unequivocally beneficial to Forgejo (in this case, more coverage for security sensitive functions as well as a test framework that makes it easier to test them in the future)
  • Address all review comments without delay (when I'm lucky to catch the eye of a reviewer, there is a better chance of a quick turnaround if I reply immediately instead of a few days later)
  • Always prefer the path chosen by the reviewer (even if I went another direction that I like better, if the reviewer took the time to suggest another way the least I can do is to follow their suggestion rather than arguing against it)
  • Do not go into details too fast (in its first iteration, I I tried to draft the pull request supporting the design to illustrate it rather than preparing it to be merged: discussions on the design can lead to an entirely different pull request)
@oliverpool @mfenniak thank you both for taking the time to review and improve that design+code. I'm quite happy about how it went and will probably refer to it as the best outcome one can hope when proposing a 1,000+ lines refactor. The main takeaway from my point of view are: - Be very patient (the refactor matters to me, that does not mean it matters to someone else and even if it does, there is no reason for it to be high in their task list) - Write and update the design and supporting pull request descriptions so they are both always up to date with the latest reviews - Maintain a nice to look at commit series - Do not assume the reviewer will read or remember the discussion backlog (it also helped me ensure the design and pull request are consistent and do not drift as time passes) - Add something that is unequivocally beneficial to Forgejo (in this case, more coverage for security sensitive functions as well as a test framework that makes it easier to test them in the future) - Address all review comments without delay (when I'm lucky to catch the eye of a reviewer, there is a better chance of a quick turnaround if I reply immediately instead of a few days later) - Always prefer the path chosen by the reviewer (even if I went another direction that I like better, if the reviewer took the time to suggest another way the least I can do is to follow their suggestion rather than arguing against it) - Do not go into details too fast (in its first iteration, I I tried to draft the pull request supporting the design to illustrate it rather than preparing it to be merged: discussions on the design can lead to an entirely different pull request)

@limiting-factor I've had an opportunity come up to fix a permission issue and lean in to using your new permission model to support it. As I reached out in Matrix, I'd like to discuss some issues with you -- but I'll share the thoughts here for posterity, and for others to see. But recognize that I don't want to prescribe things here, I want to discuss, because I don't feel I have a full grasp on every factor involved in the design. 🙂

My main point of feedback is that the testing is difficult to understand, even with the documentation. I think these fixes would help:

  • functionTest.fixtures should be called .testCases, and in the existing usage of registerFunctionTest it should be moved to the top of each file to make it clear -- these are the things being tested.
  • map[string]string for... what I'm going to call as "data hints" in newFixtureData(x)... lacks any discoverability of available options for the key and value. I think it would benefit from a structured approach, map[TestData]TestDataOption, where TestData and TestDataOption are enums or strings with const values, so that you can find what options exist and are present
  • Documentation could include a step-by-step "how to add a new middleware and test it"

Smaller points of feedback are:

  • Performance of the test is somewhat terrible at 45s of test execution for testing ~30 tiny functions.
    • I assume DB work is dominating this, but haven't measured. I'd suggest that the entire thing could be implemented with in-memory objects without DB interaction required? Possibly.
    • Given that type Context interface is, an interface, mockery may also be a tool that can be used here rather than type Permissions struct, but I'm less sure there.
  • NotFound() test cases don't have a way to identify which exit point in the subject-under-test was hit, because we try to hide 404 details from users -- which also makes the tests blind to what they're testing in a lot of cases
  • The entire structure of the testing system is very non-Go-like. This makes it unfamiliar in a way that is difficult to get started with.
    • Changing fixtures: []*fixtureType into func(t *testing.T) which can do t.Run("...", ...) for each test case might start to look familiar? And allow me to do something like for _, s := range string[]{"a", "b", "c"} {... to code the tests (rare case, for sure) rather than hard-code an array.
    • I have fewer ideas here than I'd like, so that's the only one so far. 🙂
@limiting-factor I've had an opportunity come up to fix a permission issue and lean in to using your new permission model to support it. As I reached out in Matrix, I'd like to discuss some issues with you -- but I'll share the thoughts here for posterity, and for others to see. But recognize that I don't want to prescribe things here, I want to discuss, because I don't feel I have a full grasp on every factor involved in the design. 🙂 My main point of feedback is that the testing is difficult to understand, even with the documentation. I think these fixes would help: - `functionTest.fixtures` should be called `.testCases`, and in the existing usage of `registerFunctionTest` it should be moved to the top of each file to make it clear -- these are the things being tested. - `map[string]string` for... what I'm going to call as "data hints" in `newFixtureData(x)`... lacks any discoverability of available options for the key and value. I think it would benefit from a structured approach, `map[TestData]TestDataOption`, where `TestData` and `TestDataOption` are enums or strings with const values, so that you can find what options exist and are present - Documentation could include a step-by-step "how to add a new middleware and test it" Smaller points of feedback are: - Performance of the test is somewhat terrible at 45s of test execution for testing ~30 tiny functions. - I assume DB work is dominating this, but haven't measured. I'd suggest that the entire thing could be implemented with in-memory objects without DB interaction required? Possibly. - Given that `type Context interface` is, an interface, mockery may also be a tool that can be used here rather than `type Permissions struct`, but I'm less sure there. - `NotFound()` test cases don't have a way to identify which exit point in the subject-under-test was hit, because we try to hide 404 details from users -- which also makes the tests blind to what they're testing in a lot of cases - The entire structure of the testing system is very non-Go-like. This makes it unfamiliar in a way that is difficult to get started with. - Changing `fixtures: []*fixtureType` into `func(t *testing.T)` which can do `t.Run("...", ...)` for each test case might start to look familiar? And allow me to do something like `for _, s := range string[]{"a", "b", "c"} {...` to code the tests (rare case, for sure) rather than hard-code an array. - I have fewer ideas here than I'd like, so that's the only one so far. 🙂
Author
Member
Copy link

map[string]string for... what I'm going to call as "data hints" ...

This is definitely the most annoying part of the framework. I'll re-read and reflect on your message before answering.

I have one thought that may change your perspective. I see two paths forward:

  • Refactor the test framework to make it less involved and/or difficult to grasp
  • Refactor the permissions middlewares to be less contorted (for lack of a better word) because they grew organically over time

Now that tests are in place and an interface exists, refactoring those middlewares is doable and will allow for regular go tests that should not require a framework of their own at all.

> map[string]string for... what I'm going to call as "data hints" ... This is definitely the most annoying part of the framework. I'll re-read and reflect on your message before answering. I have one thought that may change your perspective. I see two paths forward: - Refactor the test framework to make it less involved and/or difficult to grasp - Refactor the permissions middlewares to be less contorted (for lack of a better word) because they grew organically over time Now that tests are in place and an interface exists, refactoring those middlewares is doable and will allow for regular go tests that should not require a framework of their own at all.
Author
Member
Copy link

I spent an hour trying to find a low hanging fruit to refactor permissions and remove a setter. I found a few (for instance PublicOnly) but when digging further and trying to make something sensible, I kept bumping in other problems (for instance parsing scope that always require error handling: the type being a string is a very weird design choice that forces functions that should be impossible to fail to handle error).

I'll work on the simpler improvements to the test suite instead of keeping exploring this. It is a rather small context where satisfying improvements are possible and I'll need a little of that now 😄

I spent an hour trying to find a low hanging fruit to refactor permissions and remove a setter. I found a few (for instance `PublicOnly`) but when digging further and trying to make something sensible, I kept bumping in other problems (for instance parsing scope that always require error handling: the type being a string is a very weird design choice that forces functions that should be impossible to fail to handle error). I'll work on the simpler improvements to the test suite instead of keeping exploring this. It is a rather small context where satisfying improvements are possible and I'll need a little of that now 😄
Author
Member
Copy link

@mfenniak wrote in #63 (comment):

  • functionTest.fixtures should be called .testCases, and in the existing usage of registerFunctionTest it should be moved to the top of each file to make it clear -- these are the things being tested.

This is an excellent suggestion. Implemented at forgejo/forgejo#13271

@mfenniak wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-18115859: > * `functionTest.fixtures` should be called `.testCases`, and in the existing usage of `registerFunctionTest` it should be moved to the top of each file to make it clear -- these are the things being tested. This is an excellent suggestion. Implemented at https://codeberg.org/forgejo/forgejo/pulls/13271
Author
Member
Copy link

@mfenniak wrote in #63 (comment):

  • Documentation could include a step-by-step "how to add a new middleware and test it"

I realize I am rather blind to what could cause confusion when adding a middleware: I remember too much 😄 Trying to put myself in the shoes of someone bumping into this for the first time, I realized the purpose of the permissions package is not easily discoverable and proposed a terse documentation to help.

I suppose that's not exactly what you had in mind but I hope it helps. If you remember what caused you confusion when exploring this, please give me an indication and I'll do my best to modify the documentation so the next person won't stumble on the same obstacle.

@mfenniak wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-18115859: > * Documentation could include a step-by-step "how to add a new middleware and test it" I realize I am rather blind to what could cause confusion when adding a middleware: I remember too much 😄 Trying to put myself in the shoes of someone bumping into this for the first time, I realized the purpose of the permissions package is not easily discoverable and proposed [a terse documentation](https://codeberg.org/forgejo/forgejo/pulls/13277) to help. I suppose that's not exactly what you had in mind but I hope it helps. If you remember what caused you confusion when exploring this, please give me an indication and I'll do my best to modify the documentation so the next person won't stumble on the same obstacle.
Author
Member
Copy link

@mfenniak wrote in #63 (comment):

  • map[string]string for... what I'm going to call as "data hints" in newFixtureData(x)... lacks any discoverability of available options for the key and value. I think it would benefit from a structured approach, map[TestData]TestDataOption, where TestData and TestDataOption are enums or strings with const values, so that you can find what options exist and are present

I went in another direction in the proposed pull request, assuming the main (only?) reason anyone would get lost with those is because it is unclear how much side effect a key/value pair will have. If I set "org", will it be interpreted by another test case?

In the end there are only a handful of key/value that actually need to be shared because of how the permission functions depend on each other instead of being read-only.

@mfenniak wrote in https://codeberg.org/forgejo/design/issues/63#issuecomment-18115859: > * `map[string]string` for... what I'm going to call as "data hints" in `newFixtureData(x)`... lacks any discoverability of available options for the key and value. I think it would benefit from a structured approach, `map[TestData]TestDataOption`, where `TestData` and `TestDataOption` are enums or strings with const values, so that you can find what options exist and are present I went in another direction in [the proposed pull request](https://codeberg.org/forgejo/forgejo/pulls/13313), assuming the main (only?) reason anyone would get lost with those is because it is unclear how much side effect a key/value pair will have. If I set "org", will it be interpreted by another test case? In the end there are only a handful of key/value that actually need to be shared because of how the permission functions depend on each other instead of being read-only.
Sign in to join this conversation.
No Branch/Tag specified
main
15-new-pr
private-issues
file-editor
No results found.
Labels
Clear labels
User research - Accessibility
Requires input about accessibility features, likely involves user testing.
User research - Blocked
Do not pick as-is! We are happy if you can help, but please coordinate with ongoing redesign in this area.
User research - Community
Community features, such as discovering other people's work or otherwise feeling welcome on a Forgejo instance.
User research - Config (instance)
Instance-wide configuration, authentication and other admin-only needs.
User research - Errors
How to deal with errors in the application and write helpful error messages.
User research - Filters
How filter and search is being worked with.
User research - Future backlog
The issue might be inspiring for future design work.
User research - Git workflow
AGit, fork-based and new Git workflow, PR creation etc
User research - Labels
Active research about Labels
User research - Moderation
Moderation Featuers for Admins are undergoing active User Research
User research - Needs input
Use this label to let the User Research team know their input is requested.
User research - Notifications/Dashboard
Research on how users should know what to do next.
User research - Rendering
Text rendering, markup languages etc
User research - Repo creation
Active research about the New Repo dialog.
User research - Repo units
The repo sections, disabling them and the "Add more" button.
User research - Security
User research - Settings (in-app)
How to structure in-app settings in the future?
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo/design#63
Reference in a new issue
forgejo/design
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?