16
7
Fork
You've already forked design
4

Enforcing permissions for the F3 API endpoint used to update a mirror #61

Open
opened 2026年01月23日 15:56:03 +01:00 by limiting-factor · 25 comments

The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge.

For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with:

curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"https://codeberg.org", "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3

The Forgejo F3 driver based on gof3 performs the following:

  • Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository
  • Create a F3 archive in a directory, as defined by F3
  • Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository

Since it is not using the REST API, each operation of this internal driver must verify that the doer (i.e. the user used to create the F3_LOCAL_TOKEN token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels).

Implementation

Draft

Note

drafts are developed and published in the forgefriends repository

Proposed Design

The /api/v1/f3 endpoint creates or updates the mirror of a remote repository and is given:

  • The URL of the remote forge (e.g. https://codeberg.org)
  • The F3 path to mirror on the remote forge (e.g. /forge/users/myuser/projects/myproject)
  • A token with enough permissions to read this F3 path and all its descendants from the remote forge

The gof3 package implements the high level mirroring logic and relies on:

This driver provides the following functions for each resource:

  • List (e.g. listing all comments of an issue in a way similar to GET /repos/{owner}/{repo}/issues/comments)
  • Get (e.g. read a comment in a way similar to GET /repos/{owner}/{repo}/issues/comments/{id})
  • Put (e.g. create a comment in a way similar to POST /repos/{owner}/{repo}/issues/{index}/comments)
  • Patch (e.g. edit a comment in a way similar to PATCH /repos/{owner}/{repo}/issues/comments/{id})
  • Delete (e.g. delete a comment in a way similar to DELETE /repos/{owner}/{repo}/issues/comments/{id})

Each driver function is responsible to enforce the required permissions, in the same way each Forgejo REST API endpoint is (sequence of middleware or the handler implementation).

Enforcing permissions

The Forgejo REST API permissions are implemented in permission check functions. They are called by REST API endpoints in a specific order and are tested to be effective by the tests associated with each REST API endpoint.

There is a single REST API endpoint for F3 mirroring. It is given a PAT that will be used by the F3 driver functions to perform the same sequence of of permission checks than the equivalent REST API function before performing an operation.

For instance, the Get() driver function to read a repository must perform the same permission checks as the GET /repos/{owner}/{repo} REST API endpoint.

To achieve this re-use:

All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the sequence of permissions checks are harvested for each REST API call when running tests and verified to match what F3 functions do by collecting calls and matching them.

Absolute ID scoping

Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent.

For instance, the Get function on a comment must verify the ID is related by using the issue ID obtained from the parent in the F3 tree.

It must be paired with the REST API endpoint that relies on ReqValidCommentID.

Overview of Forgejo API permissions

The permissions (aka access modes) associated with an action (REST or web) of a given doer are calculated to either be read, write, admin, owner or none.

Within a repository, permissions are automatically denied if the associated Unit is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for Code, Issues, Pull Request, Releases. Other units are not currently relevant to F3: Project, Package, Actions, nor is Wiki. If the doer is a collaborator of a given repository, its permissions are futher modified (and supersedes team permissions) to be read, write or none.

An admin user is automatically granted the highest permissions on the instance. The anonymous user and the Ghost user are not in scope because the F3 API requires that a token is provided. The automatic token created on behalf of the Forgejo Actions user is restricted to act on the repository for which it was generated.

When an absolute ID is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403).

Scopes

The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes:

  • organization
  • issue
  • repository
  • user

And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package).

Repository Units

The API routes have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance mustEnableIssuesOrPull

code.forgejo.org/forgefriends/forgefriends@14075b941c/routers/api/v1/api.go (L426-L428)

forbids using any routes under /api/v1/repos/{owner}/{reponame}/issues unless the issues unit is set on a repository. Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions

code.forgejo.org/forgefriends/forgefriends@14075b941c/routers/api/v1/repo/issue.go (L497-L508)

based on the permissions granted to the doer.

code.forgejo.org/forgefriends/forgefriends@14075b941c/models/perm/access_mode.go (L13-L24)

Absolute ID scoping

Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in /api/v1/repos/{owner}/{reponame}/issues/{index}). Others contain identifiers that are absolute (e.g. the id of a comment in /api/v1/repos/{owner}/{reponame}/issues/comments/{id}). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository).

For instance reqValidCommentID

code.forgejo.org/forgefriends/forgefriends@14075b941c/routers/api/v1/api.go (L195-L203)

forbids using a comment ID if it does not belong to the related repository.

Doer

The doer of an REST API endpoint can either be:

  • A registered user
  • A system user (Ghost or Forgejo Actions)
  • Anonymous (user is nil)

The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under /api/v1/repos/{owner}/{repo}.

  • It is denied access (no read, no write) to repositories that are not the same as the repository where the actions is running
  • It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository
  • Otherwise it has write permissions on all units in the repository
The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge. For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with: ```sh curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"https://codeberg.org", "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3 ``` The [Forgejo F3 driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver) based on [gof3](https://code.forgejo.org/f3/gof3) performs the following: - Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository - Create a F3 archive in a directory, as defined by [F3](https://f3.forgefriends.org/) - Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository Since it is not using the REST API, each operation of this internal driver must verify that the doer (i.e. the user used to create the `F3_LOCAL_TOKEN` token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels). # Implementation ## Draft > **Note** drafts are developed and published in the forgefriends repository - [`/api/v1/f3` REST API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/f3/mirror.go#L20) # Proposed Design The `/api/v1/f3` endpoint creates or updates the mirror of a remote repository and is given: - The URL of the remote forge (e.g. https://codeberg.org) - The F3 path to mirror on the remote forge (e.g. `/forge/users/myuser/projects/myproject`) - A token with enough permissions to read this F3 path and all its descendants from the remote forge The [gof3 package](https://code.forgejo.org/f3/gof3) implements the high level mirroring logic and relies on: - the drivers [provided by the gof3 package](https://code.forgejo.org/f3/gof3/src/commit/d70169481604dccd5ecff9d91f5a74fe3fdd76ad/forges) to read the remote forge. The permissions are enforced by the remote forge and is not in scope for this discussion. - the [F3 internal driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver) to write to the local Forgejo instance. This driver provides the following functions for each resource: - List (e.g. listing all comments of an issue in a way similar to `GET /repos/{owner}/{repo}/issues/comments`) - Get (e.g. read a comment in a way similar to `GET /repos/{owner}/{repo}/issues/comments/{id}`) - Put (e.g. create a comment in a way similar to `POST /repos/{owner}/{repo}/issues/{index}/comments`) - Patch (e.g. edit a comment in a way similar to `PATCH /repos/{owner}/{repo}/issues/comments/{id}`) - Delete (e.g. delete a comment in a way similar to `DELETE /repos/{owner}/{repo}/issues/comments/{id}`) Each driver function is responsible to enforce the required permissions, in the same way each Forgejo REST API endpoint is (sequence of middleware or the handler implementation). ## Enforcing permissions The Forgejo REST API permissions are implemented in [permission check functions](https://code.forgejo.org/forgejo/forgejo/src/branch/forgejo/routers/api/v1/permissions). They are called by REST API endpoints in a specific order and are tested to be effective by the tests associated with each REST API endpoint. There is [a single REST API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/f3/mirror.go#L20) for F3 mirroring. It is given a PAT that will be used by [the F3 driver functions](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver) to perform the same sequence of of permission checks than the equivalent REST API function before performing an operation. For instance, the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver/project.go#L131-L149) must perform the same permission checks as the [`GET /repos/{owner}/{repo}` REST API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/api.go#L792). To achieve this re-use: - the F3 function calls the REST API permission checks in the same order they are specified in the routes of the corresponding REST API endpoint. See how the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver/project.go#L146) calls [`TokenRequiresScopes`,`RepoAccess`,`CheckTokenPublicOnly` and `ReqAnyRepoReader`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/permissions/project/project.go#L16-L24) in the same order the [REST API route to `GET /repos/{owner}/{repo}`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/api.go#L792) calls them as specified by the route that leads to the endpoint. All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the [sequence of permissions checks are harvested for each REST API call when running tests](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/permissions/testhelpers/testing.go) and verified to match what F3 functions do by [collecting calls](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/permissions/api/proxy.go#L40-L48) and [matching them](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/permissions/check.go#L32). ## Absolute ID scoping Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent. For instance, the [Get function](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/services/f3/driver/comment.go#L96) on a comment must verify the ID is related by using the issue ID obtained from the parent in the F3 tree. It must be paired with the REST API endpoint that relies on [`ReqValidCommentID`](https://code.forgejo.org/forgejo/forgejo/src/commit/e7c45cd9c87bbc776e3280d33223225cd79c848c/routers/api/v1/permissions/req_valid_comment_id.go). ## Overview of Forgejo API permissions The permissions (aka access modes) associated with an action (REST or web) of a given **doer** are calculated to either be [**read**, **write**, **admin**, **owner** or **none**](https://codeberg.org/forgejo/forgejo/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/models/perm/access_mode.go#L13-L24). Within a repository, permissions are automatically denied if the associated **Unit** is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for **Code**, **Issues**, **Pull Request**, **Releases**. Other units are not currently relevant to F3: **Project**, **Package**, **Actions**, nor is **Wiki**. If the doer is a **collaborator** of a given repository, its permissions are futher modified (and supersedes team permissions) to be **read**, **write** or **none**. An **admin user** is automatically granted the highest permissions on the instance. The **anonymous** user and the **Ghost** user are not in scope because the F3 API requires that a token is provided. The automatic token created on behalf of the **Forgejo Actions** user is restricted to act on the repository for which it was generated. When an **absolute ID** is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403). ### Scopes The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes: - organization - issue - repository - user And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package). ### Repository Units The [API routes](https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/api.go#L479) have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance `mustEnableIssuesOrPull` https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/api.go#L426-L428 forbids using any routes under `/api/v1/repos/{owner}/{reponame}/issues` unless the issues unit is set on a repository. Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/repo/issue.go#L497-L508 based on the permissions granted to the doer. https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/models/perm/access_mode.go#L13-L24 ### Absolute ID scoping Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in `/api/v1/repos/{owner}/{reponame}/issues/{index}`). Others contain identifiers that are absolute (e.g. the id of a comment in `/api/v1/repos/{owner}/{reponame}/issues/comments/{id}`). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository). For instance `reqValidCommentID` https://code.forgejo.org/forgefriends/forgefriends/src/commit/14075b941c79ed08cd49fd35805ebcc9bf9d6923/routers/api/v1/api.go#L195-L203 forbids using a comment ID if it does not belong to the related repository. ### Doer The doer of an REST API endpoint can either be: - A registered user - A system user (Ghost or Forgejo Actions) - Anonymous (user is nil) The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under `/api/v1/repos/{owner}/{repo}`. - It is denied access (no read, no write) to repositories that are not the same as the repository where the actions is running - It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository - Otherwise it has write permissions on all units in the repository
limiting-factor changed title from (削除) WIP: Securely mirror a Forgejo repository with an F3 API endpoint (削除ここまで) to Enforcing permissions for the F3 API endpoint used to update a mirror 2026年01月28日 12:33:17 +01:00
Author
Member
Copy link

This design is ready for review. It is partially implemented in the draft repository and I will wait for input before proceeding further, in case the architecture needs to be reworked entirely.

This design is ready for review. It is partially implemented in the draft repository and I will wait for input before proceeding further, in case the architecture needs to be reworked entirely.
Author
Member
Copy link

@gusted it would be great if you can spare five minutes to read this design.If it does not trigger any remark, it is fine. If it gives you a bad feeling about anything, a terse comment will be very welcome, even if it comes with no explanation.

@gusted it would be great if you can spare five minutes to read this design.If it does not trigger any remark, it is fine. If it gives you a bad feeling about anything, a terse comment will be very welcome, even if it comes with no explanation.
Author
Member
Copy link

For the record @gusted replied via Matrix and expressed interest but is not available for a review before some time.

For the record @gusted replied via Matrix and expressed interest but is not available for a review before some time.
Author
Member
Copy link

I will work some more on this in the near future.

I will work some more on this in the near future.
Author
Member
Copy link

@mfenniak I noticed today your work on repository and tokens and would be grateful if you could take a quick look at this draft design. At this stage it is not worth spending more than a few minutes on it. But if anything strikes you as obviously backward within this very short timeframe, I'd be glad to hear about it.

A quick background summary on F3: it is still in its early stages. In the past weeks it reached a point where I'm able to work on this issue by using a Forgejo instance that runs on my laptop and it mirrors the changes to Codeberg (this is a modified forgejo). It is on that occasion that I ran into this permission bug because it blocks mirroring the images attached to a comment. The commits of this modified Forgejo need a sound security model before they can be proposed as pull requests to Forgejo itself. It is my main motivation to get this design right.

@mfenniak I noticed today your work [on repository and tokens](/forge/organizations/6/projects/20/pull_requests/11) and would be grateful if you could take a quick look at this draft design. At this stage it is not worth spending more than a few minutes on it. But if anything strikes you as obviously backward within this very short timeframe, I'd be glad to hear about it. A quick background summary on F3: it is still in its early stages. In the past weeks it reached a point where I'm able to work on this issue by using a Forgejo instance that runs on my laptop and it mirrors the changes to Codeberg (this is a [modified forgejo](https://code.forgejo.org/forgefriends/forgefriends/commits/branch/main)). It is on that occasion that [I ran into this permission bug](/forge/organizations/6/projects/20/pull_requests/12) because it blocks mirroring the images attached to a comment. The commits of this modified Forgejo need a sound security model before they can be proposed as pull requests to Forgejo itself. It is my main motivation to get this design right.

Hey @limiting-factor, thanks for pinging me.

I think I understand the problem space at least -- there are already two modes that Forgejo does permission/access checks in (web, API), and it's probably fair to say that they share tools, but not implementations. In other words, it's quite possible for one of the two modes to have a differing implementation for any access check; a check can be missing or vary in logic easily, even though they both use common APIs like GetUserRepoPermission. If I understand the problem you're describing, the F3 system would be a bulk operation to mirror data from "over there" to "in here", and as that operates in bulk on a lot of objects "in here", it needs permission checks as well which could introduce a third mode of operating. And that has a risk that it could lack, or vary, permission checks as well.

Is that more-or-less right?

If I'm understanding, I don't feel like I have a lot of useful insight to provide that isn't obvious. So, just conceptual thoughts, and not specific implementation thoughts. 🙂

(a) It seems crucial that access checks be centralized in their logic, and duplicated less, not more.

(b) It feels like we need some form of abstraction or declarative model that allows us to broadly implement access control checks automatically, rather than imperatively on every object access. At some kind of boundary in the application (for example, when taking an API response and returning it to the user), the system could recognize that api.RepositoryMeta is a repository, and therefore the user needs to be able to view it, and therefore an access control check is performed automatically. But defining the boundaries and the checks that would allow this to happen seems really unclear to me.

I think when I look at things like code.forgejo.org/forgefriends/forgefriends@b17a3e1f6a/services/f3/permissions/comment/comment.go (L13-L25) ... I worry that it's in the direction of a third possibly access check that may deviate from two other implementations. But the problem is that I don't have a practical suggestion for how to do it better, where "practical" means "can be done today".

Hey @limiting-factor, thanks for pinging me. I think I understand the problem space at least -- there are already two modes that Forgejo does permission/access checks in (web, API), and it's probably fair to say that they share tools, but not implementations. In other words, it's quite possible for one of the two modes to have a differing implementation for any access check; a check can be missing or vary in logic easily, even though they both use common APIs like `GetUserRepoPermission`. If I understand the problem you're describing, the F3 system would be a bulk operation to mirror data from "over there" to "in here", and as that operates in bulk on a lot of objects "in here", it needs permission checks as well which could introduce a third mode of operating. And that has a risk that it could lack, or vary, permission checks as well. Is that more-or-less right? If I'm understanding, I don't feel like I have a lot of useful insight to provide that isn't obvious. So, just conceptual thoughts, and not specific implementation thoughts. 🙂 (a) It seems crucial that access checks be centralized in their logic, and duplicated less, not more. (b) It feels like we need some form of abstraction or declarative model that allows us to *broadly* implement access control checks automatically, rather than imperatively on every object access. At some kind of boundary in the application (for example, when taking an API response and returning it to the user), the system could recognize that `api.RepositoryMeta` is a repository, and therefore the user needs to be able to view it, and therefore an access control check is performed automatically. But defining the boundaries and the checks that would allow this to happen seems really unclear to me. I think when I look at things like https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/comment/comment.go#L13-L25 ... I worry that it's in the direction of a third possibly access check that may deviate from two other implementations. But the problem is that I don't have a practical suggestion for how to do it better, where "practical" means "can be done today".
Author
Member
Copy link

@mfenniak wrote:

Is that more-or-less right?

Exactly right.

(a) It seems crucial that access checks be centralized in their logic, and duplicated less, not more.

Yes. Crucial is the keyword. Each permission check has the potential of introducing a new security bug.

I worry that it's in the direction of a third possibly access check that may deviate from two other implementations.

It is.

But the problem is that I don't have a practical suggestion for how to do it better, where "practical" means "can be done today".

The more I work on this the more I'm convinced that the solution is close to what you suggested in (b). It is a lot of refactor work, with no immediate value to Forgejo (and the risk of introducing a security bug in the process). Once the web and REST API use a unified access control instead of being two different implementations, F3 could use it as well, without duplicating the logic.

I don't see how to solve that problem. I could spend all the time needed to perform this refactor (it will take months, there are so many endpoints, but it is worth it). However, even if I do that perfectly well and the pull requests I propose are flawless, there will be dozens of them, possibly hundreds and that will add an impossible workload to other Forgejo contributors. Realistically this will make progress so slowly that it will stall.

@mfenniak wrote: > Is that more-or-less right? Exactly right. > (a) It seems crucial that access checks be centralized in their logic, and duplicated less, not more. Yes. Crucial is the keyword. Each permission check has the potential of introducing a new security bug. > I worry that it's in the direction of a third possibly access check that may deviate from two other implementations. It is. > But the problem is that I don't have a practical suggestion for how to do it better, where "practical" means "can be done today". The more I work on this the more I'm convinced that the solution is close to what you suggested in (b). It is a lot of refactor work, with no immediate value to Forgejo (and the risk of introducing a security bug in the process). Once the web and REST API use a unified access control instead of being two different implementations, F3 could use it as well, without duplicating the logic. I don't see how to solve that problem. I could spend all the time needed to perform this refactor (it will take months, there are so many endpoints, but it is worth it). However, even if I do that perfectly well and the pull requests I propose are flawless, there will be dozens of them, possibly hundreds and that will add an impossible workload to other Forgejo contributors. Realistically this will make progress so slowly that it will stall.
Author
Member
Copy link

Or...

I could try to implement an access control system that is not specific to F3, although it would only be used by F3 at first. And then it could gradually be used to remove the logic duplication from the REST and web API. Every time an access control security bug needs to be fixed, it could use that common control system instead of fixing the ad-hoc access control in place for the REST or web API. And each time an API endpoint is modified or added, it would be a new opportunity to refactor the ad-hoc access control and use the common access control. It may take a very long time but I can see that making progress and coming to an end, eventually.

But defining the boundaries and the checks that would allow this to happen seems really unclear to me.

This is also unclear to me. However, even if that is not implemented, ensuring the access control logic is in one place instead of being duplicated is one step in the right direction. If, at a later time, someone figures out how to define those boundaries, they would have an easier time than with two (or three) implementations.

Or... I could try to implement an access control system that is not specific to F3, although it would only be used by F3 at first. And then it could gradually be used to remove the logic duplication from the REST and web API. Every time an access control security bug needs to be fixed, it could use that common control system instead of fixing the ad-hoc access control in place for the REST or web API. And each time an API endpoint is modified or added, it would be a new opportunity to refactor the ad-hoc access control and use the common access control. It may take a very long time but I can see that making progress and coming to an end, eventually. > But defining the boundaries and the checks that would allow this to happen seems really unclear to me. This is also unclear to me. However, even if that is not implemented, ensuring the access control logic is in one place instead of being duplicated is one step in the right direction. If, at a later time, someone figures out how to define those boundaries, they would have an easier time than with two (or three) implementations.
Author
Member
Copy link

I refactored the draft forgefriends permissions implementation to illustrate the proposal.

I refactored the draft forgefriends permissions implementation to illustrate the proposal. - A new Forgejo service implements the permission model https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/permissions - The storage of this permission service is supported by the implementation of an interface https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/permissions/context/interface.go#L15-L32 - The REST API context https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/context/api.go#L181-L223 - The F3 context https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/permissions/context/context.go#L48-L90 - The web context (to be done) - The permission service provides functions that implement the permission logic and that can be shared between web, REST and F3, for instance: - [routers/api/shared/middleware.go:apiAuthorization](https://code.forgejo.org/forgefriends/forgefriends/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7#diff-c4a1ea0e50db8dc054856a1b425f9448c7c4f1c4) can be replaced by the function doing the same in the permissions service [services/permissions/context/functions.go:SetAuthorization](https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/permissions/context/functions.go#L14-L35) which is a trivial refactor using the accessors expected by the interface instead of data members of the concrete struct

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

I could try to implement an access control system that is not specific to F3, although it would only be used by F3 at first. And then it could gradually be used to remove the logic duplication from the REST and web API. Every time an access control security bug needs to be fixed, it could use that common control system instead of fixing the ad-hoc access control in place for the REST or web API. And each time an API endpoint is modified or added, it would be a new opportunity to refactor the ad-hoc access control and use the common access control. It may take a very long time but I can see that making progress and coming to an end, eventually.

That's quite a compelling approach.

My historical experience casts skepticism over the idea. I've been a part of many development projects where "we'll introduce a new thing that solves all the problems, and then migrate piece by piece to it". I'm not sure I've ever been part of a project where the migration is actually completed. We inevitably end up with one more thing, forever. However, we don't need to let my professional failures destroy the idea 🤣... it's just fair to have a healthy skepticism, leading to figuring out how to manage risks, including the idea that it might not get completed.

It would be a reasonable statement to say that we're better off with a third set of permission checks, if it enables us to reduce to one set of permission checks over time. But, the new access control system would need to be complete enough to capture the subtle behaviours of the web backend and API backends, or else it would be destined to be the third thing forever.

While you've picked a case to implement in your draft proposal, there might be some other more complex cases that need to be proven out through this draft to sanity checking that the access control system can represent the real-world, complex, or subtle cases. (I don't have specifics in mind unfortunately.)

From just a technical coding perspective, the mutability of the PermissionsContext in your new permission model raises a red flag for me. I'm not sure if I'm understanding how it's expected to work correctly.

  • code.forgejo.org/forgefriends/forgefriends@adb9bd6556/services/permissions/comment/comment.go (L20-L22)
  • Calling Get(repoID, ...)(ctx) will change state on the ctx, mutating state on the ctx, impacting future permission checks, and other accesses (outside of the PermissionsContext) to the data stored on ctx.
  • I don't really expect unpredictable side-effects from a Get method.
  • It's quite plausible to me that a comment permission check occurs on multiple objects in one page load -- for example, viewing cross-referenced comments on an issue will require permission checks against the repo that those comments originated from -- and each of those will mutate the ctx state to different repos.
@limiting-factor wrote in https://codeberg.org/forgejo/design/issues/61#issuecomment-11627383: > I could try to implement an access control system that is not specific to F3, although it would only be used by F3 at first. And then it could gradually be used to remove the logic duplication from the REST and web API. Every time an access control security bug needs to be fixed, it could use that common control system instead of fixing the ad-hoc access control in place for the REST or web API. And each time an API endpoint is modified or added, it would be a new opportunity to refactor the ad-hoc access control and use the common access control. It may take a very long time but I can see that making progress and coming to an end, eventually. That's quite a compelling approach. My historical experience casts skepticism over the idea. I've been a part of many development projects where "we'll introduce a new thing that solves all the problems, and then migrate piece by piece to it". I'm not sure I've ever been part of a project where the migration is actually completed. We inevitably end up with one more thing, forever. However, we don't need to let my professional failures destroy the idea 🤣... it's just fair to have a healthy skepticism, leading to figuring out how to manage risks, including the idea that it might not get completed. It would be a reasonable statement to say that we're better off with a third set of permission checks, if it *enables* us to reduce to one set of permission checks over time. But, the new access control system would need to be complete enough to capture the subtle behaviours of the web backend and API backends, or else it would be destined to be the third thing forever. While you've picked a case to implement in your draft proposal, there might be some other more complex cases that need to be proven out through this draft to sanity checking that the access control system can represent the real-world, complex, or subtle cases. (I don't have specifics in mind unfortunately.) From just a technical coding perspective, the mutability of the `PermissionsContext` in your new permission model raises a red flag for me. I'm not sure if I'm understanding how it's expected to work correctly. - https://code.forgejo.org/forgefriends/forgefriends/src/commit/adb9bd65567d81b7b5c23516fabab4610043d7a7/services/permissions/comment/comment.go#L20-L22 - Calling `Get(repoID, ...)(ctx)` will change state on the `ctx`, mutating state on the `ctx`, impacting future permission checks, and other accesses (outside of the `PermissionsContext`) to the data stored on `ctx`. - I don't really expect unpredictable side-effects from a `Get` method. - It's quite plausible to me that a comment permission check occurs on multiple objects in one page load -- for example, viewing cross-referenced comments on an issue will require permission checks against the repo that those comments originated from -- and each of those will mutate the `ctx` state to different repos.
Author
Member
Copy link

@mfenniak wrote:

That's quite a compelling approach.

I'm glad you find this an improvement over the previous (F3 only) approach.

My historical experience casts skepticism over the idea...

I'm grateful for the kind way to put it. The previous approach was guaranteed to create a code duplication problem. This approach could work but I agree that it highly depends on its adoption rate for the REST and web API. For it to succeed it needs to get over a threshold and it is up to me to get there.

... there might be some other more complex cases that need to be proven out through this draft to sanity checking that the access control system can represent the real-world, complex, or subtle cases.

Yes, and I chose comments because it is one of those non-trivial case (absolute ID check), but there are others, possibly more complicated.

There is much less variety in how mirroring is implemented in F3 compared to the REST /web API endpoints. Each resource (issue, reaction, etc.) has a List/Get/Put/Patch/Delete function. It follows that the more irregular REST /web API behaviors and access control checks are not going to be supported if I focus primarily on F3. I don't have a specific example in mind but I'm quite sure I'll discover some. For instance there is a single F3 set of functions for handling all kinds of attachments but there are a set of REST API endpoints for each kind of resource those attachments belong to (issue, comment, review, review comment, ...).

Calling Get(repoID, ...)(ctx) will change state on the ctx... I don't really expect unpredictable side-effects from a Get method.

Good point. It should be named CheckGet as it is equivalent to the middleware used in the API routes, which buffer data in the context to avoid repeating the same operation many times. But I did not think it could be used in the context of the REST / web API routes and the current implementation / architecture is not a good fit.

Thanks a lot for showing me that the direction I was heading to was not good 🙏 I'm still unsure if creating a generic permission system is the solution but it is worth exploring further. I'll work on it and, if you don't mind, I'll ping you again when there is something concrete to look at.

@mfenniak wrote: > That's quite a compelling approach. I'm glad you find this an improvement over the previous (F3 only) approach. > My historical experience casts skepticism over the idea... I'm grateful for the kind way to put it. The previous approach was guaranteed to create a code duplication problem. This approach could work but I agree that it highly depends on its adoption rate for the REST and web API. For it to succeed it needs to get over a threshold and it is up to me to get there. > ... there might be some other more complex cases that need to be proven out through this draft to sanity checking that the access control system can represent the real-world, complex, or subtle cases. Yes, and I chose comments because it is one of those non-trivial case (absolute ID check), but there are others, possibly more complicated. There is much less variety in how mirroring is implemented in F3 compared to the REST /web API endpoints. Each resource (issue, reaction, etc.) has a List/Get/Put/Patch/Delete function. It follows that the more irregular REST /web API behaviors and access control checks are not going to be supported if I focus primarily on F3. I don't have a specific example in mind but I'm quite sure I'll discover some. For instance there is a single F3 set of functions for handling all kinds of attachments but there are a set of REST API endpoints for each kind of resource those attachments belong to (issue, comment, review, review comment, ...). > Calling Get(repoID, ...)(ctx) will change state on the ctx... I don't really expect unpredictable side-effects from a Get method. Good point. It should be named `CheckGet` as it is equivalent to the middleware used in the API routes, which buffer data in the context to avoid repeating the same operation many times. But I did not think it could be used in the context of the REST / web API routes and the current implementation / architecture is not a good fit. Thanks a lot for showing me that the direction I was heading to was not good 🙏 I'm still unsure if creating a generic permission system is the solution but it is worth exploring further. I'll work on it and, if you don't mind, I'll ping you again when there is something concrete to look at.
Author
Member
Copy link

For archive, the description of this design as it is today.


The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge.

For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with:

curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"/forge/organizations/6/projects/115/issues/42/comments/1978 "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3

The Forgejo F3 driver based on gof3 performs the following:

  • Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository
  • Create a F3 archive in a directory, as defined by F3
  • Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository

Since it is not using the API, each operation of this internal driver must verify that the doer (i.e. the user used to create the F3_LOCAL_TOKEN token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels).

Implementation

Draft

Note

drafts are developed and published in the forgefriends repository

Proposed Design

The /api/v1/f3 endpoint to create or update the mirror of a remote repository is given:

  • The URL of the remote forge (e.g. /forge/organizations/6/projects/115/issues/42/comments/1978
  • The F3 path to mirror on the remote forge (e.g. /forge/users/myuser/projects/myproject)
  • A token with enough permissions to read the repository from the remote forge

The gof3 package implements the high level mirroring logic and relies on:

This driver provides the following functions for each resource:

  • List (e.g. listing all comments of an issue in a way similar to GET /repos/{owner}/{repo}/issues/comments)
  • Get (e.g. read a comment in a way similar to GET /repos/{owner}/{repo}/issues/comments/{id})
  • Put (e.g. create a comment in a way similar to POST /repos/{owner}/{repo}/issues/{index}/comments)
  • Patch (e.g. edit a comment in a way similar to PATCH /repos/{owner}/{repo}/issues/comments/{id})
  • Delete (e.g. delete a comment in a way similar to DELETE /repos/{owner}/{repo}/issues/comments/{id})

Each driver function call is responsible to enforce the required permissions.

Enforcing permissions

The Forgejo API and web permissions are not always implemented in functions or methods common to both that F3 could re-use to ensure consistency. In addition, there is a single API endpoint for F3 mirroring which means that verifications inserted when building the API routes cannot be re-used because they private functions (e.g. commentAssignment).

To keep the permission code and logic duplication to a minimum, it is implemented as follows:

An example of permission logic duplication that cannot be avoided (without a refactoring the current Forgejo permissions architecture) is the content of the function checking the doer can read a comment.

An example of permission logic reuse is the function that calculates the permissions of the doer for a given repository. A helper of the F3 context can be used by F3 driver functions to figure out of the operation is permitted.

Absolute ID scoping

Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent.

For instance, the Get function on a comment verifies the ID is related by using the issue ID obtained from the parent in the F3 tree.

It is equivalent to what commentAssignment does for comment related API endpoints.

Overview of Forgejo API permissions

The permissions (aka access modes) associated to an action (API or web) of a given doer are calculated to either be read, write, admin, owner or none.

Within a repository, permissions are automatically denied if the associated Unit is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for Code, Issues, Pull Request, Releases. Other units are not currently relevant to F3: Project, Package, Actions, nor is Wiki. If the doer is a collaborator of a given repository, its permissions are futher modified (and supersedes team permissions) to be read, write or none.

An admin user is automatically granted the highest permissions on the instance. The anonymous user and the Ghost user are not in scope because the F3 API requires that a token is provided. The token created on behalf of the Forgejo Actions user is restricted to act on the repository for which it was generated.

When an absolute ID is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403).

Scopes

The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes:

  • organization
  • issue
  • repository
  • user

And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package).

Repository Units

The API routes have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance mustEnableIssuesOrPull

funcmustEnableIssuesOrPulls(ctx*context.APIContext){
if!ctx.Repo.CanRead(unit.TypeIssues)&&
(!ctx.Repo.Repository.CanEnablePulls()||!ctx.Repo.CanRead(unit.TypePullRequests)){
ifctx.Repo.Repository.CanEnablePulls()&&log.IsTrace(){
ifctx.IsSigned{
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
unit.TypeIssues,
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
}else{
log.Trace("Permission Denied: Anonymous user cannot read %-v and %-v in Repo %-v\n"+
"Anonymous user in Repo has Permissions: %-+v",
unit.TypeIssues,
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
}
}
ctx.NotFound()
return
}
}

forbids using any routes under /api/v1/repos/{owner}/{reponame}/issues unless the issues unit is set on a repository

Preview has been truncated
m.Group("/issues",func(){
m.Combo("").Get(repo.ListIssues).
Post(reqToken(),mustNotBeArchived,bind(api.CreateIssueOption{}),reqRepoReader(unit.TypeIssues),repo.CreateIssue)
m.Get("/pinned",reqRepoReader(unit.TypeIssues),repo.ListPinnedIssues)
m.Group("/comments",func(){
m.Get("",repo.ListRepoIssueComments)
m.Group("/{id}",func(){
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived,reqToken(),bind(api.EditIssueCommentOption{}),repo.EditIssueComment).
Delete(reqToken(),repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(),bind(api.EditReactionOption{}),repo.PostIssueCommentReaction).
Delete(reqToken(),bind(api.EditReactionOption{}),repo.DeleteIssueCommentReaction)
m.Group("/assets",func(){
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(),mustNotBeArchived,context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues,context.QuotaTargetRepo),repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(),mustNotBeArchived,bind(api.EditAttachmentOptions{}),repo.EditIssueCommentAttachment).
Delete(reqToken(),mustNotBeArchived,repo.DeleteIssueCommentAttachment)
},mustEnableAttachments)
},commentAssignment(":id"))
})
m.Group("/{index}",func(){
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(),bind(api.EditIssueOption{}),repo.EditIssue).
Delete(reqToken(),reqAdmin(),context.ReferencesGitRepo(),repo.DeleteIssue)
m.Group("/comments",func(){
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(),mustNotBeArchived,bind(api.CreateIssueCommentOption{}),repo.CreateIssueComment)
m.Combo("/{id}",reqToken(),commentAssignment(":id")).Patch(bind(api.EditIssueCommentOption{}),repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
m.Get("/timeline",repo.ListIssueCommentsAndTimeline)
m.Group("/labels",func(){
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(),bind(api.IssueLabelsOption{}),repo.AddIssueLabels).
Put(reqToken(),bind(api.IssueLabelsOption{}),repo.ReplaceIssueLabels).
Delete(reqToken(),bind(api.DeleteLabelsOption{}),repo.ClearIssueLabels)
m.Delete("/{identifier}",reqToken(),bind(api.DeleteLabelsOption{}),repo.DeleteIssueLabel)
})
m.Group("/times",func(){
m.Combo("").
Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}),repo.AddTime).
Delete(repo.ResetIssueTime)
m.Delete("/{id}",repo.DeleteTime)

Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions

if!isPull.Has(){
canReadIssues:=ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls:=ctx.Repo.CanRead(unit.TypePullRequests)
if!canReadIssues&&!canReadPulls{
ctx.NotFound()
return
}elseif!canReadIssues{
isPull=optional.Some(true)
}elseif!canReadPulls{
isPull=optional.Some(false)
}
}

based on the permissions granted to the doer.

const(
// AccessModeNone no access
AccessModeNoneAccessMode=iota// 0
// AccessModeRead read access
AccessModeRead// 1
// AccessModeWrite write access
AccessModeWrite// 2
// AccessModeAdmin admin access
AccessModeAdmin// 3
// AccessModeOwner owner access
AccessModeOwner// 4
)

Absolute ID scoping

Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in /api/v1/repos/{owner}/{reponame}/issues/{index}). Others contain identifiers that are absolute (e.g. the id of a comment in /api/v1/repos/{owner}/{reponame}/issues/comments/{id}). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository).

For instance commentAssignment

funccommentAssignment(idParamstring)func(ctx*context.APIContext){
returnfunc(ctx*context.APIContext){
comment,err:=issues_model.GetCommentByID(ctx,ctx.ParamsInt64(idParam))
iferr!=nil{
ifissues_model.IsErrCommentNotExist(err){
ctx.NotFound(err)
}else{
ctx.InternalServerError(err)
}
return
}
iferr=comment.LoadIssue(ctx);err!=nil{
ctx.InternalServerError(err)
return
}
ifcomment.Issue==nil||comment.Issue.RepoID!=ctx.Repo.Repository.ID{
ctx.NotFound()
return
}
if!ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull){
ctx.NotFound()
return
}
comment.Issue.Repo=ctx.Repo.Repository
ctx.Comment=comment
}
}

forbids using a comment ID if it does not belong to the related repository

m.Group("/{id}",func(){
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived,reqToken(),bind(api.EditIssueCommentOption{}),repo.EditIssueComment).
Delete(reqToken(),repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(),bind(api.EditReactionOption{}),repo.PostIssueCommentReaction).
Delete(reqToken(),bind(api.EditReactionOption{}),repo.DeleteIssueCommentReaction)
m.Group("/assets",func(){
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(),mustNotBeArchived,context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues,context.QuotaTargetRepo),repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(),mustNotBeArchived,bind(api.EditAttachmentOptions{}),repo.EditIssueCommentAttachment).
Delete(reqToken(),mustNotBeArchived,repo.DeleteIssueCommentAttachment)
},mustEnableAttachments)
},commentAssignment(":id"))

Doer

The doer of an action can either be:

  • A registered user
  • A system user (Ghost or Forgejo Actions)
  • Anonymous (user is nil)

The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under /api/v1/repos/{owner}/{repo}.

  • It is denied access (no read, no write) to repositories that are no the same as the repository where the actions is running
  • It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository
  • Otherwise it has write permissions on all units in the repository
ifctx.Doer!=nil&&ctx.Doer.ID==user_model.ActionsUserID{
taskID:=ctx.Data["ActionsTaskID"].(int64)
task,err:=actions_model.GetTaskByID(ctx,taskID)
iferr!=nil{
ctx.Error(http.StatusInternalServerError,"actions_model.GetTaskByID",err)
return
}
iftask.RepoID!=repo.ID{
ctx.NotFound()
return
}
iftask.IsForkPullRequest{
ctx.Repo.AccessMode=perm.AccessModeRead
}else{
ctx.Repo.AccessMode=perm.AccessModeWrite
}
iferr:=ctx.Repo.Repository.LoadUnits(ctx);err!=nil{
ctx.Error(http.StatusInternalServerError,"LoadUnits",err)
return
}
ctx.Repo.Units=ctx.Repo.Repository.Units
ctx.Repo.UnitsMode=make(map[unit.Type]perm.AccessMode)
for_,u:=rangectx.Repo.Repository.Units{
ctx.Repo.UnitsMode[u.Type]=ctx.Repo.AccessMode
}
}else{
ctx.Repo.Permission,err=access_model.GetUserRepoPermission(ctx,repo,ctx.Doer)
iferr!=nil{
ctx.Error(http.StatusInternalServerError,"GetUserRepoPermission",err)
return
}
}
For archive, the description of this design as it is today. --- The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge. For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with: ```sh curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"/forge/organizations/6/projects/115/issues/42/comments/1978 "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3 ``` The [Forgejo F3 driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/driver) based on [gof3](https://code.forgejo.org/f3/gof3) performs the following: - Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository - Create a F3 archive in a directory, as defined by [F3](https://f3.forgefriends.org/) - Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository Since it is not using the API, each operation of this internal driver must verify that the doer (i.e. the user used to create the `F3_LOCAL_TOKEN` token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels). # Implementation ## Draft > **Note** drafts are developed and published in the forgefriends repository - [API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/routers/api/v1/f3/mirror.go#L13) # Proposed Design The `/api/v1/f3` endpoint to create or update the mirror of a remote repository is given: - The URL of the remote forge (e.g. /forge/organizations/6/projects/115/issues/42/comments/1978 - The F3 path to mirror on the remote forge (e.g. `/forge/users/myuser/projects/myproject`) - A token with enough permissions to read the repository from the remote forge The [gof3 package](https://code.forgejo.org/f3/gof3) implements the high level mirroring logic and relies on: - the drivers [provided by the gof3 package](https://code.forgejo.org/f3/gof3/src/commit/d70169481604dccd5ecff9d91f5a74fe3fdd76ad/forges) to read the remote forge. The permissions are enforced by the remote forge and is not in scope for this discussion. - the [F3 internal driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/driver) to write to the local Forgejo instance. This driver provides the following functions for each resource: - List (e.g. listing all comments of an issue in a way similar to `GET /repos/{owner}/{repo}/issues/comments`) - Get (e.g. read a comment in a way similar to `GET /repos/{owner}/{repo}/issues/comments/{id}`) - Put (e.g. create a comment in a way similar to `POST /repos/{owner}/{repo}/issues/{index}/comments`) - Patch (e.g. edit a comment in a way similar to `PATCH /repos/{owner}/{repo}/issues/comments/{id}`) - Delete (e.g. delete a comment in a way similar to `DELETE /repos/{owner}/{repo}/issues/comments/{id}`) Each driver function call is responsible to enforce the required permissions. ## Enforcing permissions The Forgejo API and web permissions are not always implemented in functions or methods common to both that F3 could re-use to ensure consistency. In addition, there is [a single API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/routers/api/v1/f3/mirror.go#L13) for F3 mirroring which means that verifications inserted when building the API routes cannot be re-used because they private functions (e.g. [`commentAssignment`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/routers/api/v1/api.go#L238-L269)). To keep the permission code and logic duplication to a minimum, it is implemented as follows: - The [F3 context](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/context/context.go#L18-L22) is created in the the function that implements the `/api/v1/f3` API endpoint and [is inserted in the context](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/mirror/remote_to_local.go#L42-L44). It serves the same purpose as APIContext: it is a cache for permission related data. - The F3 driver functions verify permissions by calling helpers that rely on the F3 Context to figure out if it can proceed (e.g. [Get on an issue comment](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/driver/comment.go#L95) checks if [the commend ID is related and if the doer has permission](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/comment/comment.go#L13-L26)). - If the permission check fails, it panics with [a F3 driver specific error](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/errors/errors.go) and aborts the mirror. - The panic is recovered and the [error translated](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/mirror/remote_to_local.go#L50-L56) to a Forbidden / 403 error. An example of permission logic duplication that cannot be avoided (without a refactoring the current Forgejo permissions architecture) is the content of the [function checking the doer can read a comment](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/comment/comment.go#L13-L26). An example of permission logic reuse is the [function that calculates the permissions of the doer for a given repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/models/perm/access/repo_permission.go#L190). A [helper](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/context/context.go#L71-L87) of the F3 context can be [used by F3 driver functions](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/comment/comment.go#L21-L23) to figure out of the operation is permitted. ## Absolute ID scoping Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent. For instance, the [Get function](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/driver/comment.go#L76) on a comment [verifies the ID is related](https://code.forgejo.org/forgefriends/forgefriends/src/commit/b17a3e1f6ab1a7b696b25b9236237a417c9a9001/services/f3/permissions/comment/comment.go#L18-L20) by using the issue ID obtained from the parent in the F3 tree. It is equivalent to what [`commentAssignment`](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L238-L268) does for comment related API endpoints. ## Overview of Forgejo API permissions The permissions (aka access modes) associated to an action (API or web) of a given **doer** are calculated to either be [**read**, **write**, **admin**, **owner** or **none**](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/models/perm/access_mode.go#L13-L24). Within a repository, permissions are automatically denied if the associated **Unit** is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for **Code**, **Issues**, **Pull Request**, **Releases**. Other units are not currently relevant to F3: **Project**, **Package**, **Actions**, nor is **Wiki**. If the doer is a **collaborator** of a given repository, its permissions are futher modified (and supersedes team permissions) to be **read**, **write** or **none**. An **admin user** is automatically granted the highest permissions on the instance. The **anonymous** user and the **Ghost** user are not in scope because the F3 API requires that a token is provided. The token created on behalf of the **Forgejo Actions** user is restricted to act on the repository for which it was generated. When an **absolute ID** is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403). ### Scopes The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes: - organization - issue - repository - user And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package). ### Repository Units The [API routes](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L839) have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance `mustEnableIssuesOrPull` https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L740-L764 forbids using any routes under `/api/v1/repos/{owner}/{reponame}/issues` unless the issues unit is set on a repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L1448-L1539 Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/repo/issue.go#L497-L508 based on the permissions granted to the doer. https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/models/perm/access_mode.go#L13-L24 ### Absolute ID scoping Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in `/api/v1/repos/{owner}/{reponame}/issues/{index}`). Others contain identifiers that are absolute (e.g. the id of a comment in `/api/v1/repos/{owner}/{reponame}/issues/comments/{id}`). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository). For instance `commentAssignment` https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L238-L268 forbids using a comment ID if it does not belong to the related repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L1454-L1472 ### Doer The doer of an action can either be: - A registered user - A system user (Ghost or Forgejo Actions) - Anonymous (user is nil) The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under `/api/v1/repos/{owner}/{repo}`. - It is denied access (no read, no write) to repositories that are no the same as the repository where the actions is running - It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository - Otherwise it has write permissions on all units in the repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L195-L228
Author
Member
Copy link

The description was updated to reflect the new approach and I'll work in this direction now.

The description was updated to reflect the new approach and I'll work in this direction now.
Author
Member
Copy link

A refactor of the high level mirror logic is complete. I will now try to build on it and add more permission checks.

A refactor of the high level mirror logic is [complete](https://code.forgejo.org/forgefriends/forgefriends/src/commit/ee3ab180777f7894490b16667a5b1b8e08eca3e4). I will now try to build on it and add more permission checks.
Author
Member
Copy link

A refactor of the high level mirror logic is complete. I will now try to build on it and add more permission checks.

A refactor of the high level mirror logic is [complete](https://code.forgejo.org/forgefriends/forgefriends/src/commit/ee3ab180777f7894490b16667a5b1b8e08eca3e4). I will now try to build on it and add more permission checks.
Author
Member
Copy link

It will take another few weeks.

It will take another few weeks.
Author
Member
Copy link

I wish it was faster.

I wish it was faster.
Author
Member
Copy link

Analysis of the existing permissions check when creating, editing and deleting a comment, via the REST API or the Web UI.


Web UI

POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments

repo.NewComment

Fails if

  • !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull))
  • issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin

Routes

POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments/{id}

repo.UpdateCommentContent

Fails if

  • comment.Issue.RepoID != ctx.Repo.Repository.ID
  • !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull))
  • !comment.Type.HasContentSupport()

Routes

Middlewares

Data retrieval

context.RepoAssignment

It collects, computes and caches a large number of permission related information but does not verify any.

Features

context.RepoMustNotBeArchived

The IsArchived field of the repository is false.

Permissions

repo.MustAllowUserComment

checkIssueRights

For a pull request, check if the pull request unit has read permissions
For an issue, check if the issue unit has read permissions

verifyAuthWithOptions

Base function of the middlewares:

  • reqSignIn
  • reqSignOut
  • ignSignIn / optSignIn
  • ignExploreSignIn

REST API

POST /repos/{owner}/{repo}/issues/{index}/comments

repo.CreateIssueComment

Fails if:

  • !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)
  • issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.IsUserSiteAdmin()

Routes

GET /repos/{owner}/{repo}/issues/{index}/comments

repo.ListIssueComments

Fails if:

  • !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)

Routes

GET /repos/{owner}/{repo}/issues/comments/{id}

repo.GetIssueComment

Fails if:

  • comment.Type != issues_model.CommentTypeComment

Routes

DELETE /repos/{owner}/{repo}/issues/comments/{id}

repo.DeleteIssueComment

Fails if:

  • !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull))
  • comment.Type != commentType

Routes

Middlewares

Data retrieval

The details of which data is retrieved and cached is skipped. Only authorization related
logic is explained, when relevant.

repoAssignment

If the token is an action token associated with a different repository than the one specified in the parameters, fail with a NotFound error.
If the token is an action token from a forked pull request, read access only, othewise write access to the repository.
Unit permissions are obtained from the token scope, unless the token is set to other repositories (access_model.GetUserRepoPermissionWithReducer)

Features

mustNotBeArchived

The IsArchived field of the repository is false.

mustEnableLocalIssuesIfIsIssue

Check that the issue unit is enabled or that the index parameter matches
the index of an existing pull request in the repository.

mustEnableIssuesOrPulls

Check that:

  • The repository has the issue unit enabled and the token has read permissions AND
  • The repository is a mirror or is empty (Repository.CanEnablePulls()) OR
    The repository has the pull request unit enabled and the token has read permissions

Permissions

reqToken

Check that a token is provided by the caller (not anymous, etc.).

tokenRequiresScopes

Check that the scopes given in argument are present in the token and have the required access level:

  • POST, PUT, PATCH, DELETE: auth_model.Write
  • otherwise: auth_model.Read

checkTokenPublicOnly

If the token has a public_only scope, check that all repository, organization, user or package found in the context are publicly available.

commentAssignment

Fail if the id parameter is for an issue that does not belong to the repository.

  • comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID
  • !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)
Analysis of the existing permissions check when creating, editing and deleting a comment, via the REST API or the Web UI. --- # Web UI ## [POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments][POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments] ### [repo.NewComment][repo.NewComment] Fails if - `!ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull))` - `issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin` ### Routes - [repo.MustAllowUserComment][repo.MustAllowUserComment] - [context.RepoMustNotBeArchived][context.RepoMustNotBeArchived] - [context.RepoAssignment][context.RepoAssignment] - [verifyAuthWithOptions][verifyAuthWithOptions] options.SignInRequired ## [POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments/{id}][POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments/{id}] ### [repo.UpdateCommentContent][repo.UpdateCommentContent] Fails if - `comment.Issue.RepoID != ctx.Repo.Repository.ID` - `!ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull))` - `!comment.Type.HasContentSupport()` ### Routes - [context.RepoMustNotBeArchived][context.RepoMustNotBeArchived] - [context.RepoAssignment][context.RepoAssignment] - [verifyAuthWithOptions][verifyAuthWithOptions] options.SignInRequired # Middlewares ## Data retrieval ## [context.RepoAssignment][context.RepoAssignment] It collects, computes and caches a large number of permission related information but does not verify any. ## Features ### [context.RepoMustNotBeArchived][context.RepoMustNotBeArchived] The `IsArchived` field of the repository is false. ## Permissions ### [repo.MustAllowUserComment][repo.MustAllowUserComment] - [checkIssueRights][checkIssueRights] via `GetActionIssue` ### [checkIssueRights][checkIssueRights] For a pull request, check if the pull request unit has read permissions For an issue, check if the issue unit has read permissions ### [verifyAuthWithOptions][verifyAuthWithOptions] Base function of the middlewares: - reqSignIn - reqSignOut - ignSignIn / optSignIn - ignExploreSignIn [checkIssueRights]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/repo/issue.go#L2177-L2182 [repo.MustAllowUserComment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/repo/issue.go#L103-L114 [repo.NewComment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/repo/issue.go#L3084-L3272 [POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/web.go#L1254 [POST /{username}/{reponame}/{type:^(issues|pulls)$}/{index}/comments/{id}]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/web.go#L1291 [context.RepoMustNotBeArchived]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/services/context/repo.go#L96-L102 [context.RepoAssignment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/services/context/repo.go#L371-L425 [verifyAuthWithOptions]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/web.go#L139-L237 [repo.UpdateCommentContent]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/web/repo/issue.go#L3275-L3347 --- # REST API ## [POST /repos/{owner}/{repo}/issues/{index}/comments][POST_/repos/{owner}/{repo}/issues/{index}/comments] ### [repo.CreateIssueComment][repo.CreateIssueComment] Fails if: - `!ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)` - `issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.IsUserSiteAdmin()` ### Routes - [mustNotBeArchived][mustNotBeArchived] - [reqToken][reqToken] - [mustEnableLocalIssuesIfIsIssue][mustEnableLocalIssuesIfIsIssue] - [mustEnableIssuesOrPulls][mustEnableIssuesOrPulls] - [checkTokenPublicOnly][checkTokenPublicOnly] - [repoAssignment][repoAssignment] - [tokenRequiresScopes][tokenRequiresScopes] auth_model.AccessTokenScopeCategoryIssue ## [GET /repos/{owner}/{repo}/issues/{index}/comments][GET_/repos/{owner}/{repo}/issues/{index}/comments] ### [repo.ListIssueComments][repo.ListIssueComments] Fails if: - `!ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)` ### Routes - [mustEnableLocalIssuesIfIsIssue][mustEnableLocalIssuesIfIsIssue] - [mustEnableIssuesOrPulls][mustEnableIssuesOrPulls] - [checkTokenPublicOnly][checkTokenPublicOnly] - [repoAssignment][repoAssignment] - [tokenRequiresScopes][tokenRequiresScopes] auth_model.AccessTokenScopeCategoryIssue ## [GET /repos/{owner}/{repo}/issues/comments/{id}][GET_/repos/{owner}/{repo}/issues/comments/{id}] ### [repo.GetIssueComment][repo.GetIssueComment] Fails if: - `comment.Type != issues_model.CommentTypeComment` ### Routes - [commentAssignment][commentAssignment] - [mustEnableIssuesOrPulls][mustEnableIssuesOrPulls] - [checkTokenPublicOnly][checkTokenPublicOnly] - [repoAssignment][repoAssignment] - [tokenRequiresScopes][tokenRequiresScopes] auth_model.AccessTokenScopeCategoryIssue ## [DELETE /repos/{owner}/{repo}/issues/comments/{id}][DELETE_/repos/{owner}/{repo}/issues/comments/{id}] ### [repo.DeleteIssueComment][repo.DeleteIssueComment] Fails if: - `!ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull))` - `comment.Type != commentType` ### Routes - [reqToken][reqToken] - [commentAssignment][commentAssignment] - [mustEnableIssuesOrPulls][mustEnableIssuesOrPulls] - [checkTokenPublicOnly][checkTokenPublicOnly] - [repoAssignment][repoAssignment] - [tokenRequiresScopes][tokenRequiresScopes] auth_model.AccessTokenScopeCategoryIssue # Middlewares ## Data retrieval The details of which data is retrieved and cached is skipped. Only authorization related logic is explained, when relevant. ### [repoAssignment][repoAssignment] If the token is an action token associated with a different repository than the one specified in the parameters, fail with a `NotFound` error. If the token is an action token from a forked pull request, read access only, othewise write access to the repository. Unit permissions are obtained from the token scope, unless the token is set to other repositories (`access_model.GetUserRepoPermissionWithReducer`) ## Features ### [mustNotBeArchived][mustNotBeArchived] The `IsArchived` field of the repository is false. ### [mustEnableLocalIssuesIfIsIssue][mustEnableLocalIssuesIfIsIssue] Check that the issue unit is enabled or that the `index` parameter matches the index of an existing pull request in the repository. ### [mustEnableIssuesOrPulls][mustEnableIssuesOrPulls] Check that: - The repository has the issue unit enabled and the token has read permissions AND - The repository is a mirror or is empty (`Repository.CanEnablePulls()`) OR The repository has the pull request unit enabled and the token has read permissions ## Permissions ### [reqToken][reqToken] Check that a token is provided by the caller (not anymous, etc.). ### [tokenRequiresScopes][tokenRequiresScopes] Check that the scopes given in argument are present in the token and have the required access level: - POST, PUT, PATCH, DELETE: auth_model.Write - otherwise: auth_model.Read ### [checkTokenPublicOnly][checkTokenPublicOnly] If the token has a `public_only` scope, check that all repository, organization, user or package found in the context are publicly available. ### [commentAssignment][commentAssignment] Fail if the `id` parameter is for an issue that does not belong to the repository. - `comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID` - `!ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)` [tokenRequiresScopes]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L326-L360 [mustNotBeArchived]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L782-L787 [reqToken]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L373-L386 [mustEnableLocalIssuesIfIsIssue]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L755-L773 [mustEnableIssuesOrPulls]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L729-L753 [commentAssignment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L227-L257 [checkTokenPublicOnly]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L268-L322 [repoAssignment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L130-L224 [repo.CreateIssueComment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/repo/issue_comment.go#L369-L448 [repo.ListIssueComments]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/repo/issue_comment.go#L28-L129 [repo.DeleteIssueComment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/repo/issue_comment.go#L714-L730 [repo.GetIssueComment]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/repo/issue_comment.go#L451-L506 [POST_/repos/{owner}/{repo}/issues/{index}/comments]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L1474 [GET_/repos/{owner}/{repo}/issues/{index}/comments]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L1473 [GET_/repos/{owner}/{repo}/issues/comments/{id}]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L1450 [DELETE_/repos/{owner}/{repo}/issues/comments/{id}]: https://code.forgejo.org/forgefriends/forgefriends/src/commit/5dd3ead0754b279b150d7f8147455ac555d42a87/routers/api/v1/api.go#L1452
Author
Member
Copy link

After writing the detailed analysis related to permission checks, I was discouraged by the amount of work necessary to do the same for all API endpoints required by F3. And quite skeptical about the maintainability of the result. And very pessimistic about the chances of anyone having time to review the changes required for the associated pull requests to enter the codebase.

But today I had an epiphany and there may be a solution that is still significant work but much more likely to succeed and be maintainable. In a nutshell:

  • Ignore the web API entirely, focus on implementing F3 permissions using REST API functions
  • Do not introduce new permission check functions, re-use the REST API middleware permission check functions
  • Pair each F3 API endpoint with a REST API endpoint
  • Automate the verification that a F3 API endpoint uses the same permission check functions, in the same order, as the matching REST API endpoint

The epiphany is that the sequence of permission checks scattered in the routes of the REST API are strictly equivalent to the same permissions checks done in the same order for an equivalent operation in F3.

To help maintain consistency with the REST API, the permissions check functions can collect and store the sequence of their calls when running tests so that they can be compared to be the same as when called from the F3 tests.

There will still be some code duplication but this is much more promising and I'll go in this direction.

After writing the [detailed analysis related to permission checks](/forge/organizations/6/projects/115/issues/42/comments/1990), I was discouraged by the amount of work necessary to do the same for all API endpoints required by F3. And quite skeptical about the maintainability of the result. And very pessimistic about the chances of anyone having time to review the changes required for the associated pull requests to enter the codebase. But today I had an epiphany and there may be a solution that is still significant work but much more likely to succeed and be maintainable. In a nutshell: - Ignore the web API entirely, focus on implementing F3 permissions using REST API functions - Do not introduce new permission check functions, re-use the REST API middleware permission check functions - Pair each F3 API endpoint with a REST API endpoint - Automate the verification that a F3 API endpoint uses the same permission check functions, in the same order, as the matching REST API endpoint The epiphany is that the sequence of permission checks scattered in the routes of the REST API are strictly equivalent to the same permissions checks done in the same order for an equivalent operation in F3. To help maintain consistency with the REST API, the permissions check functions can collect and store the sequence of their calls when running tests so that they can be compared to be the same as when called from the F3 tests. There will still be some code duplication but this is much more promising and I'll go in this direction.
Author
Member
Copy link

@mfenniak since you've recently worked in this area of the code, I hope you will have time for a quick review of this draft pull request. In a nutshell it will allow F3 to re-use most of the permission check logic from the REST API. 🙏

@mfenniak since you've recently worked in this area of the code, I hope you will have time for a quick review of [this draft pull request](https://codeberg.org/forgejo/forgejo/pulls/12398). In a nutshell it will allow F3 to re-use most of the permission check logic from the REST API. 🙏
Author
Member
Copy link

Thanks to the permission related middleware distinctive signature, the list of middleware applied for a given REST API route is calculated when in test mode and can then be used by F3 functions to check the same permission checks are applied and fail if they are different.

method POST, pattern /repos/{username}/{reponame}/issues/{index}/reactions, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken, function repo.PostIssueReaction
method DELETE, pattern /repos/{username}/{reponame}/issues/{index}/reactions, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken, function repo.DeleteIssueReaction
method GET, pattern /repos/{username}/{reponame}/issues/{index}/assets, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls, function repo.ListIssueAttachments
method POST, pattern /repos/{username}/{reponame}/issues/{index}/assets, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.CreateIssueAttachment
method GET, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls, function repo.GetIssueAttachment
method PATCH, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.EditIssueAttachment
method DELETE, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.DeleteIssueAttachment
Thanks to the permission related middleware distinctive signature, the [list of middleware applied for a given REST API route is calculated when in test mode](https://code.forgejo.org/forgefriends/forgefriends/commit/83085650a6ea7da1ebfb0524e6871982fc476ab5#diff-50786303a8741f4b264404fb91fa6d2ce8cdd519) and can then be used by F3 functions to check the same permission checks are applied and fail if they are different. ``` method POST, pattern /repos/{username}/{reponame}/issues/{index}/reactions, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken, function repo.PostIssueReaction method DELETE, pattern /repos/{username}/{reponame}/issues/{index}/reactions, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken, function repo.DeleteIssueReaction method GET, pattern /repos/{username}/{reponame}/issues/{index}/assets, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls, function repo.ListIssueAttachments method POST, pattern /repos/{username}/{reponame}/issues/{index}/assets, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.CreateIssueAttachment method GET, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls, function repo.GetIssueAttachment method PATCH, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.EditIssueAttachment method DELETE, pattern /repos/{username}/{reponame}/issues/{index}/assets/{attachment_id}, permissions v1.checkTokenPublicOnly,v1.mustEnableIssuesOrPulls,v1.reqToken,v1.mustNotBeArchived, function repo.DeleteIssueAttachment ```
Author
Member
Copy link

The refactor with the new design is completed and I have a good feeling about it. I have updated the description accordingly, the significant change are copied below.


The Forgejo REST API permissions are implemented in functions or methods that F3 must re-use to avoid code duplication. There are dozens of REST API endpoints and the permissions they enforce are tested by a number of tests. It would be difficult to re-create such tests for permission checks created specifically for F3. They would also be difficult to maintain.

There is a single API endpoint for F3 mirroring. It requires a PAT that must be used by the F3 driver before performing an operation.

For instance, the Get() driver function to read a repository must perform the same permission checks as the GET /repos/{owner}/{repo} REST API endpoint.

To achieve this re-use:

All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the sequence of permissions checks are harvested for each REST API call when running tests and verified to match what F3 functions do by collecting calls and matching them.

The refactor [with the new design is completed](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf) and I have a good feeling about it. I have updated the description accordingly, the significant change are copied below. --- The Forgejo REST API permissions are implemented in functions or methods that F3 must re-use to avoid code duplication. There are dozens of REST API endpoints and the permissions they enforce are tested by a number of tests. It would be difficult to re-create such tests for permission checks created specifically for F3. They would also be difficult to maintain. There is [a single API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/f3/mirror.go#L20) for F3 mirroring. It requires a PAT that must be used by [the F3 driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver) before performing an operation. For instance, the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver/project.go#L131-L149) must perform the same permission checks as the [`GET /repos/{owner}/{repo}` REST API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1145). To achieve this re-use: - the middleware functions that perform permission checks are refactored to use [an interface](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/permissions/interface.go) rather than a struct. The methods provided by the interface are exclusively used for data storage. The logic is unchanged. See [the refactored RepoAccess middleware](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L184-L225) for an example. - the F3 function calls the REST API permission checks in the same order they are specified in the routes of the corresponding REST API endpoint. See how the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver/project.go#L146) calls [`RepoAccess` and `CheckTokenPublicOnly`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/project/project.go#L16-L17) in the same order the [REST API route to `GET /repos/{owner}/{repo}`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1145) calls them as [specified by the route](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1440) that leads to the endpoint. All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the [sequence of permissions checks are harvested for each REST API call when running tests](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/permissions/testing.go) and verified to match what F3 functions do by [collecting calls](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/api/proxy.go#L28) and [matching them](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/check.go#L27).
Author
Member
Copy link

I realized today that the refactoring of the REST API middlewares could be quite beneficial to Forgejo independent of F3. Because it creates the conditions to improve permissions checks testing. I moved this particular discussion in its own design unit. I have explored and implemented enough edge cases by now to be convinced this is a good path forward.

Implementing the REST API permissions check tests is an extra effort but I'm happy to do it because:

  • It is doable in a finite amount of time (there are not too many middleware)
  • Since it benefits Forgejo, It is much more likely to be merged than a refactor that only benefits F3
  • It does not introduce any functional changes

#63

I realized today that the refactoring of the REST API middlewares could be quite beneficial to Forgejo independent of F3. Because it creates the conditions to improve permissions checks testing. I moved this particular discussion in its own design unit. I have explored and implemented enough edge cases by now to be convinced this is a good path forward. Implementing the REST API permissions check tests is an extra effort but I'm happy to do it because: - It is doable in a finite amount of time (there are not too many middleware) - Since it benefits Forgejo, It is much more likely to be merged than a refactor that only benefits F3 - It does not introduce any functional changes https://codeberg.org/forgejo/design/issues/63
Author
Member
Copy link

For the record, here is the description of this design as it was before the Facilitate and improve the testing of the REST API permissions checks design was implemented in June 2026.

design as of June 2026

The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge.

For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with:

curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"https://codeberg.org", "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3

The Forgejo F3 driver based on gof3 performs the following:

  • Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository
  • Create a F3 archive in a directory, as defined by F3
  • Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository

Since it is not using the REST API, each operation of this internal driver must verify that the doer (i.e. the user used to create the F3_LOCAL_TOKEN token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels).

Implementation

Draft

Note

drafts are developed and published in the forgefriends repository

Proposed Design

The /api/v1/f3 endpoint to create or update the mirror of a remote repository is given:

  • The URL of the remote forge (e.g. https://codeberg.org)
  • The F3 path to mirror on the remote forge (e.g. /forge/users/myuser/projects/myproject)
  • A token with enough permissions to read the repository from the remote forge

The gof3 package implements the high level mirroring logic and relies on:

This driver provides the following functions for each resource:

  • List (e.g. listing all comments of an issue in a way similar to GET /repos/{owner}/{repo}/issues/comments)
  • Get (e.g. read a comment in a way similar to GET /repos/{owner}/{repo}/issues/comments/{id})
  • Put (e.g. create a comment in a way similar to POST /repos/{owner}/{repo}/issues/{index}/comments)
  • Patch (e.g. edit a comment in a way similar to PATCH /repos/{owner}/{repo}/issues/comments/{id})
  • Delete (e.g. delete a comment in a way similar to DELETE /repos/{owner}/{repo}/issues/comments/{id})

Each driver function call is responsible to enforce the required permissions.

Enforcing permissions

The Forgejo REST API permissions are implemented in functions or methods that F3 must re-use to avoid code duplication. There are dozens of REST API endpoints and the permissions they enforce are tested by a number of tests. It would be difficult to re-create such tests for permission checks created specifically for F3. They would also be difficult to maintain.

There is a single API endpoint for F3 mirroring. It requires a PAT that must be used by the F3 driver before performing an operation.

For instance, the Get() driver function to read a repository must perform the same permission checks as the GET /repos/{owner}/{repo} REST API endpoint.

To achieve this re-use:

All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the sequence of permissions checks are harvested for each REST API call when running tests and verified to match what F3 functions do by collecting calls and matching them.

Absolute ID scoping

Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent.

For instance, the Get function on a comment must verify the ID is related by using the issue ID obtained from the parent in the F3 tree.

It must use commentAssignment.

Overview of Forgejo API permissions

The permissions (aka access modes) associated with an action (REST or web) of a given doer are calculated to either be read, write, admin, owner or none.

Within a repository, permissions are automatically denied if the associated Unit is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for Code, Issues, Pull Request, Releases. Other units are not currently relevant to F3: Project, Package, Actions, nor is Wiki. If the doer is a collaborator of a given repository, its permissions are futher modified (and supersedes team permissions) to be read, write or none.

An admin user is automatically granted the highest permissions on the instance. The anonymous user and the Ghost user are not in scope because the F3 API requires that a token is provided. The token created on behalf of the Forgejo Actions user is restricted to act on the repository for which it was generated.

When an absolute ID is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403).

Scopes

The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes:

  • organization
  • issue
  • repository
  • user

And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package).

Repository Units

The API routes have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance mustEnableIssuesOrPull

funcmustEnableIssuesOrPulls(ctx*context.APIContext){
if!ctx.Repo.CanRead(unit.TypeIssues)&&
(!ctx.Repo.Repository.CanEnablePulls()||!ctx.Repo.CanRead(unit.TypePullRequests)){
ifctx.Repo.Repository.CanEnablePulls()&&log.IsTrace(){
ifctx.IsSigned{
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
"User in Repo has Permissions: %-+v",
ctx.Doer,
unit.TypeIssues,
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
}else{
log.Trace("Permission Denied: Anonymous user cannot read %-v and %-v in Repo %-v\n"+
"Anonymous user in Repo has Permissions: %-+v",
unit.TypeIssues,
unit.TypePullRequests,
ctx.Repo.Repository,
ctx.Repo.Permission)
}
}
ctx.NotFound()
return
}
}

forbids using any routes under /api/v1/repos/{owner}/{reponame}/issues unless the issues unit is set on a repository

Preview has been truncated
m.Group("/issues",func(){
m.Combo("").Get(repo.ListIssues).
Post(reqToken(),mustNotBeArchived,bind(api.CreateIssueOption{}),reqRepoReader(unit.TypeIssues),repo.CreateIssue)
m.Get("/pinned",reqRepoReader(unit.TypeIssues),repo.ListPinnedIssues)
m.Group("/comments",func(){
m.Get("",repo.ListRepoIssueComments)
m.Group("/{id}",func(){
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived,reqToken(),bind(api.EditIssueCommentOption{}),repo.EditIssueComment).
Delete(reqToken(),repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(),bind(api.EditReactionOption{}),repo.PostIssueCommentReaction).
Delete(reqToken(),bind(api.EditReactionOption{}),repo.DeleteIssueCommentReaction)
m.Group("/assets",func(){
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(),mustNotBeArchived,context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues,context.QuotaTargetRepo),repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(),mustNotBeArchived,bind(api.EditAttachmentOptions{}),repo.EditIssueCommentAttachment).
Delete(reqToken(),mustNotBeArchived,repo.DeleteIssueCommentAttachment)
},mustEnableAttachments)
},commentAssignment(":id"))
})
m.Group("/{index}",func(){
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(),bind(api.EditIssueOption{}),repo.EditIssue).
Delete(reqToken(),reqAdmin(),context.ReferencesGitRepo(),repo.DeleteIssue)
m.Group("/comments",func(){
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(),mustNotBeArchived,bind(api.CreateIssueCommentOption{}),repo.CreateIssueComment)
m.Combo("/{id}",reqToken(),commentAssignment(":id")).Patch(bind(api.EditIssueCommentOption{}),repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
m.Get("/timeline",repo.ListIssueCommentsAndTimeline)
m.Group("/labels",func(){
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(),bind(api.IssueLabelsOption{}),repo.AddIssueLabels).
Put(reqToken(),bind(api.IssueLabelsOption{}),repo.ReplaceIssueLabels).
Delete(reqToken(),bind(api.DeleteLabelsOption{}),repo.ClearIssueLabels)
m.Delete("/{identifier}",reqToken(),bind(api.DeleteLabelsOption{}),repo.DeleteIssueLabel)
})
m.Group("/times",func(){
m.Combo("").
Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}),repo.AddTime).
Delete(repo.ResetIssueTime)
m.Delete("/{id}",repo.DeleteTime)

Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions

if!isPull.Has(){
canReadIssues:=ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls:=ctx.Repo.CanRead(unit.TypePullRequests)
if!canReadIssues&&!canReadPulls{
ctx.NotFound()
return
}elseif!canReadIssues{
isPull=optional.Some(true)
}elseif!canReadPulls{
isPull=optional.Some(false)
}
}

based on the permissions granted to the doer.

const(
// AccessModeNone no access
AccessModeNoneAccessMode=iota// 0
// AccessModeRead read access
AccessModeRead// 1
// AccessModeWrite write access
AccessModeWrite// 2
// AccessModeAdmin admin access
AccessModeAdmin// 3
// AccessModeOwner owner access
AccessModeOwner// 4
)

Absolute ID scoping

Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in /api/v1/repos/{owner}/{reponame}/issues/{index}). Others contain identifiers that are absolute (e.g. the id of a comment in /api/v1/repos/{owner}/{reponame}/issues/comments/{id}). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository).

For instance commentAssignment

funccommentAssignment(idParamstring)func(ctx*context.APIContext){
returnfunc(ctx*context.APIContext){
comment,err:=issues_model.GetCommentByID(ctx,ctx.ParamsInt64(idParam))
iferr!=nil{
ifissues_model.IsErrCommentNotExist(err){
ctx.NotFound(err)
}else{
ctx.InternalServerError(err)
}
return
}
iferr=comment.LoadIssue(ctx);err!=nil{
ctx.InternalServerError(err)
return
}
ifcomment.Issue==nil||comment.Issue.RepoID!=ctx.Repo.Repository.ID{
ctx.NotFound()
return
}
if!ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull){
ctx.NotFound()
return
}
comment.Issue.Repo=ctx.Repo.Repository
ctx.Comment=comment
}
}

forbids using a comment ID if it does not belong to the related repository

m.Group("/{id}",func(){
m.Combo("").
Get(repo.GetIssueComment).
Patch(mustNotBeArchived,reqToken(),bind(api.EditIssueCommentOption{}),repo.EditIssueComment).
Delete(reqToken(),repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
Post(reqToken(),bind(api.EditReactionOption{}),repo.PostIssueCommentReaction).
Delete(reqToken(),bind(api.EditReactionOption{}),repo.DeleteIssueCommentReaction)
m.Group("/assets",func(){
m.Combo("").
Get(repo.ListIssueCommentAttachments).
Post(reqToken(),mustNotBeArchived,context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues,context.QuotaTargetRepo),repo.CreateIssueCommentAttachment)
m.Combo("/{attachment_id}").
Get(repo.GetIssueCommentAttachment).
Patch(reqToken(),mustNotBeArchived,bind(api.EditAttachmentOptions{}),repo.EditIssueCommentAttachment).
Delete(reqToken(),mustNotBeArchived,repo.DeleteIssueCommentAttachment)
},mustEnableAttachments)
},commentAssignment(":id"))

Doer

The doer of an action can either be:

  • A registered user
  • A system user (Ghost or Forgejo Actions)
  • Anonymous (user is nil)

The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under /api/v1/repos/{owner}/{repo}.

  • It is denied access (no read, no write) to repositories that are no the same as the repository where the actions is running
  • It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository
  • Otherwise it has write permissions on all units in the repository
ifctx.Doer!=nil&&ctx.Doer.ID==user_model.ActionsUserID{
taskID:=ctx.Data["ActionsTaskID"].(int64)
task,err:=actions_model.GetTaskByID(ctx,taskID)
iferr!=nil{
ctx.Error(http.StatusInternalServerError,"actions_model.GetTaskByID",err)
return
}
iftask.RepoID!=repo.ID{
ctx.NotFound()
return
}
iftask.IsForkPullRequest{
ctx.Repo.AccessMode=perm.AccessModeRead
}else{
ctx.Repo.AccessMode=perm.AccessModeWrite
}
iferr:=ctx.Repo.Repository.LoadUnits(ctx);err!=nil{
ctx.Error(http.StatusInternalServerError,"LoadUnits",err)
return
}
ctx.Repo.Units=ctx.Repo.Repository.Units
ctx.Repo.UnitsMode=make(map[unit.Type]perm.AccessMode)
for_,u:=rangectx.Repo.Repository.Units{
ctx.Repo.UnitsMode[u.Type]=ctx.Repo.AccessMode
}
}else{
ctx.Repo.Permission,err=access_model.GetUserRepoPermission(ctx,repo,ctx.Doer)
iferr!=nil{
ctx.Error(http.StatusInternalServerError,"GetUserRepoPermission",err)
return
}
}
For the record, here is the description of this design as it was before the [Facilitate and improve the testing of the REST API permissions checks](https://codeberg.org/forgejo/design/issues/63) design was implemented in June 2026. <details> <summary>design as of June 2026</summary> The purpose of this design is to define how to enforce permissions when F3 is used to mirror a remote forge to a local forge. For instance, mirroring https://codeberg.org/forgejo/design to a Forgejo instance running from source can be done with: ```sh curl -sS -H "Authorization: token $F3_LOCAL_TOKEN" --json '{"url":"https://codeberg.org", "token": "'$F3_CODEBERG_ORG_TOKEN'", "type": "forgejo", "from": "/forge/organizations/forgejo/projects/design"}' http://0.0.0.0:3000/api/v1/f3 ``` The [Forgejo F3 driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver) based on [gof3](https://code.forgejo.org/f3/gof3) performs the following: - Use the API with the F3_CODEBERG_ORG_TOKEN token to read from the https://codeberg.org/forgejo/design repository - Create a F3 archive in a directory, as defined by [F3](https://f3.forgefriends.org/) - Read those files and modify the Forgejo database and files (just as a series of API call would) to create or update the local copy of the design repository Since it is not using the REST API, each operation of this internal driver must verify that the doer (i.e. the user used to create the `F3_LOCAL_TOKEN` token in the example above) has the required permissions (for instance the author of an issue may not be allowed to set the labels). # Implementation ## Draft > **Note** drafts are developed and published in the forgefriends repository - [API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/f3/mirror.go#L20) # Proposed Design The `/api/v1/f3` endpoint to create or update the mirror of a remote repository is given: - The URL of the remote forge (e.g. https://codeberg.org) - The F3 path to mirror on the remote forge (e.g. `/forge/users/myuser/projects/myproject`) - A token with enough permissions to read the repository from the remote forge The [gof3 package](https://code.forgejo.org/f3/gof3) implements the high level mirroring logic and relies on: - the drivers [provided by the gof3 package](https://code.forgejo.org/f3/gof3/src/commit/d70169481604dccd5ecff9d91f5a74fe3fdd76ad/forges) to read the remote forge. The permissions are enforced by the remote forge and is not in scope for this discussion. - the [F3 internal driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver) to write to the local Forgejo instance. This driver provides the following functions for each resource: - List (e.g. listing all comments of an issue in a way similar to `GET /repos/{owner}/{repo}/issues/comments`) - Get (e.g. read a comment in a way similar to `GET /repos/{owner}/{repo}/issues/comments/{id}`) - Put (e.g. create a comment in a way similar to `POST /repos/{owner}/{repo}/issues/{index}/comments`) - Patch (e.g. edit a comment in a way similar to `PATCH /repos/{owner}/{repo}/issues/comments/{id}`) - Delete (e.g. delete a comment in a way similar to `DELETE /repos/{owner}/{repo}/issues/comments/{id}`) Each driver function call is responsible to enforce the required permissions. ## Enforcing permissions The Forgejo REST API permissions are implemented in functions or methods that F3 must re-use to avoid code duplication. There are dozens of REST API endpoints and the permissions they enforce are tested by a number of tests. It would be difficult to re-create such tests for permission checks created specifically for F3. They would also be difficult to maintain. There is [a single API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/f3/mirror.go#L20) for F3 mirroring. It requires a PAT that must be used by [the F3 driver](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver) before performing an operation. For instance, the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver/project.go#L131-L149) must perform the same permission checks as the [`GET /repos/{owner}/{repo}` REST API endpoint](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1145). To achieve this re-use: - the middleware functions that perform permission checks are refactored to use [an interface](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/permissions/interface.go) rather than a struct. The methods provided by the interface are exclusively used for data storage. The logic is unchanged. See [the refactored RepoAccess middleware](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L184-L225) for an example. - the F3 function calls the REST API permission checks in the same order they are specified in the routes of the corresponding REST API endpoint. See how the [Get() driver function to read a repository](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver/project.go#L146) calls [`RepoAccess` and `CheckTokenPublicOnly`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/project/project.go#L16-L17) in the same order the [REST API route to `GET /repos/{owner}/{repo}`](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1145) calls them as [specified by the route](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/api.go#L1440) that leads to the endpoint. All F3 function are paired with a REST API endpoint and automated tests verify that both run the same permission checks in the same order. Figuring out which permissions checks are performed by a given REST API endpoint is not trivial. The automated tests that fail when the checks change in the REST API are essential to the maintainability of F3. It would otherwise require a scrutiny that cannot reasonably be expected from maintainers. See how the [sequence of permissions checks are harvested for each REST API call when running tests](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/routers/api/v1/permissions/testing.go) and verified to match what F3 functions do by [collecting calls](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/api/proxy.go#L28) and [matching them](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/permissions/check.go#L27). ## Absolute ID scoping Every F3 resource (e.g. issue, comment, ...) is guaranteed to have a parent. When the ID of the resource is absolute (e.g. comment), it can be verified to belong to the parent. For instance, the [Get function](https://code.forgejo.org/forgefriends/forgefriends/src/commit/0a7133ea92a52932c57a68d74ac151da84b8b8cf/services/f3/driver/comment.go#L76) on a comment must verify the ID is related by using the issue ID obtained from the parent in the F3 tree. It must use [`commentAssignment`](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L238-L268). ## Overview of Forgejo API permissions The permissions (aka access modes) associated with an action (REST or web) of a given **doer** are calculated to either be [**read**, **write**, **admin**, **owner** or **none**](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/models/perm/access_mode.go#L13-L24). Within a repository, permissions are automatically denied if the associated **Unit** is disabled (e.g. if issues are disabled on a repository). For repositories that belong to an organization, team members permissions may be further modified for **Code**, **Issues**, **Pull Request**, **Releases**. Other units are not currently relevant to F3: **Project**, **Package**, **Actions**, nor is **Wiki**. If the doer is a **collaborator** of a given repository, its permissions are futher modified (and supersedes team permissions) to be **read**, **write** or **none**. An **admin user** is automatically granted the highest permissions on the instance. The **anonymous** user and the **Ghost** user are not in scope because the F3 API requires that a token is provided. The token created on behalf of the **Forgejo Actions** user is restricted to act on the repository for which it was generated. When an **absolute ID** is unrelated to its parents (e.g. a comment ID unrelated to a comment), access is denied as if it did not exist (i.e. it is 404 instead of 403). ### Scopes The token used to authentify the doer may further limit the permissions granted to the user that owns it with the following scopes: - organization - issue - repository - user And other scopes that are not currently relevant for F3 (misc, activitypub, notifications, package). ### Repository Units The [API routes](https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L839) have nested checks verifying the features (units) related to a resource (issue, release, etc.) are enabled. For instance `mustEnableIssuesOrPull` https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L740-L764 forbids using any routes under `/api/v1/repos/{owner}/{reponame}/issues` unless the issues unit is set on a repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L1448-L1539 Once the router checks have passed, it is the responsibility of each REST API endpoint to enforce additional restrictions https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/repo/issue.go#L497-L508 based on the permissions granted to the doer. https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/models/perm/access_mode.go#L13-L24 ### Absolute ID scoping Some API routes contain identifiers that are relative to their parent (e.g. the index of an issue in `/api/v1/repos/{owner}/{reponame}/issues/{index}`). Others contain identifiers that are absolute (e.g. the id of a comment in `/api/v1/repos/{owner}/{reponame}/issues/comments/{id}`). An attempt to use an absolute ID unrelated to the resource (e.g. a repository) must be forbidden as it would escalate the permissions of the doer (e.g. read a comment from any private repository). For instance `commentAssignment` https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L238-L268 forbids using a comment ID if it does not belong to the related repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L1454-L1472 ### Doer The doer of an action can either be: - A registered user - A system user (Ghost or Forgejo Actions) - Anonymous (user is nil) The Forgejo Actions automatic token is bound to the Forgejo Actions system user and subject to special restrictions on all REST API endpoints under `/api/v1/repos/{owner}/{repo}`. - It is denied access (no read, no write) to repositories that are no the same as the repository where the actions is running - It has read permissions on all units if the the action is running on a pull request and the head if it is from a forked repository - Otherwise it has write permissions on all units in the repository https://codeberg.org/forgejo/forgejo/src/commit/3855eeb6c5419d119a8e947ff2aabaabf9f56189/routers/api/v1/api.go#L195-L228 </details>
Author
Member
Copy link

The description was updated to take into account that the Facilitate and improve the testing of the REST API permissions checks design is implemented in Forgejo and F3 can make use of the facility it provides which consists of pairing a F3 operation with a REST API endpoint to verify that both rely on the same sequence of permissions functions.

F3 needs additional testing to complement the MustMatch function because the permission functions are only as good as the parameters they are given. The next steps of this design will be to:

  • Implement permission tests as part of the F3 compliance suite. It currently does not provide anything of the sort and assumes all permission checks are the responsibility of the forge and not the driver. This is a fair assumption when dealing with a remote forge. But Not for a driver that runs inside the forge itself.
  • Pair each driver function with their REST API equivalent. There will surely be exceptions that do not quite fit. Or others that require extracting the permission logic from an existing REST API handler (such as chore: add coverage for REST API fork destination permission check)
The description was updated to take into account that the [Facilitate and improve the testing of the REST API permissions checks](https://codeberg.org/forgejo/design/issues/63) design is implemented in Forgejo and F3 can make use of the facility it provides which consists of pairing a F3 operation with a REST API endpoint to verify that both rely on the same sequence of permissions functions. F3 needs additional testing to complement the `MustMatch` function because the permission functions are only as good as the parameters they are given. The next steps of this design will be to: - Implement permission tests as part of the F3 compliance suite. It currently does not provide anything of the sort and assumes all permission checks are the responsibility of the forge and not the driver. This is a fair assumption when dealing with a remote forge. But Not for a driver that runs inside the forge itself. - Pair each driver function with their REST API equivalent. There will surely be exceptions that do not quite fit. Or others that require extracting the permission logic from an existing REST API handler (such as [chore: add coverage for REST API fork destination permission check](https://codeberg.org/forgejo/forgejo/pulls/13151))
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
2 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#61
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?